]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/apioauthauthorize.php
Use array_merge instead of array_replace (same effect, and array_merge works with...
[quix0rs-gnu-social.git] / actions / apioauthauthorize.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Authorize an OAuth request token
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  API
23  * @package   StatusNet
24  * @author    Zach Copley <zach@status.net>
25  * @copyright 2010-2011 StatusNet, Inc.
26  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27  * @link      http://status.net/
28  */
29
30 if (!defined('STATUSNET')) {
31     exit(1);
32 }
33
34 require_once INSTALLDIR . '/lib/apioauth.php';
35 require_once INSTALLDIR . '/lib/info.php';
36
37 /**
38  * Authorize an OAuth request token
39  *
40  * @category API
41  * @package  StatusNet
42  * @author   Zach Copley <zach@status.net>
43  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
44  * @link     http://status.net/
45  */
46 class ApiOauthAuthorizeAction extends Action
47 {
48     var $oauthTokenParam;
49     var $reqToken;
50     var $callback;
51     var $app;
52     var $nickname;
53     var $password;
54     var $store;
55
56     /**
57      * Is this a read-only action?
58      *
59      * @return boolean false
60      */
61     function isReadOnly($args)
62     {
63         return false;
64     }
65
66     function prepare($args)
67     {
68         parent::prepare($args);
69
70         $this->nickname        = $this->trimmed('nickname');
71         $this->password        = $this->arg('password');
72         $this->oauthTokenParam = $this->arg('oauth_token');
73         $this->mode            = $this->arg('mode');
74         $this->store           = new ApiStatusNetOAuthDataStore();
75
76         try {
77             $this->app = $this->store->getAppByRequestToken($this->oauthTokenParam);
78         } catch (Exception $e) {
79             $this->clientError($e->getMessage());
80         }
81
82         return true;
83     }
84
85     /**
86      * Handle input, produce output
87      *
88      * Switches on request method; either shows the form or handles its input.
89      *
90      * @param array $args $_REQUEST data
91      *
92      * @return void
93      */
94     function handle($args)
95     {
96         parent::handle($args);
97
98         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
99
100             $this->handlePost();
101
102         } else {
103
104             // Make sure a oauth_token parameter was provided
105             if (empty($this->oauthTokenParam)) {
106                 // TRANS: Client error given when no oauth_token was passed to the OAuth API.
107                 $this->clientError(_('No oauth_token parameter provided.'));
108             } else {
109
110                 // Check to make sure the token exists
111                 $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
112
113                 if (empty($this->reqToken)) {
114                     // TRANS: Client error given when an invalid request token was passed to the OAuth API.
115                     $this->clientError(_('Invalid request token.'));
116                 } else {
117
118                     // Check to make sure we haven't already authorized the token
119                     if ($this->reqToken->state != 0) {
120                         // TRANS: Client error given when an invalid request token was passed to the OAuth API.
121                         $this->clientError(_('Request token already authorized.'));
122                     }
123                 }
124             }
125
126             // make sure there's an app associated with this token
127             if (empty($this->app)) {
128                 // TRANS: Client error given when an invalid request token was passed to the OAuth API.
129                 $this->clientError(_('Invalid request token.'));
130             }
131
132             $name = $this->app->name;
133
134             $this->showForm();
135         }
136     }
137
138     function handlePost()
139     {
140         // check session token for CSRF protection.
141
142         $token = $this->trimmed('token');
143
144         if (!$token || $token != common_session_token()) {
145             $this->showForm(
146                 // TRANS: Form validation error in API OAuth authorisation because of an invalid session token.
147                 _('There was a problem with your session token. Try again, please.'));
148             return;
149         }
150
151         // check creds
152
153         $user = null;
154
155         if (!common_logged_in()) {
156
157             // XXX Force credentials check?
158
159             // @fixme this should probably use a unified login form handler
160             $user = null;
161             if (Event::handle('StartOAuthLoginCheck', array($this, &$user))) {
162                 $user = common_check_user($this->nickname, $this->password);
163             }
164             Event::handle('EndOAuthLoginCheck', array($this, &$user));
165
166             if (empty($user)) {
167                 // TRANS: Form validation error given when an invalid username and/or password was passed to the OAuth API.
168                 $this->showForm(_("Invalid nickname / password!"));
169                 return;
170             }
171         } else {
172             $user = common_current_user();
173         }
174
175         // fetch the token
176         $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
177         assert(!empty($this->reqToken));
178
179         if ($this->arg('allow')) {
180             // mark the req token as authorized
181             try {
182                 $this->store->authorize_token($this->oauthTokenParam);
183             } catch (Exception $e) {
184                 $this->serverError($e->getMessage());
185             }
186
187             common_log(
188                 LOG_INFO,
189                 sprintf(
190                     "API OAuth - User %d (%s) has authorized request token %s for OAuth application %d (%s).",
191                     $user->id,
192                     $user->nickname,
193                     $this->reqToken->tok,
194                     $this->app->id,
195                     $this->app->name
196                 )
197             );
198
199             $tokenAssoc = new Oauth_token_association();
200
201             $tokenAssoc->profile_id     = $user->id;
202             $tokenAssoc->application_id = $this->app->id;
203             $tokenAssoc->token          = $this->oauthTokenParam;
204             $tokenAssoc->created        = common_sql_now();
205
206             $result = $tokenAssoc->insert();
207
208             if (!$result) {
209                 common_log_db_error($tokenAssoc, 'INSERT', __FILE__);
210                 // TRANS: Server error displayed when a database action fails.
211                 $this->serverError(_('Database error inserting oauth_token_association.'));
212             }
213
214             $callback = $this->getCallback();
215
216             if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
217                 $targetUrl = $this->buildCallbackUrl(
218                     $callback,
219                     array(
220                         'oauth_token'    => $this->oauthTokenParam,
221                         'oauth_verifier' => $this->reqToken->verifier // 1.0a
222                     )
223                 );
224
225                 common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
226
227                 // Redirect the user to the provided OAuth callback
228                 common_redirect($targetUrl, 303);
229
230             } elseif ($this->app->type == 2) {
231                 // Strangely, a web application seems to want to do the OOB
232                 // workflow. Because no callback was specified anywhere.
233                 common_log(
234                     LOG_WARNING,
235                     sprintf(
236                         "API OAuth - No callback provided for OAuth web client ID %s (%s) "
237                          . "during authorization step. Falling back to OOB workflow.",
238                         $this->app->id,
239                         $this->app->name
240                     )
241                 );
242             }
243
244             // Otherwise, inform the user that the rt was authorized
245             $this->showAuthorized();
246         } else if ($this->arg('cancel')) {
247             common_log(
248                 LOG_INFO,
249                 sprintf(
250                     "API OAuth - User %d (%s) refused to authorize request token %s for OAuth application %d (%s).",
251                     $user->id,
252                     $user->nickname,
253                     $this->reqToken->tok,
254                     $this->app->id,
255                     $this->app->name
256                 )
257             );
258
259             try {
260                 $this->store->revoke_token($this->oauthTokenParam, 0);
261             } catch (Exception $e) {
262                 $this->ServerError($e->getMessage());
263             }
264
265             $callback = $this->getCallback();
266
267             // If there's a callback available, inform the consumer the user
268             // has refused authorization
269             if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
270                 $targetUrl = $this->buildCallbackUrl(
271                     $callback,
272                     array(
273                         'oauth_problem' => 'user_refused',
274                     )
275                 );
276
277                 common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
278
279                 // Redirect the user to the provided OAuth callback
280                 common_redirect($targetUrl, 303);
281             }
282
283             // otherwise inform the user that authorization for the rt was declined
284             $this->showCanceled();
285
286         } else {
287             // TRANS: Client error given on when invalid data was passed through a form in the OAuth API.
288             $this->clientError(_('Unexpected form submission.'));
289         }
290     }
291
292     /**
293      * Show body - override to add a special CSS class for the authorize
294      * page's "desktop mode" (minimal display)
295      *
296      * Calls template methods
297      *
298      * @return nothing
299      */
300     function showBody()
301     {
302         $bodyClasses = array();
303
304         if ($this->desktopMode()) {
305             $bodyClasses[] = 'oauth-desktop-mode';
306         }
307
308         if (common_current_user()) {
309             $bodyClasses[] = 'user_in';
310         }
311
312         $attrs = array('id' => strtolower($this->trimmed('action')));
313
314         if (!empty($bodyClasses)) {
315             $attrs['class'] = implode(' ', $bodyClasses);
316         }
317
318         $this->elementStart('body', $attrs);
319
320         $this->elementStart('div', array('id' => 'wrap'));
321         if (Event::handle('StartShowHeader', array($this))) {
322             $this->showHeader();
323             Event::handle('EndShowHeader', array($this));
324         }
325         $this->showCore();
326         if (Event::handle('StartShowFooter', array($this))) {
327             $this->showFooter();
328             Event::handle('EndShowFooter', array($this));
329         }
330         $this->elementEnd('div');
331         $this->showScripts();
332         $this->elementEnd('body');
333     }
334
335     function showForm($error=null)
336     {
337         $this->error = $error;
338         $this->showPage();
339     }
340
341     function showScripts()
342     {
343         parent::showScripts();
344         if (!common_logged_in()) {
345             $this->autofocus('nickname');
346         }
347     }
348
349     /**
350      * Title of the page
351      *
352      * @return string title of the page
353      */
354     function title()
355     {
356         // TRANS: Title for a page where a user can confirm/deny account access by an external application.
357         return _('An application would like to connect to your account');
358     }
359
360     /**
361      * Shows the authorization form.
362      *
363      * @return void
364      */
365     function showContent()
366     {
367         $this->elementStart('form', array('method' => 'post',
368                                           'id' => 'form_apioauthauthorize',
369                                           'class' => 'form_settings',
370                                           'action' => common_local_url('ApiOauthAuthorize')));
371         $this->elementStart('fieldset');
372         $this->element('legend', array('id' => 'apioauthauthorize_allowdeny'),
373                                  // TRANS: Fieldset legend.
374                                  _('Allow or deny access'));
375
376         $this->hidden('token', common_session_token());
377         $this->hidden('mode', $this->mode);
378         $this->hidden('oauth_token', $this->oauthTokenParam);
379         $this->hidden('oauth_callback', $this->callback);
380
381         $this->elementStart('ul', 'form_data');
382         $this->elementStart('li');
383         $this->elementStart('p');
384         if (!empty($this->app->icon) && $this->app->name != 'anonymous') {
385             $this->element('img', array('src' => $this->app->icon));
386         }
387
388         $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
389           'access and update' : 'access';
390
391         if ($this->app->name == 'anonymous') {
392             // Special message for the anonymous app and consumer.
393             // TRANS: User notification of external application requesting account access.
394             // TRANS: %3$s is the access type requested (read-write or read-only), %4$s is the StatusNet sitename.
395             $msg = _('An application would like the ability ' .
396                  'to <strong>%3$s</strong> your %4$s account data. ' .
397                  'You should only give access to your %4$s account ' .
398                  'to third parties you trust.');
399         } else {
400             // TRANS: User notification of external application requesting account access.
401             // TRANS: %1$s is the application name requesting access, %2$s is the organisation behind the application,
402             // TRANS: %3$s is the access type requested, %4$s is the StatusNet sitename.
403             $msg = _('The application <strong>%1$s</strong> by ' .
404                      '<strong>%2$s</strong> would like the ability ' .
405                      'to <strong>%3$s</strong> your %4$s account data. ' .
406                      'You should only give access to your %4$s account ' .
407                      'to third parties you trust.');
408         }
409
410         $this->raw(sprintf($msg,
411                            $this->app->name,
412                            $this->app->organization,
413                            $access,
414                            common_config('site', 'name')));
415         $this->elementEnd('p');
416         $this->elementEnd('li');
417         $this->elementEnd('ul');
418
419         // quickie hack
420         $button = false;
421         if (!common_logged_in()) {
422             if (Event::handle('StartOAuthLoginForm', array($this, &$button))) {
423                 $this->elementStart('fieldset');
424                 // TRANS: Fieldset legend.
425                 $this->element('legend', null, _m('LEGEND','Account'));
426                 $this->elementStart('ul', 'form_data');
427                 $this->elementStart('li');
428                 // TRANS: Field label on OAuth API authorisation form.
429                 $this->input('nickname', _('Nickname'));
430                 $this->elementEnd('li');
431                 $this->elementStart('li');
432                 // TRANS: Field label on OAuth API authorisation form.
433                 $this->password('password', _('Password'));
434                 $this->elementEnd('li');
435                 $this->elementEnd('ul');
436
437                 $this->elementEnd('fieldset');
438             }
439             Event::handle('EndOAuthLoginForm', array($this, &$button));
440         }
441
442         $this->element('input', array('id' => 'cancel_submit',
443                                       'class' => 'submit submit form_action-primary',
444                                       'name' => 'cancel',
445                                       'type' => 'submit',
446                                       // TRANS: Button text that when clicked will cancel the process of allowing access to an account
447                                       // TRANS: by an external application.
448                                       'value' => _m('BUTTON','Cancel')));
449
450         $this->element('input', array('id' => 'allow_submit',
451                                       'class' => 'submit submit form_action-secondary',
452                                       'name' => 'allow',
453                                       'type' => 'submit',
454                                       // TRANS: Button text that when clicked will allow access to an account by an external application.
455                                       'value' => $button ? $button : _m('BUTTON','Allow')));
456
457         $this->elementEnd('fieldset');
458         $this->elementEnd('form');
459     }
460
461     /**
462      * Instructions for using the form
463      *
464      * For "remembered" logins, we make the user re-login when they
465      * try to change settings. Different instructions for this case.
466      *
467      * @return void
468      */
469     function getInstructions()
470     {
471         // TRANS: Form instructions.
472         return _('Authorize access to your account information.');
473     }
474
475     /**
476      * A local menu
477      *
478      * Shows different login/register actions.
479      *
480      * @return void
481      */
482     function showLocalNav()
483     {
484         // NOP
485     }
486
487     /*
488      * Checks to see if a the "mode" parameter is present in the request
489      * and set to "desktop". If it is, the page is meant to be displayed in
490      * a small frame of another application, and we should  suppress the
491      * header, aside, and footer.
492      */
493     function desktopMode()
494     {
495         if (isset($this->mode) && $this->mode == 'desktop') {
496             return true;
497         } else {
498             return false;
499         }
500     }
501
502     /*
503      * Override - suppress output in "desktop" mode
504      */
505     function showHeader()
506     {
507         if ($this->desktopMode() == false) {
508             parent::showHeader();
509         }
510     }
511
512     /*
513      * Override - suppress output in "desktop" mode
514      */
515     function showAside()
516     {
517         if ($this->desktopMode() == false) {
518             parent::showAside();
519         }
520     }
521
522     /*
523      * Override - suppress output in "desktop" mode
524      */
525     function showFooter()
526     {
527         if ($this->desktopMode() == false) {
528             parent::showFooter();
529         }
530     }
531
532     /**
533      * Show site notice.
534      *
535      * @return nothing
536      */
537     function showSiteNotice()
538     {
539         // NOP
540     }
541
542     /**
543      * Show notice form.
544      *
545      * Show the form for posting a new notice
546      *
547      * @return nothing
548      */
549     function showNoticeForm()
550     {
551         // NOP
552     }
553
554     /*
555      * Show a nice message confirming the authorization
556      * operation was canceled.
557      *
558      * @return nothing
559      */
560     function showCanceled()
561     {
562         $info = new InfoAction(
563             // TRANS: Header for user notification after revoking OAuth access to an application.
564             _('Authorization canceled.'),
565             sprintf(
566                 // TRANS: User notification after revoking OAuth access to an application.
567                 // TRANS: %s is an OAuth token.
568                 _('The request token %s has been revoked.'),
569                 $this->oauthTokenParam
570             )
571         );
572
573         $info->showPage();
574     }
575
576     /*
577      * Show a nice message that the authorization was successful.
578      * If the operation is out-of-band, show a pin.
579      *
580      * @return nothing
581      */
582     function showAuthorized()
583     {
584         $title = null;
585         $msg   = null;
586
587         if ($this->app->name == 'anonymous') {
588
589             $title =
590                 // TRANS: Title of the page notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
591                 _('You have successfully authorized the application');
592
593             $msg =
594                 // TRANS: Message notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
595                 _('Please return to the application and enter the following security code to complete the process.');
596
597         } else {
598
599             $title = sprintf(
600                 // TRANS: Title of the page notifying the user that the client application was successfully authorized to access the user's account with OAuth.
601                 // TRANS: %s is the authorised application name.
602                 _('You have successfully authorized %s'),
603                 $this->app->name
604             );
605
606             $msg = sprintf(
607                 // TRANS: Message notifying the user that the client application was successfully authorized to access the user's account with OAuth.
608                 // TRANS: %s is the authorised application name.
609                 _('Please return to %s and enter the following security code to complete the process.'),
610                 $this->app->name
611             );
612
613         }
614
615         if ($this->reqToken->verified_callback == 'oob') {
616             $pin = new ApiOauthPinAction(
617                 $title,
618                 $msg,
619                 $this->reqToken->verifier,
620                 $this->desktopMode()
621             );
622             $pin->showPage();
623         } else {
624             // NOTE: This would only happen if an application registered as
625             // a web application but sent in 'oob' for the oauth_callback
626             // parameter. Usually web apps will send in a callback and
627             // not use the pin-based workflow.
628
629             $info = new InfoAction(
630                 $title,
631                 $msg,
632                 $this->oauthTokenParam,
633                 $this->reqToken->verifier
634             );
635
636             $info->showPage();
637         }
638     }
639
640     /*
641      * Figure out what the callback should be
642      */
643     function getCallback()
644     {
645         $callback = null;
646
647         // Return the verified callback if we have one
648         if ($this->reqToken->verified_callback != 'oob') {
649
650             $callback = $this->reqToken->verified_callback;
651
652             // Otherwise return the callback that was provided when
653             // registering the app
654             if (empty($callback)) {
655
656                 common_debug(
657                     "No verified callback found for request token, using application callback: "
658                         . $this->app->callback_url,
659                      __FILE__
660                 );
661
662                 $callback = $this->app->callback_url;
663             }
664         }
665
666         return $callback;
667     }
668
669     /*
670      * Properly format the callback URL and parameters so it's
671      * suitable for a redirect in the OAuth dance
672      *
673      * @param string $url       the URL
674      * @param array  $params    an array of parameters
675      *
676      * @return string $url  a URL to use for redirecting to
677      */
678     function buildCallbackUrl($url, $params)
679     {
680         foreach ($params as $k => $v) {
681             $url = $this->appendQueryVar(
682                 $url,
683                 OAuthUtil::urlencode_rfc3986($k),
684                 OAuthUtil::urlencode_rfc3986($v)
685             );
686         }
687
688         return $url;
689     }
690
691     /*
692      * Append a new query parameter after any existing query
693      * parameters.
694      *
695      * @param string $url   the URL
696      * @prarm string $k     the parameter name
697      * @param string $v     value of the paramter
698      *
699      * @return string $url  the new URL with added parameter
700      */
701     function appendQueryVar($url, $k, $v) {
702         $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
703         $url = substr($url, 0, -1);
704         if (strpos($url, '?') === false) {
705             return ($url . '?' . $k . '=' . $v);
706         } else {
707             return ($url . '&' . $k . '=' . $v);
708         }
709     }
710 }