]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/apioauthauthorize.php
Merge remote branch 'gitorious/1.0.x' into 1.0.x
[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 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 Oputh 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             // XXX: Make sure we have a oauth_token_association table. The table
200             // is now in the main schema, but because it is being added with
201             // a point release, it's unlikely to be there. This code can be
202             // removed as of 1.0.
203             $this->ensureOauthTokenAssociationTable();
204
205             $tokenAssoc = new Oauth_token_association();
206
207             $tokenAssoc->profile_id     = $user->id;
208             $tokenAssoc->application_id = $this->app->id;
209             $tokenAssoc->token          = $this->oauthTokenParam;
210             $tokenAssoc->created        = common_sql_now();
211
212             $result = $tokenAssoc->insert();
213
214             if (!$result) {
215                 common_log_db_error($tokenAssoc, 'INSERT', __FILE__);
216                 // TRANS: Server error displayed when a database action fails.
217                 $this->serverError(_('Database error inserting oauth_token_association.'));
218             }
219
220             $callback = $this->getCallback();
221
222             if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
223                 $targetUrl = $this->buildCallbackUrl(
224                     $callback,
225                     array(
226                         'oauth_token'    => $this->oauthTokenParam,
227                         'oauth_verifier' => $this->reqToken->verifier // 1.0a
228                     )
229                 );
230
231                 common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
232
233                 // Redirect the user to the provided OAuth callback
234                 common_redirect($targetUrl, 303);
235
236             } elseif ($this->app->type == 2) {
237                 // Strangely, a web application seems to want to do the OOB
238                 // workflow. Because no callback was specified anywhere.
239                 common_log(
240                     LOG_WARNING,
241                     sprintf(
242                         "API OAuth - No callback provided for OAuth web client ID %s (%s) "
243                          . "during authorization step. Falling back to OOB workflow.",
244                         $this->app->id,
245                         $this->app->name
246                     )
247                 );
248             }
249
250             // Otherwise, inform the user that the rt was authorized
251             $this->showAuthorized();
252         } else if ($this->arg('cancel')) {
253             common_log(
254                 LOG_INFO,
255                 sprintf(
256                     "API OAuth - User %d (%s) refused to authorize request token %s for OAuth application %d (%s).",
257                     $user->id,
258                     $user->nickname,
259                     $this->reqToken->tok,
260                     $this->app->id,
261                     $this->app->name
262                 )
263             );
264
265             try {
266                 $this->store->revoke_token($this->oauthTokenParam, 0);
267             } catch (Exception $e) {
268                 $this->ServerError($e->getMessage());
269             }
270
271             $callback = $this->getCallback();
272
273             // If there's a callback available, inform the consumer the user
274             // has refused authorization
275             if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
276                 $targetUrl = $this->buildCallbackUrl(
277                     $callback,
278                     array(
279                         'oauth_problem' => 'user_refused',
280                     )
281                 );
282
283                 common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
284
285                 // Redirect the user to the provided OAuth callback
286                 common_redirect($targetUrl, 303);
287             }
288
289             // otherwise inform the user that authorization for the rt was declined
290             $this->showCanceled();
291
292         } else {
293             // TRANS: Client error given on when invalid data was passed through a form in the OAuth API.
294             $this->clientError(_('Unexpected form submission.'));
295         }
296     }
297
298     // XXX Remove this function when we hit 1.0
299     function ensureOauthTokenAssociationTable()
300     {
301         $schema = Schema::get();
302
303         $reqTokenCols = array(
304             new ColumnDef('profile_id', 'integer', null, true, 'PRI'),
305             new ColumnDef('application_id', 'integer', null, true, 'PRI'),
306             new ColumnDef('token', 'varchar', 255, true, 'PRI'),
307             new ColumnDef('created', 'datetime', null, false),
308             new ColumnDef(
309                 'modified',
310                 'timestamp',
311                 null,
312                 false,
313                 null,
314                 'CURRENT_TIMESTAMP',
315                 'on update CURRENT_TIMESTAMP'
316             )
317         );
318
319         $schema->ensureTable('oauth_token_association', $reqTokenCols);
320     }
321
322     /**
323      * Show body - override to add a special CSS class for the authorize
324      * page's "desktop mode" (minimal display)
325      *
326      * Calls template methods
327      *
328      * @return nothing
329      */
330     function showBody()
331     {
332         $bodyClasses = array();
333
334         if ($this->desktopMode()) {
335             $bodyClasses[] = 'oauth-desktop-mode';
336         }
337
338         if (common_current_user()) {
339             $bodyClasses[] = 'user_in';
340         }
341
342         $attrs = array('id' => strtolower($this->trimmed('action')));
343
344         if (!empty($bodyClasses)) {
345             $attrs['class'] = implode(' ', $bodyClasses);
346         }
347
348         $this->elementStart('body', $attrs);
349
350         $this->elementStart('div', array('id' => 'wrap'));
351         if (Event::handle('StartShowHeader', array($this))) {
352             $this->showHeader();
353             Event::handle('EndShowHeader', array($this));
354         }
355         $this->showCore();
356         if (Event::handle('StartShowFooter', array($this))) {
357             $this->showFooter();
358             Event::handle('EndShowFooter', array($this));
359         }
360         $this->elementEnd('div');
361         $this->showScripts();
362         $this->elementEnd('body');
363     }
364
365     function showForm($error=null)
366     {
367         $this->error = $error;
368         $this->showPage();
369     }
370
371     function showScripts()
372     {
373         parent::showScripts();
374         if (!common_logged_in()) {
375             $this->autofocus('nickname');
376         }
377     }
378
379     /**
380      * Title of the page
381      *
382      * @return string title of the page
383      */
384     function title()
385     {
386         // TRANS: Title for a page where a user can confirm/deny account access by an external application.
387         return _('An application would like to connect to your account');
388     }
389
390     /**
391      * Shows the authorization form.
392      *
393      * @return void
394      */
395     function showContent()
396     {
397         $this->elementStart('form', array('method' => 'post',
398                                           'id' => 'form_apioauthauthorize',
399                                           'class' => 'form_settings',
400                                           'action' => common_local_url('ApiOauthAuthorize')));
401         $this->elementStart('fieldset');
402         $this->element('legend', array('id' => 'apioauthauthorize_allowdeny'),
403                                  // TRANS: Fieldset legend.
404                                  _('Allow or deny access'));
405
406         $this->hidden('token', common_session_token());
407         $this->hidden('mode', $this->mode);
408         $this->hidden('oauth_token', $this->oauthTokenParam);
409         $this->hidden('oauth_callback', $this->callback);
410
411         $this->elementStart('ul', 'form_data');
412         $this->elementStart('li');
413         $this->elementStart('p');
414         if (!empty($this->app->icon) && $this->app->name != 'anonymous') {
415             $this->element('img', array('src' => $this->app->icon));
416         }
417
418         $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
419           'access and update' : 'access';
420
421         if ($this->app->name == 'anonymous') {
422             // Special message for the anonymous app and consumer.
423             // TRANS: User notification of external application requesting account access.
424             // TRANS: %3$s is the access type requested, %4$s is the StatusNet sitename.
425             $msg = _('An application would like the ability ' .
426                  'to <strong>%3$s</strong> your %4$s account data. ' .
427                  'You should only give access to your %4$s account ' .
428                  'to third parties you trust.');
429         } else {
430             // TRANS: User notification of external application requesting account access.
431             // TRANS: %1$s is the application name requesting access, %2$s is the organisation behind the application,
432             // TRANS: %3$s is the access type requested, %4$s is the StatusNet sitename.
433             $msg = _('The application <strong>%1$s</strong> by ' .
434                      '<strong>%2$s</strong> would like the ability ' .
435                      'to <strong>%3$s</strong> your %4$s account data. ' .
436                      'You should only give access to your %4$s account ' .
437                      'to third parties you trust.');
438         }
439
440         $this->raw(sprintf($msg,
441                            $this->app->name,
442                            $this->app->organization,
443                            $access,
444                            common_config('site', 'name')));
445         $this->elementEnd('p');
446         $this->elementEnd('li');
447         $this->elementEnd('ul');
448
449         // quickie hack
450         $button = false;
451         if (!common_logged_in()) {
452             if (Event::handle('StartOAuthLoginForm', array($this, &$button))) {
453                 $this->elementStart('fieldset');
454                 // TRANS: Fieldset legend.
455                 $this->element('legend', null, _m('LEGEND','Account'));
456                 $this->elementStart('ul', 'form_data');
457                 $this->elementStart('li');
458                 // TRANS: Field label on OAuth API authorisation form.
459                 $this->input('nickname', _('Nickname'));
460                 $this->elementEnd('li');
461                 $this->elementStart('li');
462                 // TRANS: Field label on OAuth API authorisation form.
463                 $this->password('password', _('Password'));
464                 $this->elementEnd('li');
465                 $this->elementEnd('ul');
466
467                 $this->elementEnd('fieldset');
468             }
469             Event::handle('EndOAuthLoginForm', array($this, &$button));
470         }
471
472         $this->element('input', array('id' => 'cancel_submit',
473                                       'class' => 'submit submit form_action-primary',
474                                       'name' => 'cancel',
475                                       'type' => 'submit',
476                                       // TRANS: Button text that when clicked will cancel the process of allowing access to an account
477                                       // TRANS: by an external application.
478                                       'value' => _m('BUTTON','Cancel')));
479
480         $this->element('input', array('id' => 'allow_submit',
481                                       'class' => 'submit submit form_action-secondary',
482                                       'name' => 'allow',
483                                       'type' => 'submit',
484                                       // TRANS: Button text that when clicked will allow access to an account by an external application.
485                                       'value' => $button ? $button : _m('BUTTON','Allow')));
486
487         $this->elementEnd('fieldset');
488         $this->elementEnd('form');
489     }
490
491     /**
492      * Instructions for using the form
493      *
494      * For "remembered" logins, we make the user re-login when they
495      * try to change settings. Different instructions for this case.
496      *
497      * @return void
498      */
499     function getInstructions()
500     {
501         // TRANS: Form instructions.
502         return _('Authorize access to your account information.');
503     }
504
505     /**
506      * A local menu
507      *
508      * Shows different login/register actions.
509      *
510      * @return void
511      */
512     function showLocalNav()
513     {
514         // NOP
515     }
516
517     /*
518      * Checks to see if a the "mode" parameter is present in the request
519      * and set to "desktop". If it is, the page is meant to be displayed in
520      * a small frame of another application, and we should  suppress the
521      * header, aside, and footer.
522      */
523     function desktopMode()
524     {
525         if (isset($this->mode) && $this->mode == 'desktop') {
526             return true;
527         } else {
528             return false;
529         }
530     }
531
532     /*
533      * Override - suppress output in "desktop" mode
534      */
535     function showHeader()
536     {
537         if ($this->desktopMode() == false) {
538             parent::showHeader();
539         }
540     }
541
542     /*
543      * Override - suppress output in "desktop" mode
544      */
545     function showAside()
546     {
547         if ($this->desktopMode() == false) {
548             parent::showAside();
549         }
550     }
551
552     /*
553      * Override - suppress output in "desktop" mode
554      */
555     function showFooter()
556     {
557         if ($this->desktopMode() == false) {
558             parent::showFooter();
559         }
560     }
561
562     /**
563      * Show site notice.
564      *
565      * @return nothing
566      */
567     function showSiteNotice()
568     {
569         // NOP
570     }
571
572     /**
573      * Show notice form.
574      *
575      * Show the form for posting a new notice
576      *
577      * @return nothing
578      */
579     function showNoticeForm()
580     {
581         // NOP
582     }
583
584     /*
585      * Show a nice message confirming the authorization
586      * operation was canceled.
587      *
588      * @return nothing
589      */
590     function showCanceled()
591     {
592         $info = new InfoAction(
593             // TRANS: Header for user notification after revoking OAuth access to an application.
594             _('Authorization canceled.'),
595             sprintf(
596                 // TRANS: User notification after revoking OAuth access to an application.
597                 // TRANS: %s is an OAuth token.
598                 _('The request token %s has been revoked.'),
599                 $this->oauthTokenParam
600             )
601         );
602
603         $info->showPage();
604     }
605
606     /*
607      * Show a nice message that the authorization was successful.
608      * If the operation is out-of-band, show a pin.
609      *
610      * @return nothing
611      */
612     function showAuthorized()
613     {
614         $title = null;
615         $msg   = null;
616
617         if ($this->app->name == 'anonymous') {
618
619             $title =
620                 // TRANS: Title of the page notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
621                 _('You have successfully authorized the application');
622
623             $msg =
624                 // TRANS: Message notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
625                 _('Please return to the application and enter the following security code to complete the process.');
626
627         } else {
628
629             $title = sprintf(
630                 // TRANS: Title of the page notifying the user that the client application was successfully authorized to access the user's account with OAuth.
631                 // TRANS: %s is the authorised application name.
632                 _('You have successfully authorized %s'),
633                 $this->app->name
634             );
635
636             $msg = sprintf(
637                 // TRANS: Message notifying the user that the client application was successfully authorized to access the user's account with OAuth.
638                 // TRANS: %s is the authorised application name.
639                 _('Please return to %s and enter the following security code to complete the process.'),
640                 $this->app->name
641             );
642
643         }
644
645         if ($this->reqToken->verified_callback == 'oob') {
646             $pin = new ApiOauthPinAction(
647                 $title,
648                 $msg,
649                 $this->reqToken->verifier,
650                 $this->desktopMode()
651             );
652             $pin->showPage();
653         } else {
654             // NOTE: This would only happen if an application registered as
655             // a web application but sent in 'oob' for the oauth_callback
656             // parameter. Usually web apps will send in a callback and
657             // not use the pin-based workflow.
658
659             $info = new InfoAction(
660                 $title,
661                 $msg,
662                 $this->oauthTokenParam,
663                 $this->reqToken->verifier
664             );
665
666             $info->showPage();
667         }
668     }
669
670     /*
671      * Figure out what the callback should be
672      */
673     function getCallback()
674     {
675         $callback = null;
676
677         // Return the verified callback if we have one
678         if ($this->reqToken->verified_callback != 'oob') {
679
680             $callback = $this->reqToken->verified_callback;
681
682             // Otherwise return the callback that was provided when
683             // registering the app
684             if (empty($callback)) {
685
686                 common_debug(
687                     "No verified callback found for request token, using application callback: "
688                         . $this->app->callback_url,
689                      __FILE__
690                 );
691
692                 $callback = $this->app->callback_url;
693             }
694         }
695
696         return $callback;
697     }
698
699     /*
700      * Properly format the callback URL and parameters so it's
701      * suitable for a redirect in the OAuth dance
702      *
703      * @param string $url       the URL
704      * @param array  $params    an array of parameters
705      *
706      * @return string $url  a URL to use for redirecting to
707      */
708     function buildCallbackUrl($url, $params)
709     {
710         foreach ($params as $k => $v) {
711             $url = $this->appendQueryVar(
712                 $url,
713                 OAuthUtil::urlencode_rfc3986($k),
714                 OAuthUtil::urlencode_rfc3986($v)
715             );
716         }
717
718         return $url;
719     }
720
721     /*
722      * Append a new query parameter after any existing query
723      * parameters.
724      *
725      * @param string $url   the URL
726      * @prarm string $k     the parameter name
727      * @param string $v     value of the paramter
728      *
729      * @return string $url  the new URL with added parameter
730      */
731     function appendQueryVar($url, $k, $v) {
732         $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
733         $url = substr($url, 0, -1);
734         if (strpos($url, '?') === false) {
735             return ($url . '?' . $k . '=' . $v);
736         } else {
737             return ($url . '&' . $k . '=' . $v);
738         }
739     }
740 }