3
* Server.php -- Server side of OpenID site
4
* Copyright 2006,2007 Internet Brands (http://www.internetbrands.com/)
5
* Copyright 2007,2008 Evan Prodromou <evan@prodromou.name>
7
* This program is free software; you can redistribute it and/or modify
8
* it under the terms of the GNU General Public License as published by
9
* the Free Software Foundation; either version 2 of the License, or
10
* (at your option) any later version.
12
* This program is distributed in the hope that it will be useful,
13
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
* GNU General Public License for more details.
17
* You should have received a copy of the GNU General Public License
18
* along with this program; if not, write to the Free Software
19
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
* @author Evan Prodromou <evan@prodromou.name>
22
* @addtogroup Extensions
25
if (!defined('MEDIAWIKI'))
28
require_once("Auth/OpenID/Server.php");
29
require_once("Auth/OpenID/Consumer.php");
31
# Special page for the server side of OpenID
32
# It has three major flavors:
33
# * no parameter is for external requests to validate a user.
34
# * 'Login' is we got a validation request but the
35
# user wasn't logged in. We show them a form (see OpenIDServerLoginForm)
36
# and they post the results, which go to OpenIDServerLogin
37
# * 'Trust' is when the user has logged in, but they haven't
38
# specified whether it's OK to let the requesting site trust them.
39
# If they haven't, we show them a form (see OpenIDServerTrustForm)
40
# and let them post results which go to OpenIDServerTrust.
42
# OpenID has its own modes; we only handle two of them ('check_setup' and
43
# 'check_immediate') and let the OpenID libraries handle the rest.
45
# Output may be just a redirect, or a form if we need info.
47
class SpecialOpenIDServer extends SpecialOpenID {
49
function SpecialOpenIDServer() {
50
SpecialPage::SpecialPage("OpenIDServer", '', false);
53
function execute($par) {
55
global $wgOut, $wgOpenIDClientOnly;
57
wfLoadExtensionMessages( 'OpenID' );
61
# No server functionality if this site is only a client
62
# Note: special page is un-registered if this flag is set,
63
# so it'd be unusual to get here.
65
if ($wgOpenIDClientOnly) {
66
$wgOut->showErrorPage('openiderror', 'openidclientonlytext');
70
$server =& $this->getServer();
74
list($request, $sreg) = $this->FetchValues();
75
$result = $this->serverLogin($request);
77
if (is_string($result)) {
78
$this->LoginForm($request, $result);
81
$this->Response($server, $result);
87
list($request, $sreg) = $this->FetchValues();
88
$result = $this->Trust($request, $sreg);
90
if (is_string($result)) {
91
$this->TrustForm($request, $sreg, $result);
94
$this->Response($server, $result);
101
wfDebug("OpenID: aborting in user validation because the parameter was empty\n");
102
$wgOut->showErrorPage('openiderror', 'openiderrortext');
105
$method = $_SERVER['REQUEST_METHOD'];
107
if ($method == 'GET') {
113
$request = $server->decodeRequest();
114
$sreg = $this->SregFromQuery($query);
120
if (!isset($request)) {
121
wfDebug("OpenID: aborting in user validation because the request was missing\n");
122
$wgOut->showErrorPage('openiderror', 'openiderrortext');
128
switch ($request->mode) {
129
case "checkid_setup":
130
$response = $this->Check($server, $request, $sreg, false);
132
case "checkid_immediate":
133
$response = $this->Check($server, $request, $sreg, true);
136
# For all the other parts, just let the libs do it
137
$response =& $server->handleRequest($request);
140
# OpenIDServerCheck returns NULL if some output (like a form)
143
if (isset($response)) {
144
# We're done; clear values
145
$this->ClearValues();
146
$this->Response($server, $response);
150
# Returns the full URL of the special page; we need to pass it around
154
$nt = Title::makeTitleSafe(NS_SPECIAL, 'OpenIDServer');
156
return $nt->getFullURL();
162
# Returns an Auth_OpenID_Server from the libraries. Utility.
164
function getServer() {
165
global $wgOpenIDServerStorePath,
166
$wgOpenIDServerStoreType;
168
$store = $this->getOpenIDStore($wgOpenIDServerStoreType,
170
array('path' => $wgOpenIDServerStorePath));
172
return new Auth_OpenID_Server($store, $this->serverUrl());
175
# Checks a validation request. $imm means don't run any UI.
176
# Fairly meticulous and step-by step, and uses assertions
177
# to point out assumptions at each step.
179
# XXX: this should probably be broken up into multiple functions for
182
function Check($server, $request, $sreg, $imm = true) {
184
global $wgUser, $wgOut;
186
assert(isset($wgUser) && isset($wgOut));
187
assert(isset($server));
188
assert(isset($request));
189
assert(isset($sreg));
190
assert(isset($imm) && is_bool($imm));
192
# Is the passed identity URL a user page?
194
$url = $request->identity;
196
assert(isset($url) && strlen($url) > 0);
198
$name = $this->UrlToUserName($url);
200
if (!isset($name) || strlen($name) == 0) {
201
wfDebug("OpenID: '$url' not a user page.\n");
202
return $request->answer(false, $this->serverUrl());
205
assert(isset($name) && strlen($name) > 0);
207
# Is there a logged in user?
209
if ($wgUser->getId() == 0) {
210
wfDebug("OpenID: User not logged in.\n");
212
return $request->answer(false, $this->serverUrl());
214
# Bank these for later
215
$this->SaveValues($request, $sreg);
216
$this->LoginForm($request);
221
assert($wgUser->getId() != 0);
223
# Is the user page for the logged-in user?
225
$user = User::newFromName($name);
228
$user->getId() != $wgUser->getId()) {
229
wfDebug("OpenID: User from url not logged in user.\n");
230
return $request->answer(false, $this->serverUrl());
233
assert(isset($user) && $user->getId() == $wgUser->getId() && $user->getId() != 0);
235
# Is the user an OpenID user?
237
$openid = $this->getUserUrl($user);
239
if (isset($openid) && strlen($openid) > 0) {
240
wfDebug("OpenID: Not one of our users; logs in with OpenID.\n");
241
return $request->answer(false, $this->serverUrl());
244
assert(is_array($sreg));
246
# Does the request require sreg fields that the user has not specified?
248
if (array_key_exists('required', $sreg)) {
250
foreach ($sreg['required'] as $reqfield) {
251
if (is_null($this->GetUserField($user, $reqfield))) {
257
wfDebug("OpenID: Consumer demands info we don't have.\n");
258
return $request->answer(false, $this->serverUrl());
264
$trust_root = $request->trust_root;
266
assert(isset($trust_root) && is_string($trust_root) && strlen($trust_root) > 0);
268
$trust = $this->GetUserTrust($user, $trust_root);
270
# Is there a trust record?
272
if (is_null($trust)) {
273
wfDebug("OpenID: No trust record.\n");
275
return $request->answer(false, $this->serverUrl());
277
# Bank these for later
278
$this->SaveValues($request, $sreg);
279
$this->TrustForm($request, $sreg);
284
assert(!is_null($trust));
286
# Is the trust record _not_ to allow trust?
289
if ($trust === false) {
290
wfDebug("OpenID: User specified not to allow trust.\n");
291
return $request->answer(false, $this->serverUrl());
294
assert(isset($trust) && is_array($trust));
296
# Does the request require sreg fields that the user has
297
# not allowed us to pass, or has not specified?
299
if (array_key_exists('required', $sreg)) {
301
foreach ($sreg['required'] as $reqfield) {
302
if (!in_array($reqfield, $trust) ||
303
is_null($this->GetUserField($user, $reqfield))) {
309
wfDebug("OpenID: Consumer demands info user doesn't want shared.\n");
310
return $request->answer(false, $this->serverUrl());
314
# assert(all required sreg fields are in $trust)
316
# XXX: run a hook here to check
320
$response_fields = array_intersect(array_unique(array_merge($sreg['required'], $sreg['optional'])),
323
$response = $request->answer(true);
325
assert(isset($response));
327
foreach ($response_fields as $field) {
328
$value = $this->GetUserField($user, $field);
329
if (!is_null($value)) {
330
$response->addField('sreg', $field, $value);
337
# Get the user's configured trust value for a particular trust root.
338
# Returns one of three values:
339
# * NULL -> no stored trust preferences
340
# * false -> stored trust preference is not to trust
341
# * array -> possibly empty array of allowed profile fields; trust is OK
343
function GetUserTrust($user, $trust_root) {
344
static $allFields = array('nickname', 'fullname', 'email', 'language');
345
global $wgOpenIDServerForceAllowTrust;
347
foreach ($wgOpenIDServerForceAllowTrust as $force) {
348
if (preg_match($force, $trust_root)) {
353
$trust_array = $this->GetUserTrustArray($user);
355
if (array_key_exists($trust_root, $trust_array)) {
356
return $trust_array[$trust_root];
358
return null; # Unspecified trust
362
function SetUserTrust(&$user, $trust_root, $value) {
364
$trust_array = $this->GetUserTrustArray($user);
366
if (is_null($value)) {
367
if (array_key_exists($trust_root, $trust_array)) {
368
unset($trust_array[$trust_root]);
371
$trust_array[$trust_root] = $value;
374
$this->SetUserTrustArray($user, $trust_array);
377
function GetUserTrustArray($user) {
378
$trust_array = array();
379
$trust_str = $user->getOption('openid_trust');
380
if (strlen($trust_str) > 0) {
381
$trust_records = explode("\x1E", $trust_str);
382
foreach ($trust_records as $record) {
383
$fields = explode("\x1F", $record);
384
$trust_root = array_shift($fields);
385
if (count($fields) == 1 && strcmp($fields[0], 'no') == 0) {
386
$trust_array[$trust_root] = false;
388
$fields = array_map('trim', $fields);
389
$fields = array_filter($fields, array($this, 'ValidField'));
390
$trust_array[$trust_root] = $fields;
397
function SetUserTrustArray(&$user, $arr) {
398
$trust_records = array();
399
foreach ($arr as $root => $value) {
400
if ($value === false) {
401
$record = implode("\x1F", array($root, 'no'));
402
} else if (is_array($value)) {
403
if (count($value) == 0) {
406
$value = array_map('trim', $value);
407
$value = array_filter($value, array($this, 'ValidField'));
408
$record = implode("\x1F", array_merge(array($root), $value));
413
$trust_records[] = $record;
415
$trust_str = implode("\x1E", $trust_records);
416
$user->setOption('openid_trust', $trust_str);
419
function ValidField($name) {
420
# XXX: eventually add timezone
421
static $fields = array('nickname', 'email', 'fullname', 'language');
422
return in_array($name, $fields);
425
function SregFromQuery($query) {
426
$sreg = array('required' => array(), 'optional' => array(),
427
'policy_url' => NULL);
428
if (array_key_exists('openid.sreg.required', $query)) {
429
$sreg['required'] = explode(',', $query['openid.sreg.required']);
431
if (array_key_exists('openid.sreg.optional', $query)) {
432
$sreg['optional'] = explode(',', $query['openid.sreg.optional']);
434
if (array_key_exists('openid.sreg.policy_url', $query)) {
435
$sreg['policy_url'] = $query['openid.sreg.policy_url'];
440
function SetUserField(&$user, $field, $value) {
443
$user->setRealName($value);
447
# FIXME: deal with validation
448
$user->setEmail($value);
452
$user->setOption('language', $value);
460
function GetUserField($user, $field) {
463
return $user->getName();
466
return $user->getRealName();
469
return $user->getEmail();
472
return $user->getOption('language');
479
function Response(&$server, &$response) {
482
assert(!is_null($server));
483
assert(!is_null($response));
487
$wr =& $server->encodeResponse($response);
489
assert(!is_null($wr));
491
header("Status: " . $wr->code);
493
foreach ($wr->headers as $k => $v) {
502
function LoginForm($request, $msg = null) {
504
global $wgOut, $wgUser;
506
$url = $request->identity;
507
$name = $this->UrlToUserName($url);
508
$trust_root = $request->trust_root;
510
$instructions = wfMsg('openidserverlogininstructions', $url, $name, $trust_root);
512
$username = wfMsg('yourname');
513
$password = wfMsg('yourpassword');
515
$cancel = wfMsg('cancel');
518
$wgOut->addHTML("<p class='error'>{$msg}</p>");
521
$sk = $wgUser->getSkin();
523
$wgOut->addHTML("<p>{$instructions}</p>" .
524
'<form action="' . $sk->makeSpecialUrl('OpenIDServer/Login') . '" method="POST">' .
526
"<tr><td><label for='username'>{$username}</label></td>" .
527
' <td><span id="username">' . htmlspecialchars($name) . '</span></td></tr>' .
528
"<tr><td><label for='password'>{$password}</label></td>" .
529
' <td><input type="password" name="wpPassword" size="32" value="" /></td></tr>' .
530
"<tr><td colspan='2'><input type='submit' name='wpOK' value='{$ok}' /> <input type='submit' name='wpCancel' value='{$cancel}' /></td></tr>" .
535
function SaveValues($request, $sreg) {
536
global $wgSessionStarted, $wgUser;
538
if (!$wgSessionStarted) {
539
$wgUser->SetupSession();
542
$_SESSION['openid_server_request'] = $request;
543
$_SESSION['openid_server_sreg'] = $sreg;
548
function FetchValues() {
549
return array($_SESSION['openid_server_request'], $_SESSION['openid_server_sreg']);
552
function ClearValues() {
553
unset($_SESSION['openid_server_request']);
554
unset($_SESSION['openid_server_sreg']);
558
function serverLogin($request) {
560
global $wgRequest, $wgUser;
562
assert(isset($request));
564
assert(isset($wgRequest));
566
if ($wgRequest->getCheck('wpCancel')) {
567
return $request->answer(false);
570
$password = $wgRequest->getText('wpPassword');
572
if (!isset($password) || strlen($password) == 0) {
573
return wfMsg('wrongpasswordempty');
576
assert (isset($password) && strlen($password) > 0);
578
$url = $request->identity;
580
assert(isset($url) && is_string($url) && strlen($url) > 0);
582
$name = $this->UrlToUserName($url);
584
assert(isset($name) && is_string($name) && strlen($name) > 0);
586
$user = User::newFromName($name);
588
assert(isset($user));
590
if (!$user->checkPassword($password)) {
591
return wfMsg('wrongpassword');
593
$id = $user->getId();
595
$wgUser->SetupSession();
596
$wgUser->SetCookies();
597
wfRunHooks('UserLoginComplete', array(&$wgUser));
602
function TrustForm($request, $sreg, $msg = NULL) {
604
global $wgOut, $wgUser;
606
$url = $request->identity;
607
$name = $this->UrlToUserName($url);
608
$trust_root = $request->trust_root;
610
$instructions = wfMsg('openidtrustinstructions', $trust_root);
611
$allow = wfMsg('openidallowtrust', $trust_root);
613
if (is_null($sreg['policy_url'])) {
614
$policy = wfMsg('openidnopolicy');
616
$policy = wfMsg('openidpolicy', $sreg['policy_url']);
620
$wgOut->addHTML("<p class='error'>{$msg}</p>");
624
$cancel = wfMsg('cancel');
626
$sk = $wgUser->getSkin();
628
$wgOut->addHTML("<p>{$instructions}</p>" .
629
'<form action="' . $sk->makeSpecialUrl('OpenIDServer/Trust') . '" method="POST">' .
630
'<input name="wpAllowTrust" type="checkbox" value="on" checked="checked" id="wpAllowTrust">' .
631
'<label for="wpAllowTrust">' . $allow . '</label><br />');
633
$fields = array_filter(array_unique(array_merge($sreg['optional'], $sreg['required'])),
634
array($this, 'ValidField'));
636
if (count($fields) > 0) {
637
$wgOut->addHTML('<table>');
638
foreach ($fields as $field) {
639
$wgOut->addHTML("<tr>");
640
$wgOut->addHTML("<th><label for='wpAllow{$field}'>");
641
$wgOut->addHTML(wfMsg("openid$field"));
642
$wgOut->addHTML("</label></th>");
643
$value = $this->GetUserField($wgUser, $field);
644
$wgOut->addHTML("</td>");
645
$wgOut->addHTML("<td> " . ((is_null($value)) ? '' : $value) . "</td>");
646
$wgOut->addHTML("<td>" . ((in_array($field, $sreg['required'])) ? wfMsg('openidrequired') : wfMsg('openidoptional')) . "</td>");
647
$wgOut->addHTML("<td><input name='wpAllow{$field}' id='wpAllow{$field}' type='checkbox'");
648
if (!is_null($value)) {
649
$wgOut->addHTML(" value='on' checked='checked' />");
651
$wgOut->addHTML(" disabled='disabled' />");
653
$wgOut->addHTML("</tr>");
655
$wgOut->addHTML('</table>');
657
$wgOut->addHTML("<input type='submit' name='wpOK' value='{$ok}' /> <input type='submit' name='wpCancel' value='{$cancel}' /></form>");
661
function Trust($request, $sreg) {
662
global $wgRequest, $wgUser;
664
assert(isset($request));
665
assert(isset($sreg));
666
assert(isset($wgRequest));
668
if ($wgRequest->getCheck('wpCancel')) {
669
return $request->answer(false);
672
$trust_root = $request->trust_root;
674
assert(isset($trust_root) && strlen($trust_root) > 0);
676
# If they don't want us to allow trust, save that.
678
if (!$wgRequest->getCheck('wpAllowTrust')) {
680
$this->SetUserTrust($wgUser, $trust_root, false);
682
$wgUser->saveSettings();
685
$fields = array_filter(array_unique(array_merge($sreg['optional'], $sreg['required'])),
686
array($this, 'ValidField'));
690
foreach ($fields as $field) {
691
if ($wgRequest->getCheck('wpAllow' . $field)) {
696
$this->SetUserTrust($wgUser, $trust_root, $allow);
698
$wgUser->saveSettings();
703
# Converts an URL to a user name, if possible
705
function UrlToUserName($url) {
707
global $wgArticlePath, $wgServer;
709
# URL must be a string
711
if (!isset($url) || !is_string($url) || strlen($url) == 0) {
715
# it must start with our server, case doesn't matter
717
if (strpos(strtolower($url), strtolower($wgServer)) !== 0) {
721
$parts = parse_url($url);
723
$relative = $parts['path'];
724
if (!is_null($parts['query']) && strlen($parts['query']) > 0) {
725
$relative .= '?' . $parts['query'];
728
# Use regexps to extract user name
730
$pattern = str_replace('$1', '(.*)', $wgArticlePath);
731
$pattern = str_replace('?', '\?', $pattern);
732
# Can't have a pound-sign in the relative, since that's for fragments
733
if (!preg_match("#$pattern#", $relative, $matches)) {
736
$titletext = urldecode($matches[1]);
737
$nt = Title::newFromText($titletext);
738
if (is_null($nt) || $nt->getNamespace() != NS_USER) {
741
return $nt->getText();
746
function serverUrl() {
747
return $this->fullURL('OpenIDServer');