git.gnu.io has moved to IP address 209.51.188.249 -- please double check where you are logging in.

FacebookBridgePlugin.php 16.2 KB
Newer Older
1 2 3
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2010-2011, StatusNet, Inc.
5
 *
6 7
 * A plugin for integrating Facebook with StatusNet. Includes single-sign-on
 * and publishing notices to Facebook using Facebook's Graph API.
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
24
 * @category  Plugin
25 26
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
27
 * @copyright 2011 StatusNet, Inc.
28 29 30 31 32 33 34 35
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

36 37
define("FACEBOOK_SERVICE", 2);

38
/**
39
 * Main class for Facebook Bridge plugin
40 41 42 43
 *
 * @category  Plugin
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
44
 * @copyright 2010-2011 StatusNet, Inc.
45 46 47
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
48
class FacebookBridgePlugin extends Plugin
49
{
50 51 52
    public $appId;  // Facebook application ID
    public $secret; // Facebook application secret

53
    public $facebook = null; // Facebook application instance
54
    public $dir      = null; // Facebook plugin dir
55 56 57 58

    /**
     * Initializer for this plugin
     *
59
     * Gets an instance of the Facebook API client object
60 61 62 63 64
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function initialize()
    {
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

        // Allow the id and key to be passed in
        // Control panel will override

        if (isset($this->appId)) {
            $appId = common_config('facebook', 'appid');
            if (empty($appId)) {
                Config::save(
                    'facebook',
                    'appid',
                    $this->appId
                );
            }
        }

        if (isset($this->secret)) {
            $secret = common_config('facebook', 'secret');
            if (empty($secret)) {
                Config::save('facebook', 'secret', $this->secret);
            }
        }

Zach Copley's avatar
Zach Copley committed
87 88 89 90
        $this->facebook = Facebookclient::getFacebook(
            $this->appId,
            $this->secret
        );
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107

        return true;
    }

    /**
     * Load related modules when needed
     *
     * @param string $cls Name of the class to be loaded
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onAutoload($cls)
    {
        $dir = dirname(__FILE__);

        switch ($cls)
        {
108
        case 'Facebook': // Facebook PHP SDK
109
            include_once $dir . '/extlib/base_facebook.php';
110 111 112
            include_once $dir . '/extlib/facebook.php';
            return false;
        }
113 114

        return parent::onAutoload($cls);
115 116
    }

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    /**
     * Database schema setup
     *
     * We maintain a table mapping StatusNet notices to Facebook items
     *
     * @see Schema
     * @see ColumnDef
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('notice_to_item', Notice_to_item::schemaDef());
        return true;
    }

Zach Copley's avatar
Zach Copley committed
134 135 136 137 138 139
    /*
     * Does this $action need the Facebook JavaScripts?
     */
    function needsScripts($action)
    {
        static $needy = array(
140
            'FacebookloginAction',
141
            'FacebookfinishloginAction',
Zach Copley's avatar
Zach Copley committed
142 143 144 145 146 147 148 149 150 151 152
            'FacebookadminpanelAction',
            'FacebooksettingsAction'
        );

        if (in_array(get_class($action), $needy)) {
            return true;
        } else {
            return false;
        }
    }

153 154 155
    /**
     * Map URLs to actions
     *
156
     * @param URLMapper $m path-to-action mapper
157 158 159
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
160
    public function onRouterInitialized(URLMapper $m)
161 162
    {
        // Always add the admin panel route
163
        $m->connect('panel/facebook', array('action' => 'facebookadminpanel'));
164

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
        $m->connect(
            'main/facebooklogin',
            array('action' => 'facebooklogin')
        );
        $m->connect(
            'main/facebookfinishlogin',
            array('action' => 'facebookfinishlogin')
        );
        $m->connect(
            'settings/facebook',
            array('action' => 'facebooksettings')
        );
        $m->connect(
            'facebook/deauthorize',
            array('action' => 'facebookdeauthorize')
        );
181 182 183 184 185 186 187 188

        return true;
    }

    /*
     * Add a login tab for Facebook, but only if there's a Facebook
     * application defined for the plugin to use.
     *
189
     * @param Action $action the current action
190 191 192
     *
     * @return void
     */
