diff options
author | Evan Prodromou <evan@prodromou.name> | 2008-06-18 01:26:38 -0400 |
---|---|---|
committer | Evan Prodromou <evan@prodromou.name> | 2008-06-18 01:26:38 -0400 |
commit | 252c4098c4910ec2fc20feb9f1c1f92ada129b04 (patch) | |
tree | 1fc6e898e763b3e9caaf05a03077c7a5a9ad6826 /actions/finishopenidlogin.php | |
parent | 111bab65b4a01fdd4fd9d1302a79692941021d50 (diff) |
finish openid
Added some code to make finishing the OpenID login work.
Changed the OID storage so that there's a "canonical" URL and a
display URL. This is because of i-names, which is annoying.
If the login succeeds, we try to find a local user associated with the
canonical URL. If they don't exist, we let the user either create a
new account, or login to an existing account and connect to it.
A totally unrelated change is that the DB engine now uses InnoDB.
darcs-hash:20080618052638-84dde-909e51dbd5b9eadadf18cd010868baa18ea2349a.gz
Diffstat (limited to 'actions/finishopenidlogin.php')
-rw-r--r-- | actions/finishopenidlogin.php | 462 |
1 files changed, 462 insertions, 0 deletions
diff --git a/actions/finishopenidlogin.php b/actions/finishopenidlogin.php new file mode 100644 index 000000000..e3db23dc3 --- /dev/null +++ b/actions/finishopenidlogin.php @@ -0,0 +1,462 @@ +<?php +/* + * Laconica - a distributed open-source microblogging tool + * Copyright (C) 2008, Controlez-Vous, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once(INSTALLDIR.'/lib/openid.php'); + +class FinishopenidloginAction extends Action { + + function handle($args) { + parent::handle($args); + if (common_logged_in()) { + common_user_error(_t('Already logged in.')); + } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if ($this->boolean('create')) { + $this->create_new_user(); + } else if ($this->boolean('connect')) { + $this->connect_user(); + } else { + $this->show_form(_t('Something weird happened.'), + $this->trimmed('newname')); + } + } else { + $this->try_login(); + } + } + + function show_form($error=NULL, $username=NULL) { + common_show_header(_t('OpenID Account Setup')); + if ($error) { + common_element('div', array('class' => 'error'), $error); + } else { + global $config; + common_element('div', 'instructions', + _t('This is the first time you\'ve logged into ') . + $config['site']['name'] . + _t(' so we must connect your OpenID to a local account. ' . + ' You can either create a new account, or connect with ' . + ' your existing account, if you have one.')); + } + common_element_start('form', array('method' => 'POST', + 'id' => 'account_connect', + 'action' => common_local_url('finishopenidlogin'))); + common_element('h2', NULL, + 'Create new account'); + common_element('p', NULL, + _t('Create a new user with this nickname.')); + common_input('newname', _t('New nickname'), + ($username) ? $username : '', + _t('1-64 lowercase letters or numbers, no punctuation or spaces')); + common_submit('create', _t('Create')); + common_element('h2', NULL, + 'Create new account'); + common_element('p', NULL, + _t('If you already have an account, login with your username and password '. + 'to connect it to your OpenID.')); + common_input('nickname', _t('Existing nickname')); + common_password('password', _t('Password')); + common_submit('connect', _t('Connect')); + common_element_end('form'); + common_show_footer(); + } + + function try_login() { + + $consumer = oid_consumer(); + + $response = $consumer->complete(common_local_url('finishopenidlogin')); + + if ($response->status == Auth_OpenID_CANCEL) { + $this->message(_t('OpenID authentication cancelled.')); + return; + } else if ($response->status == Auth_OpenID_FAILURE) { + // Authentication failed; display the error message. + $this->message(_t('OpenID authentication failed: ') . $response->message); + } else if ($response->status == Auth_OpenID_SUCCESS) { + // This means the authentication succeeded; extract the + // identity URL and Simple Registration data (if it was + // returned). + $display = $response->getDisplayIdentifier(); + $canonical = ($response->endpoint->canonicalID) ? + $response->endpoint->canonicalID : $response->getDisplayIdentifier(); + + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response); + + if ($sreg_resp) { + $sreg = $sreg_resp->contents(); + } + + $user = $this->get_user($canonical); + + if ($user) { + $this->update_user($user, $sreg); + common_set_user($user->nickname); + $this->go_home($user->nickname); + } else { + $this->save_values($display, $canonical, $sreg); + $this->show_form(NULL, $this->best_new_nickname($display, $sreg)); + } + } + } + + function message($msg) { + common_show_header(_t('OpenID Login')); + common_element('p', NULL, $msg); + common_show_footer(); + } + + function get_user($canonical) { + $user = NULL; + $oid = User_openid::staticGet('canonical', $canonical); + if ($oid) { + $user = User::staticGet('id', $oid->user_id); + } + return $user; + } + + function update_user($user, $sreg) { + + $profile = $user->getProfile(); + + $orig_profile = clone($profile); + + if ($sreg['fullname'] && strlen($sreg['fullname']) <= 255) { + $profile->fullname = $sreg['fullname']; + } + + if ($sreg['country']) { + if ($sreg['postcode']) { + # XXX: use postcode to get city and region + # XXX: also, store postcode somewhere -- it's valuable! + $profile->location = $sreg['postcode'] . ', ' . $sreg['country']; + } else { + $profile->location = $sreg['country']; + } + } + + # XXX save language if it's passed + # XXX save timezone if it's passed + + if (!$profile->update($orig_profile)) { + common_server_error(_t('Error saving the profile.')); + return; + } + + $orig_user = clone($user); + + if ($sreg['email'] && Validate::email($sreg['email'], true)) { + $user->email = $sreg['email']; + } + + if (!$user->update($orig_user)) { + common_server_error(_t('Error saving the user.')); + return; + } + } + + function save_values($display, $canonical, $sreg) { + common_ensure_session(); + $_SESSION['openid_display'] = $display; + $_SESSION['openid_canonical'] = $canonical; + $_SESSION['openid_sreg'] = $sreg; + } + + function get_saved_values($display, $canonical, $sreg) { + common_ensure_session(); + return array($_SESSION['openid_display'], + $_SESSION['openid_canonical'], + $_SESSION['openid_sreg']); + } + + function create_new_login() { + + $nickname = $this->trimmed('newname'); + + if (!Validate::string($nickname, array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { + $this->show_form(_t('Nickname must have only letters and numbers and no spaces.')); + return; + } + + if (User::staticGet('nickname', $nickname)) { + $this->show_form(_t('Nickname already in use. Try another one.')); + return; + } + + list($display, $canonical, $sreg) = $this->get_saved_values(); + + if (!$display || !$canonical) { + common_server_error(_t('Stored OpenID not found.')); + return; + } + + # Possible race condition... let's be paranoid + + $other = $this->get_user($canonical); + + if ($other) { + common_server_error(_t('Creating new account for OpenID that already has a user.')); + return; + } + + $profile = new Profile(); + + $profile->nickname = $nickname; + + if ($sreg['fullname'] && strlen($sreg['fullname']) <= 255) { + $profile->fullname = $sreg['fullname']; + } + + if ($sreg['country']) { + if ($sreg['postcode']) { + # XXX: use postcode to get city and region + # XXX: also, store postcode somewhere -- it's valuable! + $profile->location = $sreg['postcode'] . ', ' . $sreg['country']; + } else { + $profile->location = $sreg['country']; + } + } + + # XXX save language if it's passed + # XXX save timezone if it's passed + + $profile->created = DB_DataObject_Cast::dateTime(); # current time + + $id = $profile->insert(); + if (!$id) { + common_server_error(_t('Error saving the profile.')); + return; + } + + $user = new User(); + $user->id = $id; + $user->nickname = $nickname; + $user->uri = common_mint_tag('user:'.$id); + + if ($sreg['email'] && Validate::email($sreg['email'], true)) { + $user->email = $sreg['email']; + } + + $user->created = DB_DataObject_Cast::dateTime(); # current time + + $result = $user->insert(); + + if (!$result) { + # Try to clean up... + $profile->delete(); + } + + $oid = new User_openid(); + $oid->display = $display; + $oid->canonical = $canonical; + $oid->user_id = $id; + $oid->created = DB_DataObject_Cast::dateTime(); + + $result = $oid->insert(); + + if (!$result) { + # Try to clean up... + $user->delete(); + $profile->delete(); + } + + common_redirect(common_local_url('profilesettings')); + } + + function connect_user() { + + $nickname = $this->trimmed('nickname'); + $password = $this->trimmed('password'); + + if (!common_check_user($nickname, $password)) { + $this->show_form(_t('Invalid username or password.')); + return; + } + + # They're legit! + + $user = User::staticGet('nickname', $nickname); + + list($display, $canonical, $sreg) = $this->get_saved_values(); + + if (!$display || !$canonical) { + common_server_error(_t('Stored OpenID not found.')); + return; + } + + $oid = new User_openid(); + $oid->display = $display; + $oid->canonical = $canonical; + $oid->user_id = $user->id; + $oid->created = DB_DataObject_Cast::dateTime(); + + if (!$oid->insert()) { + common_server_error(_t('Error connecting OpenID.')); + return; + } + + $this->update_user($user, $sreg); + common_set_user($user->nickname); + $this->go_home($user->nickname); + } + + function go_home($nickname) { + $url = common_get_returnto(); + if ($url) { + # We don't have to return to it again + common_set_returnto(NULL); + } else { + $url = common_local_url('all', + array('nickname' => + $nickname)); + } + common_redirect($url); + } + + function best_new_nickname($display, $sreg) { + + # Try the passed-in nickname + + if ($sreg['nickname'] && $this->is_new_nickname($sreg['nickname'])) { + return $sreg['nickname']; + } + + # Try the full name + + if ($sreg['fullname']) { + $fullname = $this->nicknamize($sreg['fullname']); + if ($this->is_new_nickname($fullname)) { + return $fullname; + } + } + + # Try the URL + + $from_url = $this->openid_to_nickname($display); + + if ($from_url && $this->is_new_nickname($from_url)) { + return $from_url; + } + + # XXX: others? + + return NULL; + } + + function is_new_nickname($str) { + if (!Validate::string($str, array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { + return false; + } + if (User::staticGet('nickname', $str)) { + return false; + } + return true; + } + + function openid_to_nickname($openid) { + if (Auth_Yadis_identifierScheme($openid) == 'XRI') { + return $this->xri_to_nickname($openid); + } else { + return $this->url_to_nickname($openid); + } + } + + # We try to use an OpenID URL as a legal Laconica user name in this order + # 1. Plain hostname, like http://evanp.myopenid.com/ + # 2. One element in path, like http://profile.typekey.com/EvanProdromou/ + # or http://getopenid.com/evanprodromou + + function url_to_nickname($openid) { + static $bad = array('query', 'user', 'password', 'port', 'fragment'); + + $parts = parse_url($openid); + + # If any of these parts exist, this won't work + + foreach ($bad as $badpart) { + if (array_key_exists($badpart, $parts)) { + return NULL; + } + } + + # We just have host and/or path + + # If it's just a host... + if (array_key_exists('host', $parts) && + (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0)) + { + $hostparts = explode('.', $parts['host']); + + # Try to catch common idiom of nickname.service.tld + + if ((count($hostparts) > 2) && + (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au + (strcmp($hostparts[0], 'www') != 0)) + { + return $this->nicknamize($hostparts[0]); + } else { + # Do the whole hostname + return $this->nicknamize($parts['host']); + } + } else { + if (array_key_exists('path', $parts)) { + # Strip starting, ending slashes + $path = preg_replace('@/$@', '', $parts['path']); + $path = preg_replace('@^/@', '', $path); + if (strpos($path, '/') === false) { + return $this->nicknamize($path); + } + } + } + + return NULL; + } + + function xri_to_nickname($xri) { + $base = $this->xri_base($xri); + + if (!$base) { + return NULL; + } else { + # =evan.prodromou + # or @gratis*evan.prodromou + $parts = explode('*', substr($base, 1)); + return $this->nicknamize(array_pop($parts)); + } + } + + function xri_base($xri) { + if (substr($xri, 0, 6) == 'xri://') { + return substr($xri, 6); + } else { + return $xri; + } + } + + # Given a string, try to make it work as a nickname + + function nicknamize($str) { + $str = preg_replace('/\W/', '', $str); + return strtolower($str); + } +} |