blob: b0f3fd68102b97d9ff775c6e44023559eca4af0d [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 * Common functions used by SimpleID, and the implementation of extensions.
32 *
33 * @package simpleid
34 * @filesource
35 */
36
37/**
38 * Sets a message to display to the user on the rendered SimpleID page.
39 *
40 * @param string $msg the message to set
41 */
42function set_message($msg) {
43 global $xtpl;
44
45 $xtpl->assign('message', $msg);
46 $xtpl->parse('main.message');
47}
48
49/**
50 * Displays a fatal error message and exits.
51 *
52 * @param string $error the message to set
53 */
54function indirect_fatal_error($error) {
55 global $xtpl;
56
57 set_message($error);
58
59 $xtpl->parse('main');
60 $xtpl->out('main');
61 exit;
62}
63
64/**
65 * Send a HTTP response code to the user agent.
66 *
67 * The format of the HTTP response code depends on the way PHP is run.
68 * When run as an Apache module, a properly formatted HTTP response
69 * string is sent. When run via CGI, the response code is sent via the
70 * Status response header.
71 *
72 * @param string $code the response code along
73 */
74function header_response_code($code) {
75 if (substr(PHP_SAPI, 0,3) === 'cgi') {
76 header('Status: ' . $code);
77 } else {
78 header($_SERVER['SERVER_PROTOCOL'] . ' ' . $code);
79 }
80}
81
82/**
83 * Determines whether the current connection with the user agent is via
84 * HTTPS.
85 *
86 * HTTPS is detected if one of the following occurs:
87 *
88 * - $_SERVER['HTTPS'] is set to 'on' (Apache installations)
89 * - $_SERVER['HTTP_X_FORWARDED_PROTO'] is set to 'https' (reverse proxies)
90 * - $_SERVER['HTTP_FRONT_END_HTTPS'] is set to 'on'
91 *
92 * @return bool true if the connection is via HTTPS
93 */
94function is_https() {
95 return (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on'))
96 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && (strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'))
97 || (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && ($_SERVER['HTTP_FRONT_END_HTTPS'] == 'on'));
98}
99
100
101/**
102 * Ensure the current connection with the user agent is secure with HTTPS.
103 *
104 * This function uses {@link is_https()} to determine whether the connection
105 * is via HTTPS. If it is, this function will return successfully.
106 *
107 * If it is not, what happens next is determined by the following steps.
108 *
109 * 1. If $allow_override is true and {@link SIMPLEID_ALLOW_PLAINTEXT} is also true,
110 * then the function will return successfully
111 * 2. Otherwise, then it will either redirect (if $action is
112 * redirect) or return an error (if $action is error)
113 *
114 * @param string $action what to do if connection is not secure - either
115 * 'redirect' or 'error'
116 * @param boolean $allow_override whether SIMPLEID_ALLOW_PLAINTEXT is checked
117 * to see if an unencrypted connection is allowed
118 * @param string $redirect_url if $action is redirect, what URL to redirect to.
119 * If null, this will redirect to the same page (albeit with an HTTPS connection)
120 * @param boolean $strict whether HTTP Strict Transport Security is active
121 * @see SIMPLEID_ALLOW_PLAINTEXT
122 */
123function check_https($action = 'redirect', $allow_override = false, $redirect_url = null, $strict = true) {
124 if (is_https()) {
125 if ($strict) header('Strict-Transport-Security: max-age=3600');
126 return;
127 }
128
129 if ($allow_override && SIMPLEID_ALLOW_PLAINTEXT) return;
130
131 if ($action == 'error') {
132 if (substr(PHP_SAPI, 0,3) === 'cgi') {
133 header('Status: 426 Upgrade Required');
134 } else {
135 header($_SERVER['SERVER_PROTOCOL'] . ' 426 Upgrade Required');
136 }
137
138 header('Upgrade: TLS/1.2, HTTP/1.1');
139 header('Connection: Upgrade');
140 indirect_fatal_error(t('An encrypted connection (HTTPS) is required for this page.'));
141 return;
142 }
143
144 if ($redirect_url == null) $redirect_url = simpleid_url('', $_SERVER['QUERY_STRING'], false, 'https');
145
146 if (substr(PHP_SAPI, 0,3) === 'cgi') {
147 header('Status: 301 Moved Permanently');
148 } else {
149 header($_SERVER['SERVER_PROTOCOL'] . ' 301 Moved Permanently');
150 }
151
152 header('Location: ' . $redirect_url);
153}
154
155/**
156 * Fix PHP's handling of request data. PHP changes dots in all request parameters
157 * to underscores when creating the $_GET, $_POST and $_REQUEST arrays.
158 *
159 * This function scans the original query string and POST parameters and fixes
160 * them.
161 */
162function fix_http_request() {
163 // Fix GET parameters
164 if (isset($_SERVER['QUERY_STRING'])) {
165 $get = parse_http_query($_SERVER['QUERY_STRING']);
166
167 foreach ($get as $key => $value) {
168 // We strip out array-like identifiers - PHP uses special processing for these
169 if ((strpos($key, '[') !== FALSE) && (strpos($key, ']') !== FALSE)) $key = substr($key, 0, strpos($key, '['));
170
171 // Replace special characters with underscore as per PHP processing
172 $php_key = preg_replace('/[ .[\x80-\x9F]/', '_', $key);
173
174 // See if the PHP key is present; if so, copy and delete
175 if (($key != $php_key) && isset($_GET[$php_key])) {
176 $_GET[$key] = $_GET[$php_key];
177 $_REQUEST[$key] = $_REQUEST[$php_key];
178 unset($_GET[$php_key]);
179 unset($_REQUEST[$php_key]);
180 }
181 }
182 }
183
184 // Fix POST parameters
185 $input = file_get_contents('php://input');
186 if ($input !== FALSE) {
187 $post = parse_http_query($input);
188
189 foreach ($post as $key => $value) {
190 // We strip out array-like identifiers - PHP uses special processing for these
191 if ((strpos($key, '[') !== FALSE) && (strpos($key, ']') !== FALSE)) $key = substr($key, 0, strpos($key, '['));
192
193 // Replace special characters with underscore as per PHP processing
194 $php_key = preg_replace('/[ .[\x80-\x9F]/', '_', $key);
195
196 // See if the PHP key is present; if so, copy and delete
197 if (($key != $php_key) && isset($_POST[$php_key])) {
198 $_POST[$key] = $_POST[$php_key];
199 $_REQUEST[$key] = $_REQUEST[$php_key];
200 unset($_POST[$php_key]);
201 unset($_REQUEST[$php_key]);
202 }
203 }
204 }
205}
206
207/**
208 * Parses a query string.
209 *
210 * @param string $query the query string to parse
211 * @return array an array containing the parsed key-value pairs
212 *
213 * @since 0.7
214 */
215function parse_http_query($query) {
216 $data = array();
217
218 if ($query === NULL) return array();
219 if ($query === '') return array();
220
221 $pairs = explode('&', $query);
222
223 foreach ($pairs as $pair) {
224 list ($key, $value) = explode('=', $pair, 2);
225 $data[$key] = urldecode($value);
226 }
227
228 return $data;
229}
230
231/**
232 * Assigns and returns a unique ID for the user agent (UAID).
233 *
234 * A UAID uniquely identifies the user agent (e.g. browser) used to
235 * make the HTTP request. The UAID is stored in a long-dated
236 * cookie. Therefore, the UAID may be useful for security purposes.
237 *
238 * This function will look for a cookie sent by the user agent with
239 * the name returned by {@link simpleid_cookie_name()} with a suffix
240 * of uaid. If the cookie does not exist, it will generate a
241 * random UAID and return it to the user agent with a Set-Cookie
242 * response header.
243 *
244 * @return string the UAID
245 */
246function get_user_agent_id() {
247 if (isset($_COOKIE[simpleid_cookie_name('uaid')])) return $_COOKIE[simpleid_cookie_name('uaid')];
248
249 $uaid = bin2hex(pack('LLLL', mt_rand(), mt_rand(), mt_rand(), mt_rand()));
250 setcookie(simpleid_cookie_name('uaid'), $uaid, time() + 315360000, get_base_path(), '', false, true);
251 return $uaid;
252}
253
254/**
255 * Content type negotiation using the Accept Header.
256 *
257 * Under HTTP, the user agent is able to negoatiate the content type returned with
258 * the server using HTTP Accept header. This header contains a comma-delimited
259 * list of items (e.g. content types) which the user agent is able to
260 * accept, ranked by a quality parameter.
261 *
262 * This function takes the header from the user agent, compares it against the
263 * content types which the server can provide, then returns the item which the highest
264 * quality which the server can provide.
265 *
266 * @param array $content_types an array of content types which the server can
267 * provide
268 * @param string $accept_header the header string provided by the user agent.
269 * If NULL, this defaults to $_SERVER['HTTP_ACCEPT'] if available
270 * @return string the negotiated content type, FALSE if $accept_header is NULL and
271 * the user agent did not provide an Accept header, or NULL if the negotiation is
272 * unsuccessful
273 *
274 * @since 0.8
275 *
276 */
277function negotiate_content_type($content_types, $accept_header = NULL) {
278 $content_types = array_map("strtolower", $content_types);
279 if (($accept_header == NULL) && isset($_SERVER['HTTP_ACCEPT'])) $accept_header = $_SERVER['HTTP_ACCEPT'];
280
281 if ($accept_header) {
282 $acceptible = preg_split('/\s*,\s*/', strtolower(trim($accept_header)));
283 for ($i = 0; $i < count($acceptible); $i++) {
284 $split = preg_split('/\s*;\s*q\s*=\s*/', $acceptible[$i], 2);
285 $item = strtolower($split[0]);
286
287 if (count($split) == 1) {
288 $q = 1.0;
289 } else {
290 $q = doubleval($split[1]);
291 }
292
293 if ($q > 0.0) {
294 if (in_array($item, $content_types)) {
295 if ($q == 1.0) {
296 return $item;
297 }
298 $candidates[$item] = $q;
299 } else {
300 $item = preg_quote($item, '/');
301 $item = strtr($item, array('\*' => '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'));
302
303 foreach ($content_types as $value) {
304 if (preg_match("/^$item$/", $value)) {
305 if ($q == 1.0) {
306 return $value;
307 }
308 $candidates[$value] = $q;
309 break;
310 }
311 }
312 }
313 }
314 }
315 if (isset($candidates)) {
316 arsort($candidates);
317 reset($candidates);
318 return key($candidates);
319 }
320 return NULL;
321 } else {
322 // No headers
323 return FALSE;
324 }
325}
326
327/**
328 * Serialises a variable for inclusion as a URL parameter.
329 *
330 * @param mixed $data the data to serialise
331 * @return string serialised data
332 * @see unpickle()
333 */
334function pickle($data) {
335 return base64_encode(gzcompress(serialize($data)));
336}
337
338/**
339 * Deserialises data specified in a URL parameter as a variable.
340 *
341 * @param string $pickle the serialised data
342 * @return mixed the deserialised data
343 * @see pickle()
344 */
345function unpickle($pickle) {
346 return unserialize(gzuncompress(base64_decode($pickle)));
347}
348
349/**
350 * Compares two strings using the same time whether they're equal or not.
351 * This function should be used to mitigate timing attacks when, for
352 * example, comparing password hashes
353 *
354 * @param string $str1
355 * @param string $str2
356 * @return bool true if the two strings are equal
357 */
358function secure_compare($str1, $str2) {
359 if (function_exists('hash_equals')) return hash_equals($str1, $str2);
360
361 $xor = $str1 ^ $str2;
362 $result = strlen($str1) ^ strlen($str2); //not the same length, then fail ($result != 0)
363 for ($i = strlen($xor) - 1; $i >= 0; $i--) $result += ord($xor[$i]);
364 return !$result;
365}
366
367/**
368 * Obtains the URI of the current request, given a base URI.
369 *
370 * @param string $base the base URI
371 * @return string the request URI
372 */
373function get_request_uri($base) {
374 $i = strpos($base, '//');
375 $i = strpos($base, '/', $i + 2);
376
377 if ($i === false) {
378 return $base . $_SERVER['REQUEST_URI'];
379 } else {
380 return substr($base, 0, $i) . $_SERVER['REQUEST_URI'];
381 }
382}
383
384/**
385 * Returns the base URL path, relative to the current host, of the SimpleID
386 * installation.
387 *
388 * This is worked out from {@link SIMPLEID_BASE_URL}. It will always contain
389 * a trailing slash.
390 *
391 * @return string the base URL path
392 * @since 0.8
393 * @see SIMPLEID_BASE_URL
394 */
395function get_base_path() {
396 static $base_path;
397
398 if (!$base_path) {
399 if ((substr(SIMPLEID_BASE_URL, -1) == '/') || (substr(SIMPLEID_BASE_URL, -9) == 'index.php')) {
400 $url = SIMPLEID_BASE_URL;
401 } else {
402 $url = SIMPLEID_BASE_URL . '/';
403 }
404
405 $parts = parse_url($url);
406 $base_path = $parts['path'];
407 }
408
409 return $base_path;
410}
411
412/**
413 * Determines whether the {@link SIMPLEID_BASE_URL} configuration option is a
414 * HTTPS URL.
415 *
416 * @return true if SIMPLEID_BASE_URL is a HTTPS URL
417 */
418function is_base_https() {
419 return (stripos(SIMPLEID_BASE_URL, 'https:') === 0);
420}
421
422/**
423 * Obtains a SimpleID URL. URLs produced by SimpleID should use this function.
424 *
425 * @param string $q the q parameter
426 * @param string $params a properly encoded query string
427 * @param bool $relative whether a relative URL should be returned
428 * @param string $secure if $relative is false, either 'https' to force an HTTPS connection, 'http' to force
429 * an unencrypted HTTP connection, 'detect' to base on the current connection, or NULL to vary based on SIMPLEID_BASE_URL
430 * @return string the url
431 *
432 * @since 0.7
433 */
434function simpleid_url($q = '', $params = '', $relative = false, $secure = null) {
435 if ($relative) {
436 $url = get_base_path();
437 } else {
438 // Make sure that the base has a trailing slash
439 if ((substr(SIMPLEID_BASE_URL, -1) == '/') || (substr(SIMPLEID_BASE_URL, -9) == 'index.php')) {
440 $url = SIMPLEID_BASE_URL;
441 } else {
442 $url = SIMPLEID_BASE_URL . '/';
443 }
444
445 if (($secure == 'https') && (stripos($url, 'http:') === 0)) {
446 $url = 'https:' . substr($url, 5);
447 }
448 if (($secure == 'http') && (stripos($url, 'https:') === 0)) {
449 $url = 'http:' . substr($url, 6);
450 }
451 if (($secure == 'detect') && (is_https()) && (stripos($url, 'http:') === 0)) {
452 $url = 'https:' . substr($url, 5);
453 }
454 if (($secure == 'detect') && (!is_https()) && (stripos($url, 'https:') === 0)) {
455 $url = 'http:' . substr($url, 6);
456 }
457 }
458
459 if (SIMPLEID_CLEAN_URL) {
460 $url .= $q . (($params == '') ? '' : '?' . $params);
461 } elseif (($q == '') && ($params == '')) {
462 $url .= '';
463 } elseif ($q == '') {
464 $url .= 'index.php?' . $params;
465 } else {
466 $url .= 'index.php?q=' . $q . (($params == '') ? '' : '&' . $params);
467 }
468 return $url;
469}
470
471/**
472 * Obtains the URL of the host of the SimpleID's installation. The host is worked
473 * out based on SIMPLEID_BASE_URL
474 *
475 * @param string $secure if $relative is false, either 'https' to force an HTTPS connection, 'http' to force
476 * an unencrypted HTTP connection, or NULL to vary based on SIMPLEID_BASE_URL
477 * @return string the url
478 */
479function simpleid_host_url($secure = null) {
480 $parts = parse_url(SIMPLEID_BASE_URL);
481
482 if ($secure == 'https') {
483 $scheme = 'https';
484 } elseif ($secure == 'http') {
485 $scheme = 'http';
486 } else {
487 $scheme = $parts['scheme'];
488 }
489
490 $url = $scheme . '://';
491 if (isset($parts['user'])) {
492 $url .= $parts['user'];
493 if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
494 $url .= '@';
495 }
496 $url .= $parts['host'];
497 if (isset($parts['port'])) $url .= ':' . $parts['port'];
498
499 return $url;
500}
501
502/**
503 * Returns a relatively unique cookie name based on a specified suffix and
504 * SIMPLEID_BASE_URL.
505 *
506 * @param string $suffix the cookie name suffix
507 * @return string the cookie name
508 */
509function simpleid_cookie_name($suffix) {
510 static $prefix = NULL;
511
512 if ($prefix == NULL) {
513 $prefix = substr(get_form_token('cookie', FALSE), 0, 7) . '_';
514 }
515 return $prefix . $suffix;
516}
517
518/**
519 * Obtains a form token given a form ID.
520 *
521 * Form tokens are used in SimpleID forms to guard against cross-site forgery
522 * attacks.
523 *
524 * @param string $id the form ID
525 * @param bool $bind_session whether to bind the form token to the current session
526 * @return string a form token
527 */
528function get_form_token($id, $bind_session = TRUE) {
529 global $user;
530
531 if (store_get('site-token') == NULL) {
532 $site_token = pack('LLLL', mt_rand(), mt_rand(), mt_rand(), mt_rand());
533 store_set('site-token', $site_token);
534 } else {
535 $site_token = store_get('site-token');
536 }
537
538 return _get_form_token($site_token, $id, $bind_session);
539}
540
541/**
542 * Checks whether a form token is valid
543 *
544 * @param string $token the token returned by the user agent
545 * @param string $id the form ID
546 * @param bool $bind_session whether the token has been bound to the current session
547 * @return bool true if the form token is valid
548 */
549function validate_form_token($token, $id, $bind_session = TRUE) {
550 global $user;
551
552 $site_token = store_get('site-token');
553
554 return ($token == _get_form_token($site_token, $id, $bind_session));
555}
556
557function _get_form_token($site_token, $id, $bind_session = TRUE) {
558 global $user;
559
560 if (($user == NULL) || (!$bind_session)) {
561 $key = $site_token;
562 } else {
563 $key = session_id() . $site_token;
564 }
565
566 if (function_exists('hash_hmac') && function_exists('hash_algos') && (in_array('sha1', hash_algos()))) {
567 return hash_hmac('sha1', $id, $key);
568 } else {
569 if (strlen($site_token) > 64) {
570 $site_token = sha1($site_token, TRUE);
571 }
572
573 $site_token = str_pad($site_token, 64, chr(0x00));
574 $ipad = str_repeat(chr(0x36), 64);
575 $opad = str_repeat(chr(0x5c), 64);
576 return bin2hex(sha1(($key ^ $opad) . sha1(($key ^ $ipad) . $text, TRUE), TRUE));
577 }
578}
579
580/* ------- SimpleID extension support ---------------------------------------- */
581
582
583/**
584 * This variable holds an array of extensions specified by the user
585 *
586 * @global array $simpleid_extensions
587 * @see SIMPLEID_EXTENSIONS
588 */
589$simpleid_extensions = array();
590
591/**
592 * Initialises the extension mechanism. This function looks up the extensions
593 * to load in the {@link SIMPLEID_EXTENSIONS} constants, loads them, then
594 * calls the ns hook.
595 */
596function extension_init() {
597 global $simpleid_extensions;
598
599 $simpleid_extensions = preg_split('/,\s*/', SIMPLEID_EXTENSIONS);
600
601 foreach ($simpleid_extensions as $extension) {
602 include_once 'extensions/' . $extension . '/' . $extension . '.extension.php';
603 }
604}
605
606/**
607 * Invokes a hook in all the loaded extensions.
608 *
609 * @param string $function the name of the hook to call
610 * @param mixed $args the arguments to the hook
611 * @return array the return values from the hook
612 */
613function extension_invoke_all() {
614 global $simpleid_extensions;
615
616 $args = func_get_args();
617 $function = array_shift($args);
618 $return = array();
619
620 foreach ($simpleid_extensions as $extension) {
621 if (function_exists($extension . '_' . $function)) {
622 log_debug('extension_invoke_all: ' . $extension . '_' . $function);
623 $result = call_user_func_array($extension . '_' . $function, $args);
624 if (isset($result) && is_array($result)) {
625 $return = array_merge($return, $result);
626 } elseif (isset($result)) {
627 $return[] = $result;
628 }
629 }
630 }
631
632 return $return;
633}
634
635/**
636 * Invokes a hook in a specified extension.
637 *
638 * @param string $extension the extension to call
639 * @param string $function the name of the hook to call
640 * @param mixed $args the arguments to the hook
641 * @return mixed the return value from the hook
642 */
643function extension_invoke() {
644 $args = func_get_args();
645 $extension = array_shift($args);
646 $function = array_shift($args);
647
648 if (function_exists($extension . '_' . $function)) {
649 log_debug('extension_invoke: ' . $extension . '_' . $function);
650 return call_user_func_array($extension . '_' . $function, $args);
651 }
652}
653
654/**
655 * Returns an array of currently loaded extensions.
656 *
657 * @return array a list of the names of the currently loaded extensions.
658 */
659function get_extensions() {
660 global $simpleid_extensions;
661
662 return $simpleid_extensions;
663}
664?>