3 * StatusNet, the distributed open-source microblogging tool
5 * Authorize an OAuth request token
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.
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.
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/>.
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/
30 if (!defined('STATUSNET')) {
34 require_once INSTALLDIR . '/lib/apioauth.php';
35 require_once INSTALLDIR . '/lib/info.php';
38 * Authorize an OAuth request token
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/
47 class ApiOauthAuthorizeAction extends Action
58 * Is this a read-only action?
60 * @return boolean false
63 function isReadOnly($args)
68 function prepare($args)
70 parent::prepare($args);
72 $this->nickname = $this->trimmed('nickname');
73 $this->password = $this->arg('password');
74 $this->oauthTokenParam = $this->arg('oauth_token');
75 $this->callback = $this->arg('oauth_callback');
76 $this->store = new ApiStatusNetOAuthDataStore();
79 $this->app = $this->store->getAppByRequestToken($this->oauthTokenParam);
80 } catch (Exception $e) {
81 $this->clientError($e->getMessage());
88 * Handle input, produce output
90 * Switches on request method; either shows the form or handles its input.
92 * @param array $args $_REQUEST data
97 function handle($args)
99 parent::handle($args);
101 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
107 // Make sure a oauth_token parameter was provided
108 if (empty($this->oauthTokenParam)) {
109 $this->clientError(_('No oauth_token parameter provided.'));
112 // Check to make sure the token exists
113 $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
115 if (empty($this->reqToken)) {
116 $this->clientError(_('Invalid request token.'));
119 // Check to make sure we haven't already authorized the token
120 if ($this->reqToken->state != 0) {
121 $this->clientError(_("Invalid request token."));
126 // make sure there's an app associated with this token
127 if (empty($this->app)) {
128 $this->clientError(_('Invalid request token.'));
131 $name = $this->app->name;
137 function handlePost()
139 // check session token for CSRF protection.
141 $token = $this->trimmed('token');
143 if (!$token || $token != common_session_token()) {
145 _('There was a problem with your session token. Try again, please.'));
153 if (!common_logged_in()) {
155 // XXX Force credentials check?
159 $user = common_check_user($this->nickname, $this->password);
161 $this->showForm(_("Invalid nickname / password!"));
165 $user = common_current_user();
168 if ($this->arg('allow')) {
171 $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
173 // mark the req token as authorized
175 $this->store->authorize_token($this->oauthTokenParam);
176 } catch (Exception $e) {
177 $this->serverError($e->getMessage());
180 // Check to see if there was a previous token associated
181 // with this user/app and kill it. If the user is doing this she
182 // probably doesn't want any old tokens anyway.
184 $appUser = Oauth_application_user::getByKeys($user, $this->app);
186 if (!empty($appUser)) {
187 $result = $appUser->delete();
190 common_log_db_error($appUser, 'DELETE', __FILE__);
191 $this->serverError(_('Database error deleting OAuth application user.'));
195 // associated the authorized req token with the user and the app
197 $appUser = new Oauth_application_user();
199 $appUser->profile_id = $user->id;
200 $appUser->application_id = $this->app->id;
202 // Note: do not copy the access type from the application.
203 // The access type should always be 0 when the OAuth app
204 // user record has a request token associated with it.
205 // Access type gets assigned once an access token has been
206 // granted. The OAuth app user record then gets updated
207 // with the new access token and access type.
209 $appUser->token = $this->oauthTokenParam;
210 $appUser->created = common_sql_now();
212 $result = $appUser->insert();
215 common_log_db_error($appUser, 'INSERT', __FILE__);
216 $this->serverError(_('Database error inserting OAuth application user.'));
219 // If we have a callback redirect and provide the token
221 // Note: A callback specified in the app setup overrides whatever
222 // is passed in with the request.
224 if (!empty($this->app->callback_url)) {
225 $this->callback = $this->app->callback_url;
228 if (!empty($this->callback)) {
230 $targetUrl = $this->getCallback(
233 'oauth_token' => $this->oauthTokenParam,
234 'oauth_verifier' => $this->reqToken->verifier // 1.0a
238 // Redirect the user to the provided OAuth callback
239 common_redirect($targetUrl, 303);
241 } elseif ($this->app->type == 2) {
243 // Strangely, a web application seems to want to do the OOB
244 // workflow. Because no callback was specified anywhere.
248 "API OAuth - No callback provided for OAuth web client ID %s (%s) "
249 . "during authorization step. Falling back to OOB workflow.",
259 "The request token '%s' for OAuth application %s (%s) has been authorized.",
260 $this->oauthTokenParam,
266 // Otherwise, inform the user that the rt was authorized
267 $this->showAuthorized();
269 } else if ($this->arg('cancel')) {
272 $this->store->revoke_token($this->oauthTokenParam, 0);
273 $this->showCanceled();
274 } catch (Exception $e) {
275 $this->ServerError($e->getMessage());
279 $this->clientError(_('Unexpected form submission.'));
283 function showForm($error=null)
285 $this->error = $error;
289 function showScripts()
291 parent::showScripts();
292 if (!common_logged_in()) {
293 $this->autofocus('nickname');
300 * @return string title of the page
305 return _('An application would like to connect to your account');
309 * Shows the authorization form.
314 function showContent()
316 $this->elementStart('form', array('method' => 'post',
317 'id' => 'form_apioauthauthorize',
318 'class' => 'form_settings',
319 'action' => common_local_url('ApiOauthAuthorize')));
320 $this->elementStart('fieldset');
321 $this->element('legend', array('id' => 'apioauthauthorize_allowdeny'),
322 _('Allow or deny access'));
324 $this->hidden('token', common_session_token());
325 $this->hidden('oauth_token', $this->oauthTokenParam);
326 $this->hidden('oauth_callback', $this->callback);
328 $this->elementStart('ul', 'form_data');
329 $this->elementStart('li');
330 $this->elementStart('p');
331 if (!empty($this->app->icon)) {
332 $this->element('img', array('src' => $this->app->icon));
335 $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
336 'access and update' : 'access';
338 $msg = _('The application <strong>%1$s</strong> by ' .
339 '<strong>%2$s</strong> would like the ability ' .
340 'to <strong>%3$s</strong> your %4$s account data. ' .
341 'You should only give access to your %4$s account ' .
342 'to third parties you trust.');
344 $this->raw(sprintf($msg,
346 $this->app->organization,
348 common_config('site', 'name')));
349 $this->elementEnd('p');
350 $this->elementEnd('li');
351 $this->elementEnd('ul');
353 if (!common_logged_in()) {
355 $this->elementStart('fieldset');
356 $this->element('legend', null, _('Account'));
357 $this->elementStart('ul', 'form_data');
358 $this->elementStart('li');
359 $this->input('nickname', _('Nickname'));
360 $this->elementEnd('li');
361 $this->elementStart('li');
362 $this->password('password', _('Password'));
363 $this->elementEnd('li');
364 $this->elementEnd('ul');
366 $this->elementEnd('fieldset');
370 $this->element('input', array('id' => 'cancel_submit',
371 'class' => 'submit submit form_action-primary',
374 'value' => _('Cancel')));
376 $this->element('input', array('id' => 'allow_submit',
377 'class' => 'submit submit form_action-secondary',
380 'value' => _('Allow')));
382 $this->elementEnd('fieldset');
383 $this->elementEnd('form');
387 * Instructions for using the form
389 * For "remembered" logins, we make the user re-login when they
390 * try to change settings. Different instructions for this case.
395 function getInstructions()
397 return _('Authorize access to your account information.');
403 * Shows different login/register actions.
408 function showLocalNav()
419 function showSiteNotice()
427 * Show the form for posting a new notice
432 function showNoticeForm()
438 * Show a nice message confirming the authorization
439 * operation was canceled.
444 function showCanceled()
446 $info = new InfoAction(
447 _('Authorization canceled.'),
449 _('The request token %s has been revoked.'),
450 $this->oauthTokenParm
458 * Show a nice message that the authorization was successful.
459 * If the operation is out-of-band, show a pin.
464 function showAuthorized()
467 _("You have successfully authorized %s."),
472 _('Please return to %s and enter the following security code to complete the process.'),
476 if ($this->reqToken->verified_callback == 'oob') {
477 $pin = new ApiOauthPinAction($title, $msg, $this->reqToken->verifier);
481 // NOTE: This would only happen if an application registered as
482 // a web application but sent in 'oob' for the oauth_callback
483 // parameter. Usually web apps will send in a callback and
484 // not use the pin-based workflow.
486 $info = new InfoAction(
489 $this->oauthTokenParam,
490 $this->reqToken->verifier
498 * Properly format the callback URL and parameters so it's
499 * suitable for a redirect in the OAuth dance
501 * @param string $url the URL
502 * @param array $params an array of parameters
504 * @return string $url a URL to use for redirecting to
507 function getCallback($url, $params)
509 foreach ($params as $k => $v) {
510 $url = $this->appendQueryVar(
512 OAuthUtil::urlencode_rfc3986($k),
513 OAuthUtil::urlencode_rfc3986($v)
521 * Append a new query parameter after any existing query
524 * @param string $url the URL
525 * @prarm string $k the parameter name
526 * @param string $v value of the paramter
528 * @return string $url the new URL with added parameter
531 function appendQueryVar($url, $k, $v) {
532 $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
533 $url = substr($url, 0, -1);
534 if (strpos($url, '?') === false) {
535 return ($url . '?' . $k . '=' . $v);
537 return ($url . '&' . $k . '=' . $v);