FacebookBridgePlugin.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <?php
  2. /**
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2010-2011, StatusNet, Inc.
  5. *
  6. * A plugin for integrating Facebook with StatusNet. Includes single-sign-on
  7. * and publishing notices to Facebook using Facebook's Graph API.
  8. *
  9. * PHP version 5
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. * @category Plugin
  25. * @package StatusNet
  26. * @author Zach Copley <zach@status.net>
  27. * @copyright 2011 StatusNet, Inc.
  28. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  29. * @link http://status.net/
  30. */
  31. if (!defined('STATUSNET')) {
  32. exit(1);
  33. }
  34. define("FACEBOOK_SERVICE", 2);
  35. /**
  36. * Main class for Facebook Bridge plugin
  37. *
  38. * @category Plugin
  39. * @package StatusNet
  40. * @author Zach Copley <zach@status.net>
  41. * @copyright 2010-2011 StatusNet, Inc.
  42. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  43. * @link http://status.net/
  44. */
  45. class FacebookBridgePlugin extends Plugin
  46. {
  47. public $appId; // Facebook application ID
  48. public $secret; // Facebook application secret
  49. public $facebook = null; // Facebook application instance
  50. public $dir = null; // Facebook plugin dir
  51. /**
  52. * Initializer for this plugin
  53. *
  54. * Gets an instance of the Facebook API client object
  55. *
  56. * @return boolean hook value; true means continue processing, false means stop.
  57. */
  58. function initialize()
  59. {
  60. // Allow the id and key to be passed in
  61. // Control panel will override
  62. if (isset($this->appId)) {
  63. $appId = common_config('facebook', 'appid');
  64. if (empty($appId)) {
  65. Config::save(
  66. 'facebook',
  67. 'appid',
  68. $this->appId
  69. );
  70. }
  71. }
  72. if (isset($this->secret)) {
  73. $secret = common_config('facebook', 'secret');
  74. if (empty($secret)) {
  75. Config::save('facebook', 'secret', $this->secret);
  76. }
  77. }
  78. $this->facebook = Facebookclient::getFacebook(
  79. $this->appId,
  80. $this->secret
  81. );
  82. return true;
  83. }
  84. /**
  85. * Load related modules when needed
  86. *
  87. * @param string $cls Name of the class to be loaded
  88. *
  89. * @return boolean hook value; true means continue processing, false means stop.
  90. */
  91. function onAutoload($cls)
  92. {
  93. $dir = dirname(__FILE__);
  94. switch ($cls)
  95. {
  96. case 'Facebook': // Facebook PHP SDK
  97. include_once $dir . '/extlib/base_facebook.php';
  98. include_once $dir . '/extlib/facebook.php';
  99. return false;
  100. }
  101. return parent::onAutoload($cls);
  102. }
  103. /**
  104. * Database schema setup
  105. *
  106. * We maintain a table mapping StatusNet notices to Facebook items
  107. *
  108. * @see Schema
  109. * @see ColumnDef
  110. *
  111. * @return boolean hook value; true means continue processing, false means stop.
  112. */
  113. function onCheckSchema()
  114. {
  115. $schema = Schema::get();
  116. $schema->ensureTable('notice_to_item', Notice_to_item::schemaDef());
  117. return true;
  118. }
  119. /*
  120. * Does this $action need the Facebook JavaScripts?
  121. */
  122. function needsScripts($action)
  123. {
  124. static $needy = array(
  125. 'FacebookloginAction',
  126. 'FacebookfinishloginAction',
  127. 'FacebookadminpanelAction',
  128. 'FacebooksettingsAction'
  129. );
  130. if (in_array(get_class($action), $needy)) {
  131. return true;
  132. } else {
  133. return false;
  134. }
  135. }
  136. /**
  137. * Map URLs to actions
  138. *
  139. * @param URLMapper $m path-to-action mapper
  140. *
  141. * @return boolean hook value; true means continue processing, false means stop.
  142. */
  143. public function onRouterInitialized(URLMapper $m)
  144. {
  145. // Always add the admin panel route
  146. $m->connect('panel/facebook', array('action' => 'facebookadminpanel'));
  147. $m->connect(
  148. 'main/facebooklogin',
  149. array('action' => 'facebooklogin')
  150. );
  151. $m->connect(
  152. 'main/facebookfinishlogin',
  153. array('action' => 'facebookfinishlogin')
  154. );
  155. $m->connect(
  156. 'settings/facebook',
  157. array('action' => 'facebooksettings')
  158. );
  159. $m->connect(
  160. 'facebook/deauthorize',
  161. array('action' => 'facebookdeauthorize')
  162. );
  163. return true;
  164. }
  165. /*
  166. * Add a login tab for Facebook, but only if there's a Facebook
  167. * application defined for the plugin to use.
  168. *
  169. * @param Action $action the current action
  170. *
  171. * @return void
  172. */
  173. function onEndLoginGroupNav($action)
  174. {
  175. $action_name = $action->trimmed('action');
  176. if ($this->hasApplication()) {
  177. $action->menuItem(
  178. // TRANS: Menu item for "Facebook" login.
  179. common_local_url('facebooklogin'),
  180. _m('MENU', 'Facebook'),
  181. // TRANS: Menu title for "Facebook" login.
  182. _m('Login or register using Facebook.'),
  183. 'facebooklogin' === $action_name
  184. );
  185. }
  186. return true;
  187. }
  188. /**
  189. * If the plugin's installed, this should be accessible to admins
  190. */
  191. function onAdminPanelCheck($name, &$isOK)
  192. {
  193. if ($name == 'facebook') {
  194. $isOK = true;
  195. return false;
  196. }
  197. return true;
  198. }
  199. /**
  200. * Add a Facebook tab to the admin panels
  201. *
  202. * @param Widget $nav Admin panel nav
  203. *
  204. * @return boolean hook value
  205. */
  206. function onEndAdminPanelNav($nav)
  207. {
  208. if (AdminPanelAction::canAdmin('facebook')) {
  209. $action_name = $nav->action->trimmed('action');
  210. $nav->out->menuItem(
  211. common_local_url('facebookadminpanel'),
  212. // TRANS: Menu item for "Facebook" in administration panel.
  213. _m('MENU','Facebook'),
  214. // TRANS: Menu title for "Facebook" in administration panel.
  215. _m('Facebook integration configuration.'),
  216. $action_name == 'facebookadminpanel',
  217. 'nav_facebook_admin_panel'
  218. );
  219. }
  220. return true;
  221. }
  222. /*
  223. * Add a tab for user-level Facebook settings if the user
  224. * has a link to Facebook
  225. *
  226. * @param Action $action the current action
  227. *
  228. * @return void
  229. */
  230. function onEndConnectSettingsNav($action)
  231. {
  232. if ($this->hasApplication()) {
  233. $action_name = $action->trimmed('action');
  234. $user = common_current_user();
  235. $flink = null;
  236. if (!empty($user)) {
  237. $flink = Foreign_link::getByUserID(
  238. $user->id,
  239. FACEBOOK_SERVICE
  240. );
  241. }
  242. if (!empty($flink)) {
  243. $action->menuItem(
  244. common_local_url('facebooksettings'),
  245. // TRANS: Menu item for "Facebook" in user settings.
  246. _m('MENU','Facebook'),
  247. // TRANS: Menu title for "Facebook" in user settings.
  248. _m('Facebook settings.'),
  249. $action_name === 'facebooksettings'
  250. );
  251. }
  252. }
  253. }
  254. /*
  255. * Is there a Facebook application for the plugin to use?
  256. *
  257. * Checks to see if a Facebook application ID and secret
  258. * have been configured and a valid Facebook API client
  259. * object exists.
  260. *
  261. */
  262. function hasApplication()
  263. {
  264. if (!empty($this->facebook)) {
  265. $appId = $this->facebook->getAppId();
  266. $secret = $this->facebook->getApiSecret();
  267. if (!empty($appId) && !empty($secret)) {
  268. return true;
  269. }
  270. }
  271. return false;
  272. }
  273. /*
  274. * Output a Facebook div for the Facebook JavaSsript SDK to use
  275. *
  276. * @param Action $action the current action
  277. *
  278. */
  279. function onStartShowHeader($action)
  280. {
  281. // output <div id="fb-root"></div> as close to <body> as possible
  282. $action->element('div', array('id' => 'fb-root'));
  283. return true;
  284. }
  285. /*
  286. * Load the Facebook JavaScript SDK on pages that need them.
  287. *
  288. * @param Action $action the current action
  289. *
  290. */
  291. function onEndShowScripts($action)
  292. {
  293. if ($this->needsScripts($action)) {
  294. $action->script('https://connect.facebook.net/en_US/all.js');
  295. $script = <<<ENDOFSCRIPT
  296. function setCookie(name, value) {
  297. var date = new Date();
  298. date.setTime(date.getTime() + (5 * 60 * 1000)); // 5 mins
  299. var expires = "; expires=" + date.toGMTString();
  300. document.cookie = name + "=" + value + expires + "; path=/";
  301. }
  302. FB.init({appId: %1\$s, status: true, cookie: true, xfbml: true, oauth: true});
  303. $('#facebook_button').bind('click', function(event) {
  304. event.preventDefault();
  305. FB.login(function(response) {
  306. if (response.authResponse) {
  307. // put the access token in a cookie for the next step
  308. setCookie('fb_access_token', response.authResponse.accessToken);
  309. window.location.href = '%2\$s';
  310. } else {
  311. // NOP (user cancelled login)
  312. }
  313. }, {scope:'read_stream,publish_stream,offline_access,user_status,user_location,user_website,email'});
  314. });
  315. ENDOFSCRIPT;
  316. $action->inlineScript(
  317. sprintf(
  318. $script,
  319. json_encode($this->facebook->getAppId()),
  320. common_local_url('facebookfinishlogin')
  321. )
  322. );
  323. }
  324. }
  325. /*
  326. * Log the user out of Facebook, per the Facebook authentication guide
  327. *
  328. * @param Action action the current action
  329. */
  330. function onStartLogout($action)
  331. {
  332. if ($this->hasApplication()) {
  333. $cur = common_current_user();
  334. $flink = Foreign_link::getByUserID($cur->id, FACEBOOK_SERVICE);
  335. if (!empty($flink)) {
  336. $this->facebook->setAccessToken($flink->credentials);
  337. if (common_config('singleuser', 'enabled')) {
  338. $user = User::singleUser();
  339. $destination = common_local_url(
  340. 'showstream',
  341. array('nickname' => $user->nickname)
  342. );
  343. } else {
  344. $destination = common_local_url('public');
  345. }
  346. $logoutUrl = $this->facebook->getLogoutUrl(
  347. array('next' => $destination)
  348. );
  349. common_log(
  350. LOG_INFO,
  351. sprintf(
  352. "Logging user out of Facebook (fbuid = %s)",
  353. $fbuid
  354. ),
  355. __FILE__
  356. );
  357. $action->logout();
  358. common_redirect($logoutUrl, 303);
  359. }
  360. return true;
  361. }
  362. }
  363. /*
  364. * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse
  365. * and render XFBML tags
  366. *
  367. * @param Action $action the current action
  368. * @param array $attrs array of attributes for the HTML tag
  369. *
  370. * @return nothing
  371. */
  372. function onStartHtmlElement($action, $attrs) {
  373. if ($this->needsScripts($action)) {
  374. $attrs = array_merge(
  375. $attrs,
  376. array('xmlns:fb' => 'http://www.facebook.com/2008/fbml')
  377. );
  378. }
  379. return true;
  380. }
  381. /**
  382. * Add a Facebook queue item for each notice
  383. *
  384. * @param Notice $notice the notice
  385. * @param array &$transports the list of transports (queues)
  386. *
  387. * @return boolean hook return
  388. */
  389. function onStartEnqueueNotice($notice, &$transports)
  390. {
  391. if (self::hasApplication() && $notice->isLocal() && $notice->inScope(null)) {
  392. array_push($transports, 'facebook');
  393. }
  394. return true;
  395. }
  396. /**
  397. * Register Facebook notice queue handler
  398. *
  399. * @param QueueManager $manager
  400. *
  401. * @return boolean hook return
  402. */
  403. function onEndInitializeQueueManager($manager)
  404. {
  405. if (self::hasApplication()) {
  406. $manager->connect('facebook', 'FacebookQueueHandler');
  407. }
  408. return true;
  409. }
  410. /*
  411. * Use SSL for Facebook stuff
  412. *
  413. * @param string $action name
  414. * @param boolean $ssl outval to force SSL
  415. * @return mixed hook return value
  416. */
  417. function onSensitiveAction($action, &$ssl)
  418. {
  419. $sensitive = array(
  420. 'facebookadminpanel',
  421. 'facebooksettings',
  422. 'facebooklogin',
  423. 'facebookfinishlogin'
  424. );
  425. if (in_array($action, $sensitive)) {
  426. $ssl = true;
  427. return false;
  428. } else {
  429. return true;
  430. }
  431. }
  432. /**
  433. * If a notice gets deleted, remove the Notice_to_item mapping and
  434. * delete the item on Facebook
  435. *
  436. * @param User $user The user doing the deleting
  437. * @param Notice $notice The notice getting deleted
  438. *
  439. * @return boolean hook value
  440. */
  441. function onStartDeleteOwnNotice(User $user, Notice $notice)
  442. {
  443. $client = new Facebookclient($notice);
  444. $client->streamRemove();
  445. return true;
  446. }
  447. /**
  448. * Notify remote users when their notices get favorited.
  449. *
  450. * @param Profile or User $profile of local user doing the faving
  451. * @param Notice $notice being favored
  452. * @return hook return value
  453. */
  454. function onEndFavorNotice(Profile $profile, Notice $notice)
  455. {
  456. $client = new Facebookclient($notice, $profile);
  457. $client->like();
  458. return true;
  459. }
  460. /**
  461. * Notify remote users when their notices get de-favorited.
  462. *
  463. * @param Profile $profile Profile person doing the de-faving
  464. * @param Notice $notice Notice being favored
  465. *
  466. * @return hook return value
  467. */
  468. function onEndDisfavorNotice(Profile $profile, Notice $notice)
  469. {
  470. $client = new Facebookclient($notice, $profile);
  471. $client->unLike();
  472. return true;
  473. }
  474. /**
  475. * Add links in the user's profile block to their Facebook profile URL.
  476. *
  477. * @param Profile $profile The profile being shown
  478. * @param Array &$links Writeable array of arrays (href, text, image).
  479. *
  480. * @return boolean hook value (true)
  481. */
  482. function onOtherAccountProfiles($profile, &$links)
  483. {
  484. $fuser = null;
  485. $flink = Foreign_link::getByUserID($profile->id, FACEBOOK_SERVICE);
  486. if (!empty($flink)) {
  487. $fuser = $this->getFacebookUser($flink->foreign_id);
  488. if (!empty($fuser)) {
  489. $links[] = array("href" => $fuser->link,
  490. "text" => sprintf(_("%s on Facebook"), $fuser->name),
  491. "image" => $this->path("images/f_logo.png"));
  492. }
  493. }
  494. return true;
  495. }
  496. function getFacebookUser($id) {
  497. $key = Cache::key(sprintf("FacebookBridgePlugin:userdata:%s", $id));
  498. $c = Cache::instance();
  499. if ($c) {
  500. $obj = $c->get($key);
  501. if ($obj) {
  502. return $obj;
  503. }
  504. }
  505. $url = sprintf("https://graph.facebook.com/%s", $id);
  506. $client = new HTTPClient();
  507. $resp = $client->get($url);
  508. if (!$resp->isOK()) {
  509. return null;
  510. }
  511. $user = json_decode($resp->getBody());
  512. if ($user->error) {
  513. return null;
  514. }
  515. if ($c) {
  516. $c->set($key, $user);
  517. }
  518. return $user;
  519. }
  520. /*
  521. * Add version info for this plugin
  522. *
  523. * @param array &$versions plugin version descriptions
  524. */
  525. function onPluginVersion(&$versions)
  526. {
  527. $versions[] = array(
  528. 'name' => 'Facebook Bridge',
  529. 'version' => GNUSOCIAL_VERSION,
  530. 'author' => 'Craig Andrews, Zach Copley',
  531. 'homepage' => 'http://status.net/wiki/Plugin:FacebookBridge',
  532. 'rawdescription' =>
  533. // TRANS: Plugin description.
  534. _m('A plugin for integrating StatusNet with Facebook.')
  535. );
  536. return true;
  537. }
  538. }