]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookSSO/lib/facebookclient.php
cf00b55e3a5714fd0aefb566ee5e0e99754d4966
[quix0rs-gnu-social.git] / plugins / FacebookSSO / lib / facebookclient.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Class for communicating with Facebook
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Plugin
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Zach Copley <zach@status.net>
26  * @copyright 2009-2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     exit(1);
33 }
34
35 /**
36  * Class for communication with Facebook
37  *
38  * @category Plugin
39  * @package  StatusNet
40  * @author   Zach Copley <zach@status.net>
41  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
42  * @link     http://status.net/
43  */
44 class Facebookclient
45 {
46     protected $facebook      = null; // Facebook Graph client obj
47     protected $flink         = null; // Foreign_link StatusNet -> Facebook
48     protected $notice        = null; // The user's notice
49     protected $user          = null; // Sender of the notice
50
51     function __construct($notice)
52     {
53         $this->facebook = self::getFacebook();
54         $this->notice   = $notice;
55
56         $this->flink = Foreign_link::getByUserID(
57             $notice->profile_id,
58             FACEBOOK_SERVICE
59         );
60
61         $this->user = $this->flink->getUser();
62     }
63
64     /*
65      * Get an instance of the Facebook Graph SDK object
66      *
67      * @param string $appId     Application
68      * @param string $secret    Facebook API secret
69      *
70      * @return Facebook A Facebook SDK obj
71      */
72     static function getFacebook($appId = null, $secret = null)
73     {
74         // Check defaults and configuration for application ID and secret
75         if (empty($appId)) {
76             $appId = common_config('facebook', 'appid');
77         }
78
79         if (empty($secret)) {
80             $secret = common_config('facebook', 'secret');
81         }
82
83         // If there's no app ID and secret set in the local config, look
84         // for a global one
85         if (empty($appId) || empty($secret)) {
86             $appId  = common_config('facebook', 'global_appid');
87             $secret = common_config('facebook', 'global_secret');
88         }
89
90         return new Facebook(
91             array(
92                'appId'  => $appId,
93                'secret' => $secret,
94                'cookie' => true
95             )
96         );
97     }
98
99     /*
100      * Broadcast a notice to Facebook
101      *
102      * @param Notice $notice    the notice to send
103      */
104     static function facebookBroadcastNotice($notice)
105     {
106         common_debug('Facebook broadcast');
107         $client = new Facebookclient($notice);
108         return $client->sendNotice();
109     }
110
111     /*
112      * Should the notice go to Facebook?
113      */
114     function isFacebookBound() {
115
116         if (empty($this->flink)) {
117             common_log(
118                 LOG_WARN,
119                 sprintf(
120                     "No Foreign_link to Facebook for the author of notice %d.",
121                     $this->notice->id
122                 ),
123                 __FILE__
124             );
125             return false;
126         }
127
128         // Avoid a loop
129         if ($this->notice->source == 'Facebook') {
130             common_log(
131                 LOG_INFO,
132                 sprintf(
133                     'Skipping notice %d because its source is Facebook.',
134                     $this->notice->id
135                 ),
136                 __FILE__
137             );
138             return false;
139         }
140
141         // If the user does not want to broadcast to Facebook, move along
142         if (!($this->flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
143             common_log(
144                 LOG_INFO,
145                 sprintf(
146                     'Skipping notice %d because user has FOREIGN_NOTICE_SEND bit off.',
147                     $this->notice->id
148                 ),
149                 __FILE__
150             );
151             return false;
152         }
153
154         // If it's not a reply, or if the user WANTS to send @-replies,
155         // then, yeah, it can go to Facebook.
156         if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $this->notice->content) ||
157             ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
158             return true;
159         }
160
161         return false;
162     }
163
164     /*
165      * Determine whether we should send this notice using the Graph API or the
166      * old REST API and then dispatch
167      */
168     function sendNotice()
169     {
170         // If there's nothing in the credentials field try to send via
171         // the Old Rest API
172
173         if ($this->isFacebookBound()) {
174             common_debug("notice is facebook bound", __FILE__);
175             if (empty($this->flink->credentials)) {
176                 $this->sendOldRest();
177             } else {
178
179                 // Otherwise we most likely have an access token
180                 $this->sendGraph();
181             }
182
183         } else {
184             common_debug(
185                 sprintf(
186                     "Skipping notice %d - not bound for Facebook",
187                     $this->notice->id,
188                     __FILE__
189                 )
190             );
191         }
192     }
193
194     /*
195      * Send a notice to Facebook using the Graph API
196      */
197     function sendGraph()
198     {
199         try {
200
201             $fbuid = $this->flink->foreign_id;
202
203             common_debug(
204                 sprintf(
205                     "Attempting use Graph API to post notice %d as a stream item for %s (%d), fbuid %s",
206                     $this->notice->id,
207                     $this->user->nickname,
208                     $this->user->id,
209                     $fbuid
210                 ),
211                 __FILE__
212             );
213
214             $params = array(
215                 'access_token' => $this->flink->credentials,
216                 'message'      => $this->notice->content
217             );
218
219             $attachments = $this->notice->attachments();
220
221             if (!empty($attachments)) {
222
223                 // We can only send one attachment with the Graph API
224
225                 $first = array_shift($attachments);
226
227                 if (substr($first->mimetype, 0, 6) == 'image/'
228                     || in_array(
229                         $first->mimetype,
230                         array('application/x-shockwave-flash', 'audio/mpeg' ))) {
231
232                    $params['picture'] = $first->url;
233                    $params['caption'] = 'Click for full size';
234                    $params['source']  = $first->url;
235                 }
236
237             }
238
239             $result = $this->facebook->api(
240                 sprintf('/%s/feed', $fbuid), 'post', $params
241             );
242
243         } catch (FacebookApiException $e) {
244             return $this->handleFacebookError($e);
245         }
246
247         return true;
248     }
249
250     /*
251      * Send a notice to Facebook using the deprecated Old REST API. We need this
252      * for backwards compatibility. Users who signed up for Facebook bridging
253      * using the old Facebook Canvas application do not have an OAuth 2.0
254      * access token.
255      */
256     function sendOldRest()
257     {
258         try {
259
260             $canPublish = $this->checkPermission('publish_stream');
261             $canUpdate  = $this->checkPermission('status_update');
262
263             // We prefer to use stream.publish, because it can handle
264             // attachments and returns the ID of the published item
265
266             if ($canPublish == 1) {
267                 $this->restPublishStream();
268             } else if ($canUpdate == 1) {
269                 // as a last resort we can just update the user's "status"
270                 $this->restStatusUpdate();
271             } else {
272
273                 $msg = 'Not sending notice %d to Facebook because user %s '
274                      . '(%d), fbuid %s,  does not have \'status_update\' '
275                      . 'or \'publish_stream\' permission.';
276
277                 common_log(
278                     LOG_WARNING,
279                     sprintf(
280                         $msg,
281                         $this->notice->id,
282                         $this->user->nickname,
283                         $this->user->id,
284                         $this->flink->foreign_id
285                     ),
286                     __FILE__
287                 );
288             }
289
290         } catch (FacebookApiException $e) {
291             return $this->handleFacebookError($e);
292         }
293
294         return true;
295     }
296
297     /*
298      * Query Facebook to to see if a user has permission
299      *
300      *
301      *
302      * @param $permission the permission to check for - must be either
303      *                    public_stream or status_update
304      *
305      * @return boolean result
306      */
307     function checkPermission($permission)
308     {
309         if (!in_array($permission, array('publish_stream', 'status_update'))) {
310              throw new ServerException("No such permission!");
311         }
312
313         $fbuid = $this->flink->foreign_id;
314
315         common_debug(
316             sprintf(
317                 'Checking for %s permission for user %s (%d), fbuid %s',
318                 $permission,
319                 $this->user->nickname,
320                 $this->user->id,
321                 $fbuid
322             ),
323             __FILE__
324         );
325
326         $hasPermission = $this->facebook->api(
327             array(
328                 'method'   => 'users.hasAppPermission',
329                 'ext_perm' => $permission,
330                 'uid'      => $fbuid
331             )
332         );
333
334         if ($hasPermission == 1) {
335
336             common_debug(
337                 sprintf(
338                     '%s (%d), fbuid %s has %s permission',
339                     $permission,
340                     $this->user->nickname,
341                     $this->user->id,
342                     $fbuid
343                 ),
344                 __FILE__
345             );
346
347             return true;
348
349         } else {
350
351             $logMsg = '%s (%d), fbuid $fbuid does NOT have %s permission.'
352                     . 'Facebook returned: %s';
353
354             common_debug(
355                 sprintf(
356                     $logMsg,
357                     $this->user->nickname,
358                     $this->user->id,
359                     $permission,
360                     $fbuid,
361                     var_export($result, true)
362                 ),
363                 __FILE__
364             );
365
366             return false;
367
368         }
369     }
370
371     /*
372      * Handle a Facebook API Exception
373      *
374      * @param FacebookApiException $e the exception
375      *
376      */
377     function handleFacebookError($e)
378     {
379         $fbuid  = $this->flink->foreign_id;
380         $errmsg = $e->getMessage();
381         $code   = $e->getCode();
382
383         // The Facebook PHP SDK seems to always set the code attribute
384         // of the Exception to 0; they put the real error code in
385         // the message. Gar!
386         if ($code == 0) {
387             preg_match('/^\(#(?<code>\d+)\)/', $errmsg, $matches);
388             $code = $matches['code'];
389         }
390
391         // XXX: Check for any others?
392         switch($code) {
393          case 100: // Invalid parameter
394             $msg = 'Facebook claims notice %d was posted with an invalid '
395                  . 'parameter (error code 100 - %s) Notice details: '
396                  . '[nickname=%s, user id=%d, fbuid=%d, content="%s"]. '
397                  . 'Dequeing.';
398             common_log(
399                 LOG_ERR, sprintf(
400                     $msg,
401                     $this->notice->id,
402                     $errmsg,
403                     $this->user->nickname,
404                     $this->user->id,
405                     $fbuid,
406                     $this->notice->content
407                 ),
408                 __FILE__
409             );
410             return true;
411             break;
412          case 200: // Permissions error
413          case 250: // Updating status requires the extended permission status_update
414             $this->disconnect();
415             return true; // dequeue
416             break;
417          case 341: // Feed action request limit reached
418                 $msg = '%s (userid=%d, fbuid=%d) has exceeded his/her limit '
419                      . 'for posting notices to Facebook today. Dequeuing '
420                      . 'notice %d';
421                 common_log(
422                     LOG_INFO, sprintf(
423                         $msg,
424                         $user->nickname,
425                         $user->id,
426                         $fbuid,
427                         $this->notice->id
428                     ),
429                     __FILE__
430                 );
431             // @fixme: We want to rety at a later time when the throttling has expired
432             // instead of just giving up.
433             return true;
434             break;
435          default:
436             $msg = 'Facebook returned an error we don\'t know how to deal with '
437                  . 'when posting notice %d. Error code: %d, error message: "%s"'
438                  . ' Notice details: [nickname=%s, user id=%d, fbuid=%d, '
439                  . 'notice content="%s"]. Dequeing.';
440             common_log(
441                 LOG_ERR, sprintf(
442                     $msg,
443                     $this->notice->id,
444                     $code,
445                     $errmsg,
446                     $this->user->nickname,
447                     $this->user->id,
448                     $fbuid,
449                     $this->notice->content
450                 ),
451                 __FILE__
452             );
453             return true; // dequeue
454             break;
455         }
456     }
457
458     /*
459      * Publish a notice to Facebook as a status update
460      *
461      * This is the least preferable way to send a notice to Facebook because
462      * it doesn't support attachments and the API method doesn't return
463      * the ID of the post on Facebook.
464      *
465      */
466     function restStatusUpdate()
467     {
468         $fbuid = $this->flink->foreign_id;
469
470         common_debug(
471             sprintf(
472                 "Attempting to post notice %d as a status update for %s (%d), fbuid %s",
473                 $this->notice->id,
474                 $this->user->nickname,
475                 $this->user->id,
476                 $fbuid
477             ),
478             __FILE__
479         );
480
481         $result = $this->facebook->api(
482             array(
483                 'method'               => 'users.setStatus',
484                 'status'               => $this->notice->content,
485                 'status_includes_verb' => true,
486                 'uid'                  => $fbuid
487             )
488         );
489
490         common_log(
491             LOG_INFO,
492             sprintf(
493                 "Posted notice %s as a status update for %s (%d), fbuid %s",
494                 $this->notice->id,
495                 $this->user->nickname,
496                 $this->user->id,
497                 $fbuid
498             ),
499             __FILE__
500         );
501
502     }
503
504     /*
505      * Publish a notice to a Facebook user's stream using the old REST API
506      */
507     function restPublishStream()
508     {
509         $fbuid = $this->flink->foreign_id;
510
511         common_debug(
512             sprintf(
513                 'Attempting to post notice %d as stream item for %s (%d) fbuid %s',
514                 $this->notice->id,
515                 $this->user->nickname,
516                 $this->user->id,
517                 $fbuid
518             ),
519             __FILE__
520         );
521
522         $fbattachment = $this->formatAttachments();
523
524         $result = $this->facebook->api(
525             array(
526                 'method'     => 'stream.publish',
527                 'message'    => $this->notice->content,
528                 'attachment' => $fbattachment,
529                 'uid'        => $fbuid
530             )
531         );
532
533         common_log(
534             LOG_INFO,
535             sprintf(
536                 'Posted notice %d as a %s for %s (%d), fbuid %s',
537                 $this->notice->id,
538                 empty($fbattachment) ? 'stream item' : 'stream item with attachment',
539                 $this->user->nickname,
540                 $this->user->id,
541                 $fbuid
542             ),
543             __FILE__
544         );
545
546     }
547
548     /*
549      * Format attachments for the old REST API stream.publish method
550      *
551      * Note: Old REST API supports multiple attachments per post
552      *
553      */
554     function formatAttachments()
555     {
556         $attachments = $this->notice->attachments();
557
558         $fbattachment          = array();
559         $fbattachment['media'] = array();
560
561         foreach($attachments as $attachment)
562         {
563             if($enclosure = $attachment->getEnclosure()){
564                 $fbmedia = $this->getFacebookMedia($enclosure);
565             }else{
566                 $fbmedia = $this->getFacebookMedia($attachment);
567             }
568             if($fbmedia){
569                 $fbattachment['media'][]=$fbmedia;
570             }else{
571                 $fbattachment['name'] = ($attachment->title ?
572                                       $attachment->title : $attachment->url);
573                 $fbattachment['href'] = $attachment->url;
574             }
575         }
576         if(count($fbattachment['media'])>0){
577             unset($fbattachment['name']);
578             unset($fbattachment['href']);
579         }
580         return $fbattachment;
581     }
582
583     /**
584      * given a File objects, returns an associative array suitable for Facebook media
585      */
586     function getFacebookMedia($attachment)
587     {
588         $fbmedia    = array();
589
590         if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
591             $fbmedia['type']         = 'image';
592             $fbmedia['src']          = $attachment->url;
593             $fbmedia['href']         = $attachment->url;
594         } else if ($attachment->mimetype == 'audio/mpeg') {
595             $fbmedia['type']         = 'mp3';
596             $fbmedia['src']          = $attachment->url;
597         }else if ($attachment->mimetype == 'application/x-shockwave-flash') {
598             $fbmedia['type']         = 'flash';
599
600             // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
601             // says that imgsrc is required... but we have no value to put in it
602             // $fbmedia['imgsrc']='';
603
604             $fbmedia['swfsrc']       = $attachment->url;
605         }else{
606             return false;
607         }
608         return $fbmedia;
609     }
610
611     /*
612      * Disconnect a user from Facebook by deleting his Foreign_link.
613      * Notifies the user his account has been disconnected by email.
614      */
615     function disconnect()
616     {
617         $fbuid = $this->flink->foreign_id;
618
619         common_log(
620             LOG_INFO,
621             sprintf(
622                 'Removing Facebook link for %s (%d), fbuid %s',
623                 $this->user->nickname,
624                 $this->user->id,
625                 $fbuid
626             ),
627             __FILE__
628         );
629
630         $result = $this->flink->delete();
631
632         if (empty($result)) {
633             common_log(
634                 LOG_ERR,
635                 sprintf(
636                     'Could not remove Facebook link for %s (%d), fbuid %s',
637                     $this->user->nickname,
638                     $this->user->id,
639                     $fbuid
640                 ),
641                 __FILE__
642             );
643             common_log_db_error($flink, 'DELETE', __FILE__);
644         }
645
646         // Notify the user that we are removing their Facebook link
647
648         $result = $this->mailFacebookDisconnect();
649
650         if (!$result) {
651
652             $msg = 'Unable to send email to notify %s (%d), fbuid %s '
653                  . 'about his/her Facebook link being removed.';
654
655             common_log(
656                 LOG_WARNING,
657                 sprintf(
658                     $msg,
659                     $this->user->nickname,
660                     $this->user->id,
661                     $fbuid
662                 ),
663                 __FILE__
664             );
665         }
666     }
667
668     /**
669      * Send a mail message to notify a user that her Facebook link
670      * has been terminated.
671      *
672      * @return boolean success flag
673      */
674     function mailFacebookDisconnect()
675     {
676         $profile = $this->user->getProfile();
677
678         $siteName = common_config('site', 'name');
679
680         common_switch_locale($this->user->language);
681
682         $subject = _m('Your Facebook connection has been removed');
683
684         $msg = <<<BODY
685 Hi %1$s,
686
687 We're sorry to inform you we are unable to publish your notice to
688 Facebook, and have removed the connection between your %2$s account and
689 Facebook.
690
691 This may have happened because you have removed permission for %2$s
692 to post on your behalf, or perhaps you have deactivated your Facebook
693 account. You can reconnect your %s account to Facebook at any time by
694 logging in with Facebook again.
695
696 Sincerely,
697
698 %2$s
699 BODY;
700         $body = sprintf(
701             _m($msg),
702             $this->user->nickname,
703             $siteName
704         );
705
706         common_switch_locale();
707
708         return mail_to_user($this->user, $subject, $body);
709     }
710
711 }