193
    function onEndLoginGroupNav($action)
194 195 196 197 198 199
    {
        $action_name = $action->trimmed('action');

        if ($this->hasApplication()) {

            $action->menuItem(
200
                // TRANS: Menu item for "Facebook" login.
201 202
                common_local_url('facebooklogin'),
                _m('MENU', 'Facebook'),
203 204
                // TRANS: Menu title for "Facebook" login.
                _m('Login or register using Facebook.'),
205 206 207 208 209 210 211
               'facebooklogin' === $action_name
            );
        }

        return true;
    }

212 213 214 215 216 217 218 219 220 221 222 223 224
    /**
     * If the plugin's installed, this should be accessible to admins
     */
    function onAdminPanelCheck($name, &$isOK)
    {
        if ($name == 'facebook') {
            $isOK = true;
            return false;
        }

        return true;
    }

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    /**
     * Add a Facebook tab to the admin panels
     *
     * @param Widget $nav Admin panel nav
     *
     * @return boolean hook value
     */
    function onEndAdminPanelNav($nav)
    {
        if (AdminPanelAction::canAdmin('facebook')) {

            $action_name = $nav->action->trimmed('action');

            $nav->out->menuItem(
                common_local_url('facebookadminpanel'),
240
                // TRANS: Menu item for "Facebook" in administration panel.
241
                _m('MENU','Facebook'),
242 243
                // TRANS: Menu title for "Facebook" in administration panel.
                _m('Facebook integration configuration.'),
244 245 246 247 248 249 250 251
                $action_name == 'facebookadminpanel',
                'nav_facebook_admin_panel'
            );
        }

        return true;
    }

252
    /*
253 254
     * Add a tab for user-level Facebook settings if the user
     * has a link to Facebook
255
     *
256
     * @param Action $action the current action
257 258 259
     *
     * @return void
     */
260
    function onEndConnectSettingsNav($action)
261 262 263 264
    {
        if ($this->hasApplication()) {
            $action_name = $action->trimmed('action');

265
            $user = common_current_user();
266 267 268 269 270 271 272 273 274 275 276 277 278 279

            $flink = null;

            if (!empty($user)) {
                $flink = Foreign_link::getByUserID(
                    $user->id,
                    FACEBOOK_SERVICE
                );
            }

            if (!empty($flink)) {

                $action->menuItem(
                    common_local_url('facebooksettings'),
280
                    // TRANS: Menu item for "Facebook" in user settings.
281
                    _m('MENU','Facebook'),
282 283
                    // TRANS: Menu title for "Facebook" in user settings.
                    _m('Facebook settings.'),
284 285 286
                    $action_name === 'facebooksettings'
                );
            }
287 288 289
        }
    }

290 291
    /*
     * Is there a Facebook application for the plugin to use?
292 293 294 295 296
     *
     * Checks to see if a Facebook application ID and secret
     * have been configured and a valid Facebook API client
     * object exists.
     *
297 298 299
     */
    function hasApplication()
    {
Zach Copley's avatar
Zach Copley committed
300 301 302 303 304 305 306 307
        if (!empty($this->facebook)) {

            $appId  = $this->facebook->getAppId();
            $secret = $this->facebook->getApiSecret();

            if (!empty($appId) && !empty($secret)) {
                return true;
            }
308
        }
Zach Copley's avatar
Zach Copley committed
309 310

        return false;
311 312
    }

313 314 315 316 317 318
    /*
     * Output a Facebook div for the Facebook JavaSsript SDK to use
     *
     * @param Action $action the current action
     *
     */
319 320
    function onStartShowHeader($action)
    {
321 322 323 324
        // output <div id="fb-root"></div> as close to <body> as possible
        $action->element('div', array('id' => 'fb-root'));
        return true;
    }
325

326 327 328 329 330 331
    /*
     * Load the Facebook JavaScript SDK on pages that need them.
     *
     * @param Action $action the current action
     *
     */
