[BUGFIX] Fix T3editor after PSR-7 change
[Packages/TYPO3.CMS.git] / typo3 / sysext / openid / lib / php-openid / Auth / OpenID / FileStore.php
1 <?php
2
3 /**
4 * This file supplies a Memcached store backend for OpenID servers and
5 * consumers.
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: See the COPYING file included in this distribution.
10 *
11 * @package OpenID
12 * @author JanRain, Inc. <openid@janrain.com>
13 * @copyright 2005-2008 Janrain, Inc.
14 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
15 */
16
17 /**
18 * Require base class for creating a new interface.
19 */
20 require_once 'Auth/OpenID.php';
21 require_once 'Auth/OpenID/Interface.php';
22 require_once 'Auth/OpenID/HMAC.php';
23 require_once 'Auth/OpenID/Nonce.php';
24
25 /**
26 * This is a filesystem-based store for OpenID associations and
27 * nonces. This store should be safe for use in concurrent systems on
28 * both windows and unix (excluding NFS filesystems). There are a
29 * couple race conditions in the system, but those failure cases have
30 * been set up in such a way that the worst-case behavior is someone
31 * having to try to log in a second time.
32 *
33 * Most of the methods of this class are implementation details.
34 * People wishing to just use this store need only pay attention to
35 * the constructor.
36 *
37 * @package OpenID
38 */
39 class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore {
40
41 /**
42 * Initializes a new {@link Auth_OpenID_FileStore}. This
43 * initializes the nonce and association directories, which are
44 * subdirectories of the directory passed in.
45 *
46 * @param string $directory This is the directory to put the store
47 * directories in.
48 */
49 function Auth_OpenID_FileStore($directory)
50 {
51 if (!Auth_OpenID::ensureDir($directory)) {
52 trigger_error('Not a directory and failed to create: '
53 . $directory, E_USER_ERROR);
54 }
55 $directory = realpath($directory);
56
57 $this->directory = $directory;
58 $this->active = true;
59
60 $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces';
61
62 $this->association_dir = $directory . DIRECTORY_SEPARATOR .
63 'associations';
64
65 // Temp dir must be on the same filesystem as the assciations
66 // $directory.
67 $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp';
68
69 $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds
70
71 if (!$this->_setup()) {
72 trigger_error('Failed to initialize OpenID file store in ' .
73 $directory, E_USER_ERROR);
74 }
75 }
76
77 function destroy()
78 {
79 Auth_OpenID_FileStore::_rmtree($this->directory);
80 $this->active = false;
81 }
82
83 /**
84 * Make sure that the directories in which we store our data
85 * exist.
86 *
87 * @access private
88 */
89 function _setup()
90 {
91 return (Auth_OpenID::ensureDir($this->nonce_dir) &&
92 Auth_OpenID::ensureDir($this->association_dir) &&
93 Auth_OpenID::ensureDir($this->temp_dir));
94 }
95
96 /**
97 * Create a temporary file on the same filesystem as
98 * $this->association_dir.
99 *
100 * The temporary directory should not be cleaned if there are any
101 * processes using the store. If there is no active process using
102 * the store, it is safe to remove all of the files in the
103 * temporary directory.
104 *
105 * @return array ($fd, $filename)
106 * @access private
107 */
108 function _mktemp()
109 {
110 $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir);
111 $file_obj = @fopen($name, 'wb');
112 if ($file_obj !== false) {
113 return array($file_obj, $name);
114 } else {
115 Auth_OpenID_FileStore::_removeIfPresent($name);
116 }
117 }
118
119 function cleanupNonces()
120 {
121 global $Auth_OpenID_SKEW;
122
123 $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
124 $now = time();
125
126 $removed = 0;
127 // Check all nonces for expiry
128 foreach ($nonces as $nonce_fname) {
129 $base = basename($nonce_fname);
130 $parts = explode('-', $base, 2);
131 $timestamp = $parts[0];
132 $timestamp = intval($timestamp, 16);
133 if (abs($timestamp - $now) > $Auth_OpenID_SKEW) {
134 Auth_OpenID_FileStore::_removeIfPresent($nonce_fname);
135 $removed += 1;
136 }
137 }
138 return $removed;
139 }
140
141 /**
142 * Create a unique filename for a given server url and
143 * handle. This implementation does not assume anything about the
144 * format of the handle. The filename that is returned will
145 * contain the domain name from the server URL for ease of human
146 * inspection of the data directory.
147 *
148 * @return string $filename
149 */
150 function getAssociationFilename($server_url, $handle)
151 {
152 if (!$this->active) {
153 trigger_error("FileStore no longer active", E_USER_ERROR);
154 return null;
155 }
156
157 if (strpos($server_url, '://') === false) {
158 trigger_error(sprintf("Bad server URL: %s", $server_url),
159 E_USER_WARNING);
160 return null;
161 }
162
163 list($proto, $rest) = explode('://', $server_url, 2);
164 $parts = explode('/', $rest);
165 $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]);
166 $url_hash = Auth_OpenID_FileStore::_safe64($server_url);
167 if ($handle) {
168 $handle_hash = Auth_OpenID_FileStore::_safe64($handle);
169 } else {
170 $handle_hash = '';
171 }
172
173 $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash,
174 $handle_hash);
175
176 return $this->association_dir. DIRECTORY_SEPARATOR . $filename;
177 }
178
179 /**
180 * Store an association in the association directory.
181 */
182 function storeAssociation($server_url, $association)
183 {
184 if (!$this->active) {
185 trigger_error("FileStore no longer active", E_USER_ERROR);
186 return false;
187 }
188
189 $association_s = $association->serialize();
190 $filename = $this->getAssociationFilename($server_url,
191 $association->handle);
192 list($tmp_file, $tmp) = $this->_mktemp();
193
194 if (!$tmp_file) {
195 trigger_error("_mktemp didn't return a valid file descriptor",
196 E_USER_WARNING);
197 return false;
198 }
199
200 fwrite($tmp_file, $association_s);
201
202 fflush($tmp_file);
203
204 fclose($tmp_file);
205
206 if (@rename($tmp, $filename)) {
207 return true;
208 } else {
209 // In case we are running on Windows, try unlinking the
210 // file in case it exists.
211 @unlink($filename);
212
213 // Now the target should not exist. Try renaming again,
214 // giving up if it fails.
215 if (@rename($tmp, $filename)) {
216 return true;
217 }
218 }
219
220 // If there was an error, don't leave the temporary file
221 // around.
222 Auth_OpenID_FileStore::_removeIfPresent($tmp);
223 return false;
224 }
225
226 /**
227 * Retrieve an association. If no handle is specified, return the
228 * association with the most recent issue time.
229 *
230 * @return mixed $association
231 */
232 function getAssociation($server_url, $handle = null)
233 {
234 if (!$this->active) {
235 trigger_error("FileStore no longer active", E_USER_ERROR);
236 return null;
237 }
238
239 if ($handle === null) {
240 $handle = '';
241 }
242
243 // The filename with the empty handle is a prefix of all other
244 // associations for the given server URL.
245 $filename = $this->getAssociationFilename($server_url, $handle);
246
247 if ($handle) {
248 return $this->_getAssociation($filename);
249 } else {
250 $association_files =
251 Auth_OpenID_FileStore::_listdir($this->association_dir);
252 $matching_files = array();
253
254 // strip off the path to do the comparison
255 $name = basename($filename);
256 foreach ($association_files as $association_file) {
257 $base = basename($association_file);
258 if (strpos($base, $name) === 0) {
259 $matching_files[] = $association_file;
260 }
261 }
262
263 $matching_associations = array();
264 // read the matching files and sort by time issued
265 foreach ($matching_files as $full_name) {
266 $association = $this->_getAssociation($full_name);
267 if ($association !== null) {
268 $matching_associations[] = array($association->issued,
269 $association);
270 }
271 }
272
273 $issued = array();
274 $assocs = array();
275 foreach ($matching_associations as $key => $assoc) {
276 $issued[$key] = $assoc[0];
277 $assocs[$key] = $assoc[1];
278 }
279
280 array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
281 $matching_associations);
282
283 // return the most recently issued one.
284 if ($matching_associations) {
285 list($issued, $assoc) = $matching_associations[0];
286 return $assoc;
287 } else {
288 return null;
289 }
290 }
291 }
292
293 /**
294 * @access private
295 */
296 function _getAssociation($filename)
297 {
298 if (!$this->active) {
299 trigger_error("FileStore no longer active", E_USER_ERROR);
300 return null;
301 }
302
303 if (file_exists($filename) !== true) {
304 return null;
305 }
306
307 $assoc_file = @fopen($filename, 'rb');
308
309 if ($assoc_file === false) {
310 return null;
311 }
312
313 $filesize = filesize($filename);
314 if ($filesize === false || $filesize <= 0) {
315 return null;
316 }
317
318 $assoc_s = fread($assoc_file, $filesize);
319 fclose($assoc_file);
320
321 if (!$assoc_s) {
322 return null;
323 }
324
325 $association =
326 Auth_OpenID_Association::deserialize('Auth_OpenID_Association',
327 $assoc_s);
328
329 if (!$association) {
330 Auth_OpenID_FileStore::_removeIfPresent($filename);
331 return null;
332 }
333
334 if ($association->getExpiresIn() == 0) {
335 Auth_OpenID_FileStore::_removeIfPresent($filename);
336 return null;
337 } else {
338 return $association;
339 }
340 }
341
342 /**
343 * Remove an association if it exists. Do nothing if it does not.
344 *
345 * @return bool $success
346 */
347 function removeAssociation($server_url, $handle)
348 {
349 if (!$this->active) {
350 trigger_error("FileStore no longer active", E_USER_ERROR);
351 return null;
352 }
353
354 $assoc = $this->getAssociation($server_url, $handle);
355 if ($assoc === null) {
356 return false;
357 } else {
358 $filename = $this->getAssociationFilename($server_url, $handle);
359 return Auth_OpenID_FileStore::_removeIfPresent($filename);
360 }
361 }
362
363 /**
364 * Return whether this nonce is present. As a side effect, mark it
365 * as no longer present.
366 *
367 * @return bool $present
368 */
369 function useNonce($server_url, $timestamp, $salt)
370 {
371 global $Auth_OpenID_SKEW;
372
373 if (!$this->active) {
374 trigger_error("FileStore no longer active", E_USER_ERROR);
375 return null;
376 }
377
378 if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
379 return false;
380 }
381
382 if ($server_url) {
383 list($proto, $rest) = explode('://', $server_url, 2);
384 } else {
385 $proto = '';
386 $rest = '';
387 }
388
389 $parts = explode('/', $rest, 2);
390 $domain = $this->_filenameEscape($parts[0]);
391 $url_hash = $this->_safe64($server_url);
392 $salt_hash = $this->_safe64($salt);
393
394 $filename = sprintf('%08x-%s-%s-%s-%s', $timestamp, $proto,
395 $domain, $url_hash, $salt_hash);
396 $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $filename;
397
398 $result = @fopen($filename, 'x');
399
400 if ($result === false) {
401 return false;
402 } else {
403 fclose($result);
404 return true;
405 }
406 }
407
408 /**
409 * Remove expired entries from the database. This is potentially
410 * expensive, so only run when it is acceptable to take time.
411 *
412 * @access private
413 */
414 function _allAssocs()
415 {
416 $all_associations = array();
417
418 $association_filenames =
419 Auth_OpenID_FileStore::_listdir($this->association_dir);
420
421 foreach ($association_filenames as $association_filename) {
422 $association_file = fopen($association_filename, 'rb');
423
424 if ($association_file !== false) {
425 $assoc_s = fread($association_file,
426 filesize($association_filename));
427 fclose($association_file);
428
429 // Remove expired or corrupted associations
430 $association =
431 Auth_OpenID_Association::deserialize(
432 'Auth_OpenID_Association', $assoc_s);
433
434 if ($association === null) {
435 Auth_OpenID_FileStore::_removeIfPresent(
436 $association_filename);
437 } else {
438 if ($association->getExpiresIn() == 0) {
439 $all_associations[] = array($association_filename,
440 $association);
441 }
442 }
443 }
444 }
445
446 return $all_associations;
447 }
448
449 function clean()
450 {
451 if (!$this->active) {
452 trigger_error("FileStore no longer active", E_USER_ERROR);
453 return null;
454 }
455
456 $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
457 $now = time();
458
459 // Check all nonces for expiry
460 foreach ($nonces as $nonce) {
461 if (!Auth_OpenID_checkTimestamp($nonce, $now)) {
462 $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
463 Auth_OpenID_FileStore::_removeIfPresent($filename);
464 }
465 }
466
467 foreach ($this->_allAssocs() as $pair) {
468 list($assoc_filename, $assoc) = $pair;
469 if ($assoc->getExpiresIn() == 0) {
470 Auth_OpenID_FileStore::_removeIfPresent($assoc_filename);
471 }
472 }
473 }
474
475 /**
476 * @access private
477 */
478 function _rmtree($dir)
479 {
480 if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) {
481 $dir .= DIRECTORY_SEPARATOR;
482 }
483
484 if ($handle = opendir($dir)) {
485 while ($item = readdir($handle)) {
486 if (!in_array($item, array('.', '..'))) {
487 if (is_dir($dir . $item)) {
488
489 if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) {
490 return false;
491 }
492 } else if (is_file($dir . $item)) {
493 if (!unlink($dir . $item)) {
494 return false;
495 }
496 }
497 }
498 }
499
500 closedir($handle);
501
502 if (!@rmdir($dir)) {
503 return false;
504 }
505
506 return true;
507 } else {
508 // Couldn't open directory.
509 return false;
510 }
511 }
512
513 /**
514 * @access private
515 */
516 function _mkstemp($dir)
517 {
518 foreach (range(0, 4) as $i) {
519 $name = tempnam($dir, "php_openid_filestore_");
520
521 if ($name !== false) {
522 return $name;
523 }
524 }
525 return false;
526 }
527
528 /**
529 * @access private
530 */
531 static function _mkdtemp($dir)
532 {
533 foreach (range(0, 4) as $i) {
534 $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) .
535 "-" . strval(rand(1, time()));
536 if (!mkdir($name, 0700)) {
537 return false;
538 } else {
539 return $name;
540 }
541 }
542 return false;
543 }
544
545 /**
546 * @access private
547 */
548 function _listdir($dir)
549 {
550 $handle = opendir($dir);
551 $files = array();
552 while (false !== ($filename = readdir($handle))) {
553 if (!in_array($filename, array('.', '..'))) {
554 $files[] = $dir . DIRECTORY_SEPARATOR . $filename;
555 }
556 }
557 return $files;
558 }
559
560 /**
561 * @access private
562 */
563 function _isFilenameSafe($char)
564 {
565 $_Auth_OpenID_filename_allowed = Auth_OpenID_letters .
566 Auth_OpenID_digits . ".";
567 return (strpos($_Auth_OpenID_filename_allowed, $char) !== false);
568 }
569
570 /**
571 * @access private
572 */
573 function _safe64($str)
574 {
575 $h64 = base64_encode(Auth_OpenID_SHA1($str));
576 $h64 = str_replace('+', '_', $h64);
577 $h64 = str_replace('/', '.', $h64);
578 $h64 = str_replace('=', '', $h64);
579 return $h64;
580 }
581
582 /**
583 * @access private
584 */
585 function _filenameEscape($str)
586 {
587 $filename = "";
588 $b = Auth_OpenID::toBytes($str);
589
590 for ($i = 0; $i < count($b); $i++) {
591 $c = $b[$i];
592 if (Auth_OpenID_FileStore::_isFilenameSafe($c)) {
593 $filename .= $c;
594 } else {
595 $filename .= sprintf("_%02X", ord($c));
596 }
597 }
598 return $filename;
599 }
600
601 /**
602 * Attempt to remove a file, returning whether the file existed at
603 * the time of the call.
604 *
605 * @access private
606 * @return bool $result True if the file was present, false if not.
607 */
608 function _removeIfPresent($filename)
609 {
610 return @unlink($filename);
611 }
612
613 function cleanupAssociations()
614 {
615 $removed = 0;
616 foreach ($this->_allAssocs() as $pair) {
617 list($assoc_filename, $assoc) = $pair;
618 if ($assoc->getExpiresIn() == 0) {
619 $this->_removeIfPresent($assoc_filename);
620 $removed += 1;
621 }
622 }
623 return $removed;
624 }
625 }
626
627