blob: 756e7259f110fc6341b91242f89bcf60814eefe3 [file] [log] [blame]
Nico Huberee52fbc2023-06-24 11:52:57 +00001<?php
2/*
3 * SimpleID
4 *
5 * Copyright (C) Kelvin Mo 2007-8
6 *
7 * Includes code Drupal OpenID module (http://drupal.org/project/openid)
8 * Rowan Kerr <rowan@standardinteractive.com>
9 * James Walker <james@bryght.com>
10 *
11 * Copyright (C) Rowan Kerr and James Walker
12 *
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public
15 * License as published by the Free Software Foundation; either
16 * version 2 of the License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public
24 * License along with this program; if not, write to the Free
25 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
26 *
27 * $Id$
28 */
29
30/**
31 * Main SimpleID file.
32 *
33 * @package simpleid
34 * @filesource
35 */
36
37include_once "version.inc.php";
38include_once "locale.inc.php";
39
40// Check if the configuration file has been defined
41if (file_exists('conf/config.php')) {
42 include_once 'conf/config.php';
43} elseif (file_exists('config.php')) {
44 include_once 'config.php';
45} else {
46 die(t('No configuration file found. See the <a href="!url">manual</a> for instructions on how to set up a configuration file.', array('!url' => 'http://simpleid.org/docs/1/installing/')));
47}
48
49include_once "config.default.php";
50include_once "log.inc.php";
51include_once "common.inc.php";
52include_once "simpleweb.inc.php";
53include_once "openid.inc.php";
54include_once "discovery.inc.php";
55include_once "user.inc.php";
56include_once "cache.inc.php";
57include_once SIMPLEID_STORE . ".store.php";
58include_once "page.inc.php";
59include_once "lib/xtemplate.class.php";
60
61define('CACHE_DIR', SIMPLEID_CACHE_DIR);
62
63/**
64 */
65define('CHECKID_OK', 127);
66define('CHECKID_RETURN_TO_SUSPECT', 3);
67define('CHECKID_APPROVAL_REQUIRED', 2);
68define('CHECKID_LOGIN_REQUIRED', -1);
69define('CHECKID_IDENTITIES_NOT_MATCHING', -2);
70define('CHECKID_IDENTITY_NOT_EXIST', -3);
71define('CHECKID_PROTOCOL_ERROR', -127);
72
73define('ASSOCIATION_PRIVATE', 2);
74define('ASSOCIATION_SHARED', 1);
75
76
77/**
78 * This variable holds the version of the OpenID specification associated with
79 * the current OpenID request. This can be either {@link OPENID_VERSION_1_1}
80 * or {@link OPENID_VERSION_2}.
81 *
82 * @global float $version
83 */
84$version = OPENID_VERSION_1_1;
85
86/**
87 * This variable holds an instance of the XTemplate engine.
88 *
89 * @global object $xtpl
90 */
91$xtpl = NULL;
92
93/**
94 * This variable holds the combined $_GET and $_POST superglobal arrays.
95 * This is then passed through {@link fix_http_request()}.
96 *
97 * @global array $GETPOST
98 */
99$GETPOST = array();
100
101simpleid_start();
102
103/**
104 * Entry point for SimpleID.
105 *
106 * @see user_init()
107 */
108function simpleid_start() {
109 global $xtpl, $GETPOST;
110
111 locale_init(SIMPLEID_LOCALE);
112
113 $xtpl = new XTemplate('html/template.xtpl');
114 $xtpl->assign('version', SIMPLEID_VERSION);
115 $xtpl->assign('base_path', get_base_path());
116 $xtpl->assign('footer_doc', t('Documentation'));
117 $xtpl->assign('footer_support', t('Support'));
118
119 if (!is_dir(SIMPLEID_IDENTITIES_DIR)) {
120 log_fatal('Identities directory not found.');
121 indirect_fatal_error(t('Identities directory not found. See the <a href="!url">manual</a> for instructions on how to set up SimpleID.', array('!url' => 'http://simpleid.org/documentation/getting-started')));
122 }
123
124 if (!is_dir(SIMPLEID_CACHE_DIR) || !is_writeable(SIMPLEID_CACHE_DIR)) {
125 log_fatal('Cache directory not found or not writeable.');
126 indirect_fatal_error(t('Cache directory not found or not writeable. See the <a href="!url">manual</a> for instructions on how to set up SimpleID.', array('!url' => 'http://simpleid.org/docs/1/installing/')));
127 }
128
129
130 if (!is_dir(SIMPLEID_STORE_DIR) || !is_writeable(SIMPLEID_STORE_DIR)) {
131 log_fatal('Store directory not found or not writeable.');
132 indirect_fatal_error(t('Store directory not found or not writeable. See the <a href="!url">manual</a> for instructions on how to set up SimpleID.', array('!url' => 'http://simpleid.org/docs/1/installing/')));
133 }
134
135 if ((@ini_get('register_globals') === 1) || (@ini_get('register_globals') === '1') || (strtolower(@ini_get('register_globals')) == 'on')) {
136 log_fatal('register_globals is enabled in PHP configuration.');
137 indirect_fatal_error(t('register_globals is enabled in PHP configuration, which is not supported by SimpleID. See the <a href="!url">manual</a> for further information.', array('!url' => 'http://simpleid.org/docs/1/system-requirements/')));
138 }
139
140 if (!bignum_loaded()) {
141 log_fatal('gmp/bcmath PHP extension not loaded.');
142 indirect_fatal_error(t('One or more required PHP extensions (%extension) is not loaded. See the <a href="!url">manual</a> for further information on system requirements.', array('%extension' => 'gmp/bcmath', '!url' => 'http://simpleid.org/docs/1/system-requirements/')));
143 }
144 if (!function_exists('preg_match')) {
145 log_fatal('pcre PHP extension not loaded.');
146 indirect_fatal_error(t('One or more required PHP extensions (%extension) is not loaded. See the <a href="!url">manual</a> for further information on system requirements.', array('%extension' => 'pcre', '!url' => 'http://simpleid.org/docs/1/system-requirements/')));
147 }
148 if (!function_exists('session_start')) {
149 log_fatal('session PHP extension not loaded.');
150 indirect_fatal_error(t('One or more required PHP extensions (%extension) is not loaded. See the <a href="!url">manual</a> for further information on system requirements.', array('%extension' => 'session', '!url' => 'http://simpleid.org/docs/1/system-requirements/')));
151 }
152 if (!function_exists('xml_parser_create_ns')) {
153 log_fatal('xml PHP extension not loaded.');
154 indirect_fatal_error(t('One or more required PHP extensions (%extension) is not loaded. See the <a href="!url">manual</a> for further information on system requirements.', array('%extension' => 'xml', '!url' => 'http://simpleid.org/docs/1/system-requirements/')));
155 }
156 if (!function_exists('hash')) {
157 log_fatal('hash PHP extension not loaded.');
158 indirect_fatal_error(t('One or more required PHP extensions (%extension) is not loaded. See the <a href="!url">manual</a> for further information on system requirements.', array('%extension' => 'hash', '!url' => 'http://simpleid.org/docs/1/system-requirements/')));
159 }
160 if (is_numeric(@ini_get('suhosin.get.max_value_length')) && (@ini_get('suhosin.get.max_value_length') < 1024)) {
161 log_fatal('suhosin.get.max_value_length < 1024');
162 indirect_fatal_error(t('suhosin.get.max_value_length is less than 1024, which will lead to problems. See the <a href="!url">manual</a> for further information on system requirements.', array('!url' => 'http://simpleid.org/docs/1/system-requirements/')));
163 }
164
165 fix_http_request();
166 $GETPOST = array_merge($_GET, $_POST);
167 openid_parse_request($GETPOST);
168
169 $q = (isset($GETPOST['q'])) ? $GETPOST['q'] : '';
170
171 $uaid = get_user_agent_id();
172 extension_init();
173 user_init($q);
174 log_info('Session opened for "' . $q . '" from ' . $uaid . ' [' . $_SERVER['REMOTE_ADDR'] . ', ' . gethostbyaddr($_SERVER['REMOTE_ADDR']) . ']');
175
176 // Clean stale assocations
177 cache_expire(array(
178 'association' => SIMPLEID_ASSOC_EXPIRES_IN,
179 'stateless' => 300)
180 );
181
182 simpleid_route($q);
183}
184
185/**
186 * Dispatches to the correct SimpleID function based on the request path. The
187 * request path usually comes from the q parameter in the query string (which may
188 * be inserted by mod_rewrite), but can come from other functions as well.
189 *
190 * @param string $q the request path
191 */
192function simpleid_route($q) {
193 $routes = array(
194 'continue' => 'simpleid_continue',
195 'login' => 'user_login',
196 'logout' => 'user_logout',
197 'my/dashboard' => 'page_dashboard',
198 'my/sites' => 'page_sites',
199 'my/profile' => 'page_profile',
200 'otp' => 'user_otp_page',
201 'openid/consent' => 'simpleid_openid_consent',
202 'ppid/(.*)' => 'user_ppid_page',
203 'user' => 'user_public_page',
204 'user/(.+)' => 'user_public_page',
205 'xrds/(.*)' => 'user_xrds',
206 'xrds' => 'simpleid_xrds',
207 );
208 $routes = array_merge($routes, extension_invoke_all('routes'), array('.*' => 'simpleid_index'));
209
210 simpleweb_run($routes, $q);
211}
212
213/**
214 * The default route, called when the q parameter is missing or is invalid.
215 *
216 * This function performs the following:
217 *
218 * - If openid.mode is present, then the request is an OpenID request. This
219 * is passed to {@link simpleid_process_openid()}
220 * - If the Accept HTTP header contains the expression application/xrds+xml, then
221 * the request is a YADIS discovery request for SimpleID as an OpenID provider. Thi
222 * is passed to {@link simpleid_xrds()}
223 * - Otherwise, the dashboard or the login page is displayed to the user as
224 * appropriate
225 *
226 */
227function simpleid_index() {
228 global $GETPOST;
229
230 log_debug('simpleid_index');
231
232 $content_type = negotiate_content_type(array('text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml'));
233
234 header('Vary: Accept');
235 if (isset($GETPOST['openid.mode'])) {
236 simpleid_process_openid($GETPOST);
237 return;
238 } elseif ($content_type == 'application/xrds+xml') {
239 simpleid_xrds();
240 } else {
241 // Point to SimpleID's XRDS document
242 header('X-XRDS-Location: ' . simpleid_url('xrds'));
243 page_dashboard();
244 }
245}
246
247
248/**
249 * Process an OpenID request.
250 *
251 * This function determines the version of the OpenID specification that is
252 * relevant to this request, checks openid.mode and passes the
253 * request on to the function required to process the request.
254 *
255 * The OpenID request expressed as an array contain key-value pairs corresponding
256 * to the HTTP request. This is usually contained in the <code>$_REQUEST</code>
257 * variable.
258 *
259 * @param array $request the OpenID request
260 */
261function simpleid_process_openid($request) {
262 global $version;
263
264 $version = openid_get_version($request);
265
266 switch ($request['openid.mode']) {
267 case 'associate':
268 simpleid_associate($request);
269 return;
270 case 'checkid_immediate':
271 case 'checkid_setup':
272 check_https('redirect', true, simpleid_url('continue', 's=' . rawurlencode(pickle($request)), false, 'https'));
273
274 return simpleid_checkid($request);
275 case 'check_authentication':
276 simpleid_check_authentication($request);
277 break;
278 default:
279 if (isset($request['openid.return_to'])) {
280 // Indirect communication - send error via indirect communication.
281 header_response_code('400 Bad Request');
282 set_message(t('Invalid OpenID message.'));
283 page_dashboard();
284 } else {
285 // Direct communication
286 openid_direct_error('Invalid OpenID message.');
287 }
288 }
289}
290
291/**
292 * Processes an association request from a relying party.
293 *
294 * An association request has an openid.mode value of
295 * associate. This function checks whether the association request
296 * is valid, and if so, creates an association and sends the response to
297 * the relying party.
298 *
299 * @see _simpleid_create_association()
300 * @param array $request the OpenID request
301 * @link http://openid.net/specs/openid-authentication-1_1.html#mode_associate, http://openid.net/specs/openid-authentication-2_0.html#associations
302 *
303 */
304function simpleid_associate($request) {
305 global $version;
306
307 log_info('OpenID association request: ' . log_array($request));
308
309 $assoc_types = openid_association_types();
310 $session_types = openid_session_types(is_https(), $version);
311
312 // Common Request Parameters [8.1.1]
313 if (($version == OPENID_VERSION_1_1) && !isset($request['openid.session_type'])) $request['openid.session_type'] = '';
314 $assoc_type = $request['openid.assoc_type'];
315 $session_type = $request['openid.session_type'];
316
317 // Diffie-Hellman Request Parameters [8.1.2]
318 $dh_modulus = (isset($request['openid.dh_modulus'])) ? $request['openid.dh_modulus'] : NULL;
319 $dh_gen = (isset($request['openid.dh_gen'])) ? $request['openid.dh_gen'] : NULL;
320 $dh_consumer_public = $request['openid.dh_consumer_public'];
321
322 if (!isset($request['openid.session_type']) || !isset($request['openid.assoc_type'])) {
323 log_error('Association failed: openid.session_type or openid.assoc_type not set');
324 openid_direct_error('openid.session_type or openid.assoc_type not set');
325 return;
326 }
327
328 // Check if the assoc_type is supported
329 if (!array_key_exists($assoc_type, $assoc_types)) {
330 $error = array(
331 'error_code' => 'unsupported-type',
332 'session_type' => 'DH-SHA1',
333 'assoc_type' => 'HMAC-SHA1'
334 );
335 log_error('Association failed: The association type is not supported by SimpleID.');
336 openid_direct_error('The association type is not supported by SimpleID.', $error, $version);
337 return;
338 }
339 // Check if the session_type is supported
340 if (!array_key_exists($session_type, $session_types)) {
341 $error = array(
342 'error_code' => 'unsupported-type',
343 'session_type' => 'DH-SHA1',
344 'assoc_type' => 'HMAC-SHA1'
345 );
346 log_error('Association failed: The session type is not supported by SimpleID.');
347 openid_direct_error('The session type is not supported by SimpleID.', $error, $version);
348 return;
349 }
350
351 if ($session_type == 'DH-SHA1' || $session_type == 'DH-SHA256') {
352 if (!$dh_consumer_public) {
353 log_error('Association failed: openid.dh_consumer_public not set');
354 openid_direct_error('openid.dh_consumer_public not set');
355 return;
356 }
357 }
358
359 $response = _simpleid_create_association(ASSOCIATION_SHARED, $assoc_type, $session_type, $dh_modulus, $dh_gen, $dh_consumer_public);
360
361 openid_direct_response(openid_direct_message($response, $version));
362}
363
364
365/**
366 * Creates an association.
367 *
368 * This function calls {@link openid_dh_server_assoc()} where required, to
369 * generate the cryptographic values required for an association response.
370 *
371 * @param int $mode either ASSOCIATION_SHARED or ASSOCIATION_PRIVATE
372 * @param string $assoc_type a valid OpenID association type
373 * @param string $session_type a valid OpenID session type
374 * @param string $dh_modulus for Diffie-Hellman key exchange, the modulus encoded in Base64
375 * @param string $dh_gen for Diffie-Hellman key exchange, g encoded in Base64
376 * @param string $dh_consumer_public for Diffie-Hellman key exchange, the public key of the relying party encoded in Base64
377 * @return mixed if $mode is ASSOCIATION_SHARED, an OpenID response
378 * to the association request, if $mode is ASSOCIATION_PRIVATE, the
379 * association data for storage.
380 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor14, http://openid.net/specs/openid-authentication-2_0.html#anchor20
381 */
382function _simpleid_create_association($mode = ASSOCIATION_SHARED, $assoc_type = 'HMAC-SHA1', $session_type = 'no-encryption', $dh_modulus = NULL, $dh_gen = NULL, $dh_consumer_public = NULL) {
383 global $version;
384
385 $assoc_types = openid_association_types();
386 $session_types = openid_session_types(is_https(), $version);
387
388 $mac_size = $assoc_types[$assoc_type]['mac_size'];
389 $hmac_func = $assoc_types[$assoc_type]['hmac_func'];
390
391 $assoc_handle = random_id();
392 $expires_in = SIMPLEID_ASSOC_EXPIRES_IN;
393
394 $secret = random_bytes($mac_size);
395
396 $response = array(
397 'assoc_handle' => $assoc_handle,
398 'assoc_type' => $assoc_type,
399 'expires_in' => $expires_in
400 );
401
402 // If $session_type is '', then it must be using OpenID 1.1 (blank parameter
403 // is not allowed for OpenID 2.0. For OpenID 1.1 blank requests, we don't
404 // put a session_type in the response.
405 if ($session_type != '') $response['session_type'] = $session_type;
406
407 if (($session_type == 'no-encryption') || ($session_type == '')) {
408 $mac_key = base64_encode(call_user_func($hmac_func, $secret, $response['assoc_handle']));
409 $response['mac_key'] = $mac_key;
410 } elseif ($session_type == 'DH-SHA1' || $session_type == 'DH-SHA256') {
411 $hash_func = $session_types[$session_type]['hash_func'];
412
413 $dh_assoc = openid_dh_server_assoc($secret, $dh_consumer_public, $dh_modulus, $dh_gen, $hash_func);
414 $mac_key = base64_encode($secret);
415 $response['dh_server_public'] = $dh_assoc['dh_server_public'];
416 $response['enc_mac_key'] = $dh_assoc['enc_mac_key'];
417 }
418
419 $association = array('assoc_handle' => $assoc_handle, 'assoc_type' => $assoc_type, 'mac_key' => $mac_key, 'created' => time());
420 if ($mode == ASSOCIATION_PRIVATE) $association['private'] = 1;
421 cache_set('association', $assoc_handle, $association);
422
423 if ($mode == ASSOCIATION_SHARED) {
424 log_info('Created association: ' . log_array($response));
425 log_debug('***** MAC key: ' . $association['mac_key']);
426 return $response;
427 } else {
428 log_info('Created association: private; ' . log_array($association, array('assoc_handle', 'assoc_type')));
429 log_debug('***** MAC key: ' . $association['mac_key']);
430 return $association;
431 }
432}
433
434
435/**
436 * Processes an authentication request from a relying party.
437 *
438 * An authentication request has an openid.mode value of
439 * checkid_setup or checkid_immediate.
440 *
441 * If the authentication request is a standard OpenID request about an identity
442 * (i.e. contains the key openid.identity), this function calls
443 * {@link simpleid_checkid_identity()} to see whether the user logged on into SimpleID
444 * matches the identity supplied in the OpenID request.
445 *
446 * If the authentication request is not about an identity, this function calls
447 * the {@link hook_checkid() checkid hook} of the loaded extensions.
448 *
449 * Depending on the OpenID version, this function will supply an appropriate
450 * assertion.
451 *
452 * @param array $request the OpenID request
453 *
454 */
455function simpleid_checkid($request) {
456 global $version;
457
458 $immediate = ($request['openid.mode'] == 'checkid_immediate');
459
460 log_info('OpenID authentication request: ' . (($immediate) ? 'immediate' : 'setup') . '; '. log_array($request));
461
462 // Check for protocol correctness
463 if ($version == OPENID_VERSION_1_1) {
464 if (!isset($request['openid.return_to'])) {
465 log_error('Protocol Error: openid.return_to not set.');
466 indirect_fatal_error(t('Protocol Error: openid.return_to not set.'));
467 return;
468 }
469 if (!isset($request['openid.identity'])) {
470 log_error('Protocol Error: openid.identity not set.');
471 indirect_fatal_error(t('Protocol Error: openid.identity not set.'));
472 return;
473 }
474 }
475
476 if ($version >= OPENID_VERSION_2) {
477 if (isset($request['openid.identity']) && !isset($request['openid.claimed_id'])) {
478 log_error('Protocol Error: openid.identity set, but not openid.claimed_id.');
479 indirect_fatal_error(t('Protocol Error: openid.identity set, but not openid.claimed_id.'));
480 return;
481 }
482
483 if (!isset($request['openid.realm']) && !isset($request['openid.return_to'])) {
484 log_error('Protocol Error: openid.return_to not set when openid.realm is not set.');
485 indirect_fatal_error(t('Protocol Error: openid.return_to not set when openid.realm is not set.'));
486 return;
487 }
488 }
489
490 if (isset($request['openid.return_to'])) {
491 $realm = openid_get_realm($request, $version);
492
493 if (!openid_url_matches_realm($request['openid.return_to'], $realm)) {
494 log_error('Protocol Error: openid.return_to does not match realm.');
495 openid_indirect_error($request['openid.return_to'], 'Protocol Error: openid.return_to does not match realm.');
496 return;
497 }
498 }
499
500 if (isset($request['openid.identity'])) {
501 // Standard request
502 log_debug('openid.identity found, use simpleid_checkid_identity');
503 $result = simpleid_checkid_identity($request, $immediate);
504 } else {
505 log_debug('openid.identity not found, trying extensions');
506 // Extension request
507 $results = extension_invoke_all('checkid', $request, $immediate);
508
509 // Filter out nulls
510 $results = array_merge(array_diff($results, array(NULL)));
511
512 // If there are still results, it is the lowest value, otherwise, it is CHECKID_PROTOCOL_ERROR
513 $result = ($results) ? min($results) : CHECKID_PROTOCOL_ERROR;
514 }
515
516 switch ($result) {
517 case CHECKID_APPROVAL_REQUIRED:
518 log_info('CHECKID_APPROVAL_REQUIRED');
519 if ($immediate) {
520 $response = simpleid_checkid_approval_required($request);
521 simpleid_assertion_response($response, $request['openid.return_to']);
522 } else {
523 $response = simpleid_checkid_ok($request);
524 simpleid_openid_consent_form($request, $response, $result);
525 }
526 break;
527 case CHECKID_RETURN_TO_SUSPECT:
528 log_info('CHECKID_RETURN_TO_SUSPECT');
529 if ($immediate) {
530 $response = simpleid_checkid_error($request, $immediate);
531 simpleid_assertion_response($response, $request['openid.return_to']);
532 } else {
533 $response = simpleid_checkid_ok($request);
534 simpleid_openid_consent_form($request, $response, $result);
535 }
536 break;
537 case CHECKID_OK:
538 log_info('CHECKID_OK');
539 $response = simpleid_checkid_ok($request);
540 $response = simpleid_sign($response, isset($request['openid.assoc_handle']) ? $request['openid.assoc_handle'] : NULL);
541 simpleid_assertion_response($response, $request['openid.return_to']);
542 break;
543 case CHECKID_LOGIN_REQUIRED:
544 log_info('CHECKID_LOGIN_REQUIRED');
545 if ($immediate) {
546 $response = simpleid_checkid_login_required($request);
547 simpleid_assertion_response($response, $request['openid.return_to']);
548 } else {
549 user_login_form('continue', pickle($request));
550 exit;
551 }
552 break;
553 case CHECKID_IDENTITIES_NOT_MATCHING:
554 case CHECKID_IDENTITY_NOT_EXIST:
555 log_info('CHECKID_IDENTITIES_NOT_MATCHING | CHECKID_IDENTITY_NOT_EXIST');
556 $response = simpleid_checkid_error($request, $immediate);
557 if ($immediate) {
558 simpleid_assertion_response($response, $request['openid.return_to']);
559 } else {
560 simpleid_openid_consent_form($request, $response, $result);
561 }
562 break;
563 case CHECKID_PROTOCOL_ERROR:
564 if (isset($request['openid.return_to'])) {
565 $response = simpleid_checkid_error($request, $immediate);
566 simpleid_assertion_response($response, $request['openid.return_to']);
567 } else {
568 indirect_fatal_error('Unrecognised request.');
569 }
570 break;
571 }
572}
573
574/**
575 * Processes a standard OpenID authentication request about an identity.
576 *
577 * Checks whether the current user logged into SimpleID matches the identity
578 * supplied in an OpenID request.
579 *
580 * @param array &$request the OpenID request
581 * @param bool $immediate whether checkid_immediate was used
582 * @return int one of CHECKID_OK, CHECKID_APPROVAL_REQUIRED, CHECKID_RETURN_TO_SUSPECT, CHECKID_IDENTITY_NOT_EXIST,
583 * CHECKID_IDENTITIES_NOT_MATCHING, CHECKID_LOGIN_REQUIRED or CHECKID_PROTOCOL_ERROR
584 * @global array the current logged in user
585 */
586function simpleid_checkid_identity(&$request, $immediate) {
587 global $user, $version;
588
589 $realm = openid_get_realm($request, $version);
590
591 // Check 1: Is the user logged into SimpleID as any user?
592 if ($user == NULL) {
593 return CHECKID_LOGIN_REQUIRED;
594 } else {
595 $uid = $user['uid'];
596 }
597
598 // Check 2: Is the user logged in as the same identity as the identity requested?
599 // Choose the identity URL for the user automatically
600 if ($request['openid.identity'] == OPENID_IDENTIFIER_SELECT) {
601 $test_user = user_load($uid);
602 $identity = $test_user['identity'];
603
604 log_info('OpenID identifier selection: Selected ' . $uid . ' [' . $identity . ']');
605 } else {
606 $identity = $request['openid.identity'];
607 $test_user = user_load_from_identity($identity);
608 }
609 if ($test_user == NULL) return CHECKID_IDENTITY_NOT_EXIST;
610 if ($test_user['uid'] != $user['uid']) {
611 log_notice('Requested user ' . $test_user['uid'] . ' does not match logged in user ' . $user['uid']);
612 return CHECKID_IDENTITIES_NOT_MATCHING;
613 }
614
615 // Pass the assertion to extensions
616 $assertion_results = extension_invoke_all('checkid_identity', $request, $identity, $immediate);
617 $assertion_results = array_merge(array_diff($assertion_results, array(NULL)));
618
619 // Populate the request with the selected identity
620 if ($request['openid.identity'] == OPENID_IDENTIFIER_SELECT) {
621 $request['openid.claimed_id'] = $identity;
622 $request['openid.identity'] = $identity;
623 }
624
625 // Check 3: Discover the realm and match its return_to
626 $user_rp = (isset($user['rp'][$realm])) ? $user['rp'][$realm] : NULL;
627
628 if (($version >= OPENID_VERSION_2) && SIMPLEID_VERIFY_RETURN_URL_USING_REALM) {
629 $verified = FALSE;
630
631 $rp_info = simpleid_get_rp_info($realm);
632 $services = discovery_xrds_services_by_type($rp_info['services'], OPENID_RETURN_TO);
633
634 log_info('OpenID 2 discovery: ' . count($services) . ' matching services');
635
636 if ($services) {
637 $return_to_uris = array();
638
639 foreach ($services as $service) {
640 $return_to_uris = array_merge($return_to_uris, $service['uri']);
641 }
642 foreach ($return_to_uris as $return_to) {
643 if (openid_url_matches_realm($request['openid.return_to'], $return_to)) {
644 log_info('OpenID 2 discovery: verified');
645 $verified = TRUE;
646 break;
647 }
648 }
649 }
650
651 $rp_info['return_to_verified'] = $verified;
652 simpleid_set_rp_info($realm, $rp_info);
653
654 if (!$verified) {
655 if (($user_rp != NULL) && ($user_rp['auto_release'] == 1)) {
656 log_notice('OpenID 2 discovery: not verified, but overridden by user preference');
657 } else {
658 log_notice('OpenID 2 discovery: not verified');
659 $assertion_results[] = CHECKID_RETURN_TO_SUSPECT;
660 }
661 }
662 }
663
664 // Check 4: For checkid_immediate, the user must already have given
665 // permission to log in automatically.
666 if (($user_rp != NULL) && ($user_rp['auto_release'] == 1)) {
667 log_info('Automatic set for realm ' . $realm);
668 $assertion_results[] = CHECKID_OK;
669 return min($assertion_results);
670 } else {
671 $assertion_results[] = CHECKID_APPROVAL_REQUIRED;
672 return min($assertion_results);
673 }
674}
675
676/**
677 * Obtains information on a relying party by performing discovery on them. Information
678 * obtained includes the discovery URL, the parsed XRDS document, and any other
679 * information saved by SimpleID extensions
680 *
681 * The results are cached for 1 hour. For performance reasons, stale results may
682 * be obtained by using the $allow_stale parameter
683 *
684 * @param string $realm the openid.realm parameter
685 * @param bool $allow_stale allow stale results to be returned, otherwise discovery
686 * will occur
687 * @return array containing information on a relying party.
688 * @link http://openid.net/specs/openid-authentication-2_0.html#rp_discovery
689 * @since 0.8
690 */
691function simpleid_get_rp_info($realm, $allow_stale = FALSE) {
692 $url = openid_realm_discovery_url($realm);
693
694 log_info('simpleid_get_rp_info');
695
696 $rp_info = cache_get('rp-info', $realm);
697
698 if (($rp_info == NULL) || (!isset($rp_info['updated'])) || (!$allow_stale && ($rp_info['updated'] < time() - 3600))) {
699 log_info('OpenID 2 RP discovery: realm: ' . $realm . '; url: ' . $url);
700
701 $rp_info = array(
702 'url' => $url,
703 'services' => discovery_xrds_discover($url),
704 'updated' => time()
705 );
706
707 cache_set('rp-info', $realm, $rp_info);
708 }
709
710 return $rp_info;
711}
712
713/**
714 * Saves information on a relying party to disk.
715 *
716 * @param string $realm the openid.realm parameter
717 * @param array $rp_info containing information on a relying party.
718 *
719 * @since 0.8
720 */
721function simpleid_set_rp_info($realm, $rp_info) {
722 if (!isset($rp_info['updated'])) $rp_info['updated'] = time();
723 cache_set('rp-info', $realm, $rp_info, $rp_info['updated']);
724}
725
726/**
727 * Returns an OpenID response indicating a positive assertion.
728 *
729 * @param array $request the OpenID request
730 * @return array an OpenID response with a positive assertion
731 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor17, http://openid.net/specs/openid-authentication-1_1.html#anchor23, http://openid.net/specs/openid-authentication-2_0.html#positive_assertions
732 */
733function simpleid_checkid_ok($request) {
734 global $version;
735
736 $message = array(
737 'openid.mode' => 'id_res',
738 'openid.op_endpoint' => simpleid_url(),
739 'openid.response_nonce' => openid_nonce()
740 );
741
742 if (isset($request['openid.assoc_handle'])) $message['openid.assoc_handle'] = $request['openid.assoc_handle'];
743 if (isset($request['openid.identity'])) $message['openid.identity'] = $request['openid.identity'];
744 if (isset($request['openid.return_to'])) $message['openid.return_to'] = $request['openid.return_to'];
745
746 if (($version >= OPENID_VERSION_2) && isset($request['openid.claimed_id'])) {
747 $message['openid.claimed_id'] = $request['openid.claimed_id'];
748 }
749
750 $message = array_merge($message, extension_invoke_all('response', TRUE, $request));
751
752 log_info('OpenID authentication response: ' . log_array($message));
753 return openid_indirect_message($message, $version);
754}
755
756/**
757 * Returns an OpenID response indicating a negative assertion to a
758 * checkid_immediate request, where an approval of the relying party by the
759 * user is required
760 *
761 * @param mixed $request the OpenID request
762 * @return mixed an OpenID response with a negative assertion
763 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor17, http://openid.net/specs/openid-authentication-1_1.html#anchor23, http://openid.net/specs/openid-authentication-2_0.html#negative_assertions
764 */
765function simpleid_checkid_approval_required($request) {
766 global $version;
767
768 if ($version >= OPENID_VERSION_2) {
769 $message = array('openid.mode' => 'setup_needed');
770 } else {
771 $request['openid.mode'] = 'checkid_setup';
772 $message = array(
773 'openid.mode' => 'id_res',
774 'openid.user_setup_url' => simpleid_url('continue', 's=' . rawurlencode(pickle($request)))
775 );
776 }
777
778 $message = array_merge($message, extension_invoke_all('response', FALSE, $request));
779
780 log_info('OpenID authentication response: ' . log_array($message));
781 return openid_indirect_message($message, $version);
782}
783
784/**
785 * Returns an OpenID response indicating a negative assertion to a
786 * checkid_immediate request, where the user has not logged in.
787 *
788 * @param array $request the OpenID request
789 * @return array an OpenID response with a negative assertion
790 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor17, http://openid.net/specs/openid-authentication-1_1.html#anchor23, http://openid.net/specs/openid-authentication-2_0.html#negative_assertions
791 */
792function simpleid_checkid_login_required($request) {
793 global $version;
794
795 if ($version >= OPENID_VERSION_2) {
796 $message = array('openid.mode' => 'setup_needed');
797 } else {
798 $message = array(
799 'openid.mode' => 'id_res',
800 'openid.user_setup_url' => simpleid_url('login', 'destination=continue&s=' . rawurlencode(pickle($request)))
801 );
802 }
803
804 $message = array_merge($message, extension_invoke_all('response', FALSE, $request));
805
806 log_info('OpenID authentication response: ' . log_array($message));
807 return openid_indirect_message($message, $version);
808}
809
810/**
811 * Returns an OpenID response indicating a generic negative assertion.
812 *
813 * The content of the negative version depends on the OpenID version, and whether
814 * the openid.mode of the request was checkid_immediate
815 *
816 * @param array $request the OpenID request
817 * @param bool $immediate true if openid.mode of the request was checkid_immediate
818 * @return array an OpenID response with a negative assertion
819 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor17, http://openid.net/specs/openid-authentication-1_1.html#anchor23, http://openid.net/specs/openid-authentication-2_0.html#negative_assertions
820 */
821function simpleid_checkid_error($request, $immediate = false) {
822 global $version;
823
824 $message = array();
825 if ($immediate) {
826 if ($version >= OPENID_VERSION_2) {
827 $message['openid.mode'] = 'setup_needed';
828 } else {
829 $message['openid.mode'] = 'id_res';
830 }
831 } else {
832 $message['openid.mode'] = 'cancel';
833 }
834
835 $message = array_merge($message, extension_invoke_all('response', FALSE, $request));
836
837 log_info('OpenID authentication response: ' . log_array($message));
838 return openid_indirect_message($message, $version);
839}
840
841/**
842 * Signs an OpenID response, using signature information from an association
843 * handle.
844 *
845 * @param array &$response the OpenID response
846 * @param array $assoc_handle the association handle containing key information
847 * for the signature. If $assoc_handle is not specified, a private association
848 * is created
849 * @return array the signed OpenID response
850 *
851 */
852function simpleid_sign(&$response, $assoc_handle = NULL) {
853 global $version;
854
855 if (!$assoc_handle) {
856 $assoc = _simpleid_create_association(ASSOCIATION_PRIVATE);
857 $response['openid.assoc_handle'] = $assoc['assoc_handle'];
858 } else {
859 $assoc = cache_get('association', $assoc_handle);
860
861 if (!is_array($assoc) || ($assoc['created'] + SIMPLEID_ASSOC_EXPIRES_IN < time())) {
862 // Association has expired, need to create a new one
863 log_notice('Association handle ' . ($assoc['assoc_handle'] ?? '(none)') . ' expired. Using stateless mode.');
864 $response['openid.invalidate_handle'] = $assoc_handle;
865 $assoc = _simpleid_create_association(ASSOCIATION_PRIVATE);
866 $response['openid.assoc_handle'] = $assoc['assoc_handle'];
867 }
868 }
869
870 // If we are using stateless mode, then we need to cache the response_nonce
871 // so that the RP can only verify once
872 if (isset($assoc['private']) && ($assoc['private'] == 1) && isset($response['openid.response_nonce'])) {
873 cache_set('stateless', $response['openid.response_nonce'], array(
874 'response_nonce' => $response['openid.response_nonce'],
875 'assoc_handle' => $response['openid.assoc_handle']));
876 }
877
878 // Get all the signed fields [10.1]
879 openid_parse_request($response); // Fill the namespace array
880 $signed_fields = array('op_endpoint', 'return_to', 'response_nonce', 'assoc_handle', 'identity', 'claimed_id');
881 $signed_fields = array_merge($signed_fields, extension_invoke_all('signed_fields', $response));
882
883 // Check if the signed keys are actually present
884 $to_sign = array();
885 foreach ($signed_fields as $field) {
886 if (isset($response['openid.' . $field])) $to_sign[] = $field;
887 }
888
889 $response['openid.signed'] = implode(',', $to_sign);
890
891 // Generate signature for this message
892 $mac_key = $assoc['mac_key'];
893 $assoc_types = openid_association_types();
894 $hmac_func = $assoc_types[$assoc['assoc_type']]['hmac_func'];
895
896 $response['openid.sig'] = openid_sign($response, $to_sign, $mac_key, $hmac_func, $version);
897
898 log_info('OpenID signed authentication response: ' . log_array($response));
899
900 return $response;
901}
902
903/**
904 * Processes a direct verification request. This is used in the OpenID specification
905 * to verify signatures generated using stateless mode.
906 *
907 * @param array $request the OpenID request
908 * @see http://openid.net/specs/openid-authentication-1_1.html#mode_check_authentication, http://openid.net/specs/openid-authentication-2_0.html#verifying_signatures
909 */
910function simpleid_check_authentication($request) {
911 global $version;
912
913 log_info('OpenID direct verification: ' . log_array($request));
914
915 $is_valid = simpleid_verify_signatures($request);
916
917 if ($is_valid) {
918 $response = array('is_valid' => 'true');
919 } else {
920 $response = array('is_valid' => 'false');
921 }
922
923 // RP wants to check whether a handle is invalid
924 if (isset($request['openid.invalidate_handle'])) {
925 $invalid_assoc = cache_get('association', $request['openid.invalidate_handle']);
926
927 if (!$invalid_assoc || ($invalid_assoc['created'] + SIMPLEID_ASSOC_EXPIRES_IN < time())) {
928 // Yes, it's invalid
929 $response['invalidate_handle'] = $request['openid.invalidate_handle'];
930 }
931 }
932
933 log_info('OpenID direct verification response: ' . log_array($response));
934
935 openid_direct_response(openid_direct_message($response, $version));
936}
937
938/**
939 * Verifies the signature of a signed OpenID request/response.
940 *
941 * @param array $request the OpenID request/response
942 * @return bool true if the signature is verified
943 * @since 0.8
944 */
945function simpleid_verify_signatures($request) {
946 global $version;
947
948 log_info('simpleid_verify_signatures');
949
950 $is_valid = TRUE;
951
952 $assoc = (isset($request['openid.assoc_handle'])) ? cache_get('association', $request['openid.assoc_handle']) : NULL;
953 $stateless = (isset($request['openid.response_nonce'])) ? cache_get('stateless', $request['openid.response_nonce']) : NULL;
954
955 if (!$assoc) {
956 log_notice('simpleid_verify_signatures: Association not found.');
957 $is_valid = FALSE;
958 } elseif (!$assoc['assoc_type']) {
959 log_error('simpleid_verify_signatures: Association does not contain valid assoc_type.');
960 $is_valid = FALSE;
961 } elseif (!isset($assoc['private']) || ($assoc['private'] != 1)) {
962 log_warn('simpleid_verify_signatures: Attempting to verify an association with a shared key.');
963 $is_valid = FALSE;
964 } elseif (!$stateless || ($stateless['assoc_handle'] != $request['openid.assoc_handle'])) {
965 log_warn('simpleid_verify_signatures: Attempting to verify a response_nonce more than once, or private association expired.');
966 $is_valid = FALSE;
967 } else {
968 $mac_key = $assoc['mac_key'];
969 $assoc_types = openid_association_types();
970 $hmac_func = $assoc_types[$assoc['assoc_type']]['hmac_func'];
971
972 $signed_keys = explode(',', $request['openid.signed']);
973 $signature = openid_sign($request, $signed_keys, $mac_key, $hmac_func, $version);
974 log_debug('***** Signature: ' . $signature);
975
976 if ($signature != $request['openid.sig']) {
977 log_warn('simpleid_verify_signatures: Signature supplied in request does not match the signatured generated.');
978 $is_valid = FALSE;
979 }
980
981 cache_delete('stateless', $request['openid.response_nonce']);
982 }
983
984 return $is_valid;
985}
986
987
988/**
989 * Continues an OpenID authentication request.
990 *
991 * This function decodes an OpenID authentication request specified in the
992 * s request parameter and feeds it to the
993 * {@link simpleid_process_openid} function. This allows SimpleID to preserve
994 * the state of an OpenID request.
995 */
996function simpleid_continue() {
997 global $GETPOST;
998
999 $request = unpickle($GETPOST['s']);
1000 openid_parse_request($request);
1001 simpleid_process_openid($request);
1002}
1003
1004/**
1005 * Provides a form for user consent of an OpenID relying party, where the
1006 * {@link simpleid_checkid_identity()} function returns a CHECKID_APPROVAL_REQUIRED
1007 * or CHECKID_RETURN_TO_SUSPECT.
1008 *
1009 * Alternatively, provide a form for the user to rectify the situation where
1010 * {@link simpleid_checkid_identity()} function returns a CHECKID_IDENTITIES_NOT_MATCHING
1011 * or CHECKID_IDENTITY_NOT_EXIST
1012 *
1013 * @param array $request the original OpenID request
1014 * @param array $response the proposed OpenID response, subject to user
1015 * verification
1016 * @param int $reason either CHECKID_APPROVAL_REQUIRED, CHECKID_RETURN_TO_SUSPECT,
1017 * CHECKID_IDENTITIES_NOT_MATCHING or CHECKID_IDENTITY_NOT_EXIST
1018 */
1019function simpleid_openid_consent_form($request, $response, $reason = CHECKID_APPROVAL_REQUIRED) {
1020 global $user;
1021 global $xtpl;
1022 global $version;
1023
1024 $request_state = pickle($request);
1025
1026 user_header($request_state);
1027
1028 $realm = openid_get_realm($request, $version);
1029
1030 $xtpl->assign('token', get_form_token('rp'));
1031 $xtpl->assign('state', pickle($response));
1032 $xtpl->assign('realm', htmlspecialchars($realm, ENT_QUOTES, 'UTF-8'));
1033
1034 $xtpl->assign('cancel_button', t('Cancel'));
1035
1036 if ($response['openid.mode'] == 'cancel') {
1037 $xtpl->assign('return_to', htmlspecialchars($request['openid.return_to'], ENT_QUOTES, 'UTF-8'));
1038
1039 $xtpl->assign('unable_label', t('Unable to log into <strong class="realm">@realm</strong>.', array('@realm' => $realm)));
1040 $xtpl->assign('identity_not_matching_label', t('Your current identity does not match the requested identity %identity.', array('%identity' => $request['openid.identity'])));
1041 $xtpl->assign('switch_user_label', t('<a href="!url">Switch to a different user</a> and try again.', array('!url' => simpleid_url('logout', 'destination=continue&s=' . rawurlencode($request_state), true))));
1042
1043 $xtpl->parse('main.openid_consent.cancel');
1044 } else {
1045 $xtpl->assign('javascript', '<script src="' . get_base_path() . 'html/openid-consent.js" type="text/javascript"></script>');
1046
1047 $rp = (isset($user['rp'][$realm])) ? $user['rp'][$realm] : NULL;
1048
1049 $extensions = extension_invoke_all('consent_form', $request, $response, $rp);
1050 $xtpl->assign('extensions', implode($extensions));
1051
1052 if ($reason == CHECKID_RETURN_TO_SUSPECT) {
1053 $xtpl->assign('suspect_label', t('Warning: This web site has not confirmed its identity and might be fraudulent. Do not share any personal information with this web site unless you are sure it is legitimate. See the <a href="!url" class="popup">SimpleID documentation for details</a> (OpenID version 2.0 return_to discovery failure)',
1054 array('!url' => 'http://simpleid.org/docs/1/return_to/')));
1055
1056 $xtpl->parse('main.openid_consent.setup.suspect');
1057 $xtpl->assign('realm_class', 'return-to-suspect');
1058 }
1059
1060 $xtpl->assign('realm_label', t('You are being logged into <strong class="realm">@realm</strong>.', array('@realm' => $realm)));
1061 $xtpl->assign('auto_release_label', t('Automatically send my information to this site for any future requests.'));
1062 $xtpl->assign('ok_button', t('OK'));
1063
1064 $xtpl->parse('main.openid_consent.setup');
1065 }
1066
1067 $xtpl->parse('main.openid_consent');
1068
1069 $xtpl->assign(array('js_locale_label' => 'openid_suspect', 'js_locale_text' => addslashes(t('This web site has not confirmed its identity and might be fraudulent.')) . '\n\n' . addslashes(t('Are you sure you wish to automatically send your information to this site for any future requests?'))));
1070 $xtpl->parse('main.js_locale');
1071
1072 $xtpl->parse('main.framekiller');
1073
1074 header('X-Frame-Options: DENY');
1075
1076 $xtpl->assign('title', t('OpenID Login'));
1077 $xtpl->assign('page_class', 'dialog-page');
1078 $xtpl->parse('main');
1079
1080 $xtpl->out('main');
1081}
1082
1083
1084/**
1085 * Processes a user response from the {@link simpleid_openid_consent_form()} function.
1086 *
1087 * If the user verifies the relying party, an OpenID response will be sent to
1088 * the relying party. Otherwise, the dashboard will be displayed to the user.
1089 *
1090 */
1091function simpleid_openid_consent() {
1092 global $xtpl, $user, $version, $GETPOST;
1093
1094 if ($user == NULL) {
1095 user_login_form('');
1096 return;
1097 }
1098
1099 if (!validate_form_token($GETPOST['tk'], 'rp')) {
1100 set_message(t('SimpleID detected a potential security attack. Please try again.'));
1101 $xtpl->assign('title', t('OpenID Login'));
1102 $xtpl->parse('main');
1103 $xtpl->out('main');
1104 return;
1105 }
1106
1107 $uid = $user['uid'];
1108
1109 $response = unpickle($GETPOST['s']);
1110 $version = openid_get_version($response);
1111 openid_parse_request($response);
1112 $return_to = $response['openid.return_to'];
1113 if (!$return_to) $return_to = $GETPOST['openid.return_to'];
1114
1115 if ($GETPOST['op'] == t('Cancel')) {
1116 $response = simpleid_checkid_error(false);
1117 if (!$return_to) set_message(t('Log in cancelled.'));
1118 } else {
1119 $now = time();
1120 $realm = $GETPOST['openid.realm'];
1121
1122 if (isset($user['rp'][$realm])) {
1123 $rp = $user['rp'][$realm];
1124 } else {
1125 $rp = array('realm' => $realm, 'first_time' => $now);
1126 }
1127 $rp['last_time'] = $now;
1128 $rp['auto_release'] = (isset($GETPOST['autorelease']) && $GETPOST['autorelease']) ? 1 : 0;
1129
1130 // Mimic extension_invoke_all, but allow for passing by reference {{
1131 global $simpleid_extensions;
1132
1133 foreach ($simpleid_extensions as $extension) {
1134 $consent_function = $extension . '_consent';
1135 if (function_exists($consent_function)) {
1136 $consent_function($GETPOST, $response, $rp);
1137 }
1138 }
1139 // }}
1140
1141 $user['rp'][$realm] = $rp;
1142 user_save($user);
1143
1144 $response = simpleid_sign($response, isset($response['openid.assoc_handle']) ? $response['openid.assoc_handle'] : NULL);
1145 if (!$return_to) set_message(t('You were logged in successfully.'));
1146 }
1147
1148 if ($return_to) {
1149 simpleid_assertion_response($response, $return_to);
1150 } else {
1151 page_dashboard();
1152 }
1153}
1154
1155/**
1156 * Sends an OpenID assertion response.
1157 *
1158 * The OpenID specification version 2.0 provides for the sending of assertions
1159 * via indirect communication. However, future versions of the OpenID
1160 * specification may provide for sending of assertions via direct communication.
1161 *
1162 * @param array $response the signed OpenID assertion response to send
1163 * @param string $indirect_url the URL to which the OpenID response is sent. If
1164 * this is an empty string, the response is sent via direct communication
1165 */
1166function simpleid_assertion_response($response, $indirect_url = NULL) {
1167 global $xtpl, $version;
1168
1169 if ($indirect_url) {
1170 // We want to see if the extensions want to change the way indirect responses are made
1171 $results = extension_invoke_all('indirect_response', $indirect_url, $response);
1172 $results = array_filter($results, 'is_null');
1173 $component = ($results) ? max($results) : OPENID_RESPONSE_QUERY;
1174
1175 openid_indirect_response($indirect_url, $response, $component);
1176 } else {
1177 openid_direct_response(openid_direct_message($response, $version));
1178 }
1179}
1180
1181/**
1182 * Displays the XRDS document for this SimpleID installation.
1183 *
1184 */
1185function simpleid_xrds() {
1186 global $xtpl;
1187
1188 log_debug('Providing XRDS.');
1189
1190 header('Content-Type: application/xrds+xml');
1191 header('Content-Disposition: inline; filename=yadis.xml');
1192
1193 $types = extension_invoke_all('xrds_types');
1194 foreach ($types as $type) {
1195 $xtpl->assign('uri', htmlspecialchars($type, ENT_QUOTES, 'UTF-8'));
1196 $xtpl->parse('xrds.op_xrds.type');
1197 }
1198
1199 $xtpl->assign('simpleid_base_url', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8'), false, 'detect');
1200 $xtpl->parse('xrds.op_xrds');
1201 $xtpl->parse('xrds');
1202 $xtpl->out('xrds');
1203}
1204
1205
1206?>