332 333 334
    function onEndShowScripts($action)
    {
        if ($this->needsScripts($action)) {
335

336
            $action->script('https://connect.facebook.net/en_US/all.js');
Zach Copley's avatar
Zach Copley committed
337 338

            $script = <<<ENDOFSCRIPT
339 340 341 342 343 344 345
function setCookie(name, value) {
    var date = new Date();
    date.setTime(date.getTime() + (5 * 60 * 1000)); // 5 mins
    var expires = "; expires=" + date.toGMTString();
    document.cookie = name + "=" + value + expires + "; path=/";
}

346
FB.init({appId: %1\$s, status: true, cookie: true, xfbml: true, oauth: true});
347 348 349 350 351 352

$('#facebook_button').bind('click', function(event) {

    event.preventDefault();

    FB.login(function(response) {
353
        if (response.authResponse) {
354 355
            // put the access token in a cookie for the next step
            setCookie('fb_access_token', response.authResponse.accessToken);
356
            window.location.href = '%2\$s';
357 358
        } else {
            // NOP (user cancelled login)
359
        }
360
    }, {scope:'read_stream,publish_stream,offline_access,user_status,user_location,user_website,email'});
361
});
362 363
ENDOFSCRIPT;

Zach Copley's avatar
Zach Copley committed
364
            $action->inlineScript(
365 366
                sprintf(
                    $script,
Zach Copley's avatar
Zach Copley committed
367
                    json_encode($this->facebook->getAppId()),
368
                    common_local_url('facebookfinishlogin')
Zach Copley's avatar
Zach Copley committed
369 370 371
                )
            );
        }
372 373
    }

374 375 376
    /*
     * Log the user out of Facebook, per the Facebook authentication guide
     *
377
     * @param Action action the current action
378
     */
379
    function onStartLogout($action)
380
    {
Zach Copley's avatar
Zach Copley committed
381 382
        if ($this->hasApplication()) {

383 384 385 386 387 388
            $cur = common_current_user();
            $flink = Foreign_link::getByUserID($cur->id, FACEBOOK_SERVICE);

            if (!empty($flink)) {

                $this->facebook->setAccessToken($flink->credentials);
Zach Copley's avatar
Zach Copley committed
389

390 391 392 393 394 395 396 397 398 399
                if (common_config('singleuser', 'enabled')) {
                    $user = User::singleUser();

                    $destination = common_local_url(
                        'showstream',
                        array('nickname' => $user->nickname)
                    );
                } else {
                    $destination = common_local_url('public');
                }
Zach Copley's avatar
Zach Copley committed
400 401

                $logoutUrl = $this->facebook->getLogoutUrl(
402
                    array('next' => $destination)
Zach Copley's avatar
Zach Copley committed
403 404 405 406 407 408 409 410 411 412
                );

                common_log(
                    LOG_INFO,
                    sprintf(
                        "Logging user out of Facebook (fbuid = %s)",
                        $fbuid
                    ),
                    __FILE__
                );
413 414 415

                $action->logout();

Zach Copley's avatar
Zach Copley committed
416 417
                common_redirect($logoutUrl, 303);
            }
418 419

            return true;
420 421 422 423
        }
    }

    /*
424 425
     * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse
     * and render XFBML tags
426
     *
427
     * @param Action    $action   the current action
428 429 430 431
     * @param array     $attrs    array of attributes for the HTML tag
     *
     * @return nothing
     */
432
    function onStartHtmlElement($action, $attrs) {
Zach Copley's avatar
Zach Copley committed
433 434 435 436 437 438 439 440

        if ($this->needsScripts($action)) {
            $attrs = array_merge(
                $attrs,
                array('xmlns:fb' => 'http://www.facebook.com/2008/fbml')
            );
        }

441 442 443
        return true;
    }

444 445 446 447 448 449 450 451 452 453
    /**
     * Add a Facebook queue item for each notice
     *
     * @param Notice $notice      the notice
     * @param array  &$transports the list of transports (queues)
     *
     * @return boolean hook return
     */
    function onStartEnqueueNotice($notice, &$transports)
    {
454
        if (self::hasApplication() && $notice->isLocal() && $notice->inScope(null)) {
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
            array_push($transports, 'facebook');
        }
        return true;
    }

    /**
     * Register Facebook notice queue handler
     *
     * @param QueueManager $manager
     *
     * @return boolean hook return
     */
    function onEndInitializeQueueManager($manager)
    {
        if (self::hasApplication()) {
            $manager->connect('facebook', 'FacebookQueueHandler');
        }
        return true;
    }

475 476 477 478 479 480 481 482 483 484 485 486 487
    /**
     * If a notice gets deleted, remove the Notice_to_item mapping and
     * delete the item on Facebook
     *
     * @param User   $user   The user doing the deleting
     * @param Notice $notice The notice getting deleted
     *
     * @return boolean hook value
     */
    function onStartDeleteOwnNotice(User $user, Notice $notice)
    {
        $client = new Facebookclient($notice);
        $client->streamRemove();
488

489 490 491 492 493 494 495 496 497 498 499 500
        return true;
    }

    /**
     * Notify remote users when their notices get favorited.
     *
     * @param Profile or User $profile of local user doing the faving
     * @param Notice $notice being favored
     * @return hook return value
     */
    function onEndFavorNotice(Profile $profile, Notice $notice)
    {
501
        $client = new Facebookclient($notice, $profile);
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
        $client->like();

        return true;
    }

    /**
     * Notify remote users when their notices get de-favorited.
     *
     * @param Profile $profile Profile person doing the de-faving
     * @param Notice  $notice  Notice being favored
     *
     * @return hook return value
     */
    function onEndDisfavorNotice(Profile $profile, Notice $notice)
    {
517
        $client = new Facebookclient($notice, $profile);
518 519 520 521 522
        $client->unLike();

        return true;
    }

523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
    /**
     * Add links in the user's profile block to their Facebook profile URL.
     *
     * @param Profile $profile The profile being shown
     * @param Array   &$links  Writeable array of arrays (href, text, image).
     *
     * @return boolean hook value (true)
     */

    function onOtherAccountProfiles($profile, &$links)
    {
        $fuser = null;

        $flink = Foreign_link::getByUserID($profile->id, FACEBOOK_SERVICE);

        if (!empty($flink)) {

            $fuser = $this->getFacebookUser($flink->foreign_id);

            if (!empty($fuser)) {
                $links[] = array("href" => $fuser->link,
544
                                 "text" => sprintf(_("%s on Facebook"), $fuser->name),
545 546 547 548 549 550 551 552 553
                                 "image" => $this->path("images/f_logo.png"));
            }
        }

        return true;
    }

    function getFacebookUser($id) {

554
        $key = Cache::key(sprintf("FacebookBridgePlugin:userdata:%s", $id));
555 556 557 558 559 560 561 562 563 564 565

        $c = Cache::instance();

        if ($c) {
            $obj = $c->get($key);
            if ($obj) {
                return $obj;
            }
        }

        $url = sprintf("https://graph.facebook.com/%s", $id);
566
        $client = new HTTPClient();
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
        $resp = $client->get($url);

        if (!$resp->isOK()) {
            return null;
        }

        $user = json_decode($resp->getBody());

        if ($user->error) {
            return null;
        }

        if ($c) {
            $c->set($key, $user);
        }

        return $user;
    }

586 587 588 589 590
    /*
     * Add version info for this plugin
     *
     * @param array &$versions    plugin version descriptions
     */
591
    function onPluginVersion(array &$versions)
592
    {
593
        $versions[] = array(
594
            'name' => 'Facebook Bridge',
595
            'version' => GNUSOCIAL_VERSION,
Zach Copley's avatar
Zach Copley committed
596
            'author' => 'Craig Andrews, Zach Copley',
597
            'homepage' => 'http://status.net/wiki/Plugin:FacebookBridge',
598
            'rawdescription' =>
599
             // TRANS: Plugin description.
Zach Copley's avatar
Zach Copley committed
600
            _m('A plugin for integrating StatusNet with Facebook.')
601 602
        );

603 604 605
        return true;
    }
}