TYPO3 API  SVNRelease
FileStore.php
Go to the documentation of this file.
00001 <?php
00002 
00003 /**
00004  * This file supplies a Memcached store backend for OpenID servers and
00005  * consumers.
00006  *
00007  * PHP versions 4 and 5
00008  *
00009  * LICENSE: See the COPYING file included in this distribution.
00010  *
00011  * @package OpenID
00012  * @author JanRain, Inc. <openid@janrain.com>
00013  * @copyright 2005-2008 Janrain, Inc.
00014  * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
00015  */
00016 
00017 /**
00018  * Require base class for creating a new interface.
00019  */
00020 require_once 'Auth/OpenID.php';
00021 require_once 'Auth/OpenID/Interface.php';
00022 require_once 'Auth/OpenID/HMAC.php';
00023 require_once 'Auth/OpenID/Nonce.php';
00024 
00025 /**
00026  * This is a filesystem-based store for OpenID associations and
00027  * nonces.  This store should be safe for use in concurrent systems on
00028  * both windows and unix (excluding NFS filesystems).  There are a
00029  * couple race conditions in the system, but those failure cases have
00030  * been set up in such a way that the worst-case behavior is someone
00031  * having to try to log in a second time.
00032  *
00033  * Most of the methods of this class are implementation details.
00034  * People wishing to just use this store need only pay attention to
00035  * the constructor.
00036  *
00037  * @package OpenID
00038  */
00039 class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore {
00040 
00041     /**
00042      * Initializes a new {@link Auth_OpenID_FileStore}.  This
00043      * initializes the nonce and association directories, which are
00044      * subdirectories of the directory passed in.
00045      *
00046      * @param string $directory This is the directory to put the store
00047      * directories in.
00048      */
00049     function Auth_OpenID_FileStore($directory)
00050     {
00051         if (!Auth_OpenID::ensureDir($directory)) {
00052             trigger_error('Not a directory and failed to create: '
00053                           . $directory, E_USER_ERROR);
00054         }
00055         $directory = realpath($directory);
00056 
00057         $this->directory = $directory;
00058         $this->active = true;
00059 
00060         $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces';
00061 
00062         $this->association_dir = $directory . DIRECTORY_SEPARATOR .
00063             'associations';
00064 
00065         // Temp dir must be on the same filesystem as the assciations
00066         // $directory.
00067         $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp';
00068 
00069         $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds
00070 
00071         if (!$this->_setup()) {
00072             trigger_error('Failed to initialize OpenID file store in ' .
00073                           $directory, E_USER_ERROR);
00074         }
00075     }
00076 
00077     function destroy()
00078     {
00079         Auth_OpenID_FileStore::_rmtree($this->directory);
00080         $this->active = false;
00081     }
00082 
00083     /**
00084      * Make sure that the directories in which we store our data
00085      * exist.
00086      *
00087      * @access private
00088      */
00089     function _setup()
00090     {
00091         return (Auth_OpenID::ensureDir($this->nonce_dir) &&
00092                 Auth_OpenID::ensureDir($this->association_dir) &&
00093                 Auth_OpenID::ensureDir($this->temp_dir));
00094     }
00095 
00096     /**
00097      * Create a temporary file on the same filesystem as
00098      * $this->association_dir.
00099      *
00100      * The temporary directory should not be cleaned if there are any
00101      * processes using the store. If there is no active process using
00102      * the store, it is safe to remove all of the files in the
00103      * temporary directory.
00104      *
00105      * @return array ($fd, $filename)
00106      * @access private
00107      */
00108     function _mktemp()
00109     {
00110         $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir);
00111         $file_obj = @fopen($name, 'wb');
00112         if ($file_obj !== false) {
00113             return array($file_obj, $name);
00114         } else {
00115             Auth_OpenID_FileStore::_removeIfPresent($name);
00116         }
00117     }
00118 
00119     function cleanupNonces()
00120     {
00121         global $Auth_OpenID_SKEW;
00122 
00123         $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
00124         $now = time();
00125 
00126         $removed = 0;
00127         // Check all nonces for expiry
00128         foreach ($nonces as $nonce_fname) {
00129             $base = basename($nonce_fname);
00130             $parts = explode('-', $base, 2);
00131             $timestamp = $parts[0];
00132             $timestamp = intval($timestamp, 16);
00133             if (abs($timestamp - $now) > $Auth_OpenID_SKEW) {
00134                 Auth_OpenID_FileStore::_removeIfPresent($nonce_fname);
00135                 $removed += 1;
00136             }
00137         }
00138         return $removed;
00139     }
00140 
00141     /**
00142      * Create a unique filename for a given server url and
00143      * handle. This implementation does not assume anything about the
00144      * format of the handle. The filename that is returned will
00145      * contain the domain name from the server URL for ease of human
00146      * inspection of the data directory.
00147      *
00148      * @return string $filename
00149      */
00150     function getAssociationFilename($server_url, $handle)
00151     {
00152         if (!$this->active) {
00153             trigger_error("FileStore no longer active", E_USER_ERROR);
00154             return null;
00155         }
00156 
00157         if (strpos($server_url, '://') === false) {
00158             trigger_error(sprintf("Bad server URL: %s", $server_url),
00159                           E_USER_WARNING);
00160             return null;
00161         }
00162 
00163         list($proto, $rest) = explode('://', $server_url, 2);
00164         $parts = explode('/', $rest);
00165         $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]);
00166         $url_hash = Auth_OpenID_FileStore::_safe64($server_url);
00167         if ($handle) {
00168             $handle_hash = Auth_OpenID_FileStore::_safe64($handle);
00169         } else {
00170             $handle_hash = '';
00171         }
00172 
00173         $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash,
00174                             $handle_hash);
00175 
00176         return $this->association_dir. DIRECTORY_SEPARATOR . $filename;
00177     }
00178 
00179     /**
00180      * Store an association in the association directory.
00181      */
00182     function storeAssociation($server_url, $association)
00183     {
00184         if (!$this->active) {
00185             trigger_error("FileStore no longer active", E_USER_ERROR);
00186             return false;
00187         }
00188 
00189         $association_s = $association->serialize();
00190         $filename = $this->getAssociationFilename($server_url,
00191                                                   $association->handle);
00192         list($tmp_file, $tmp) = $this->_mktemp();
00193 
00194         if (!$tmp_file) {
00195             trigger_error("_mktemp didn't return a valid file descriptor",
00196                           E_USER_WARNING);
00197             return false;
00198         }
00199 
00200         fwrite($tmp_file, $association_s);
00201 
00202         fflush($tmp_file);
00203 
00204         fclose($tmp_file);
00205 
00206         if (@rename($tmp, $filename)) {
00207             return true;
00208         } else {
00209             // In case we are running on Windows, try unlinking the
00210             // file in case it exists.
00211             @unlink($filename);
00212 
00213             // Now the target should not exist. Try renaming again,
00214             // giving up if it fails.
00215             if (@rename($tmp, $filename)) {
00216                 return true;
00217             }
00218         }
00219 
00220         // If there was an error, don't leave the temporary file
00221         // around.
00222         Auth_OpenID_FileStore::_removeIfPresent($tmp);
00223         return false;
00224     }
00225 
00226     /**
00227      * Retrieve an association. If no handle is specified, return the
00228      * association with the most recent issue time.
00229      *
00230      * @return mixed $association
00231      */
00232     function getAssociation($server_url, $handle = null)
00233     {
00234         if (!$this->active) {
00235             trigger_error("FileStore no longer active", E_USER_ERROR);
00236             return null;
00237         }
00238 
00239         if ($handle === null) {
00240             $handle = '';
00241         }
00242 
00243         // The filename with the empty handle is a prefix of all other
00244         // associations for the given server URL.
00245         $filename = $this->getAssociationFilename($server_url, $handle);
00246 
00247         if ($handle) {
00248             return $this->_getAssociation($filename);
00249         } else {
00250             $association_files =
00251                 Auth_OpenID_FileStore::_listdir($this->association_dir);
00252             $matching_files = array();
00253 
00254             // strip off the path to do the comparison
00255             $name = basename($filename);
00256             foreach ($association_files as $association_file) {
00257                 $base = basename($association_file);
00258                 if (strpos($base, $name) === 0) {
00259                     $matching_files[] = $association_file;
00260                 }
00261             }
00262 
00263             $matching_associations = array();
00264             // read the matching files and sort by time issued
00265             foreach ($matching_files as $full_name) {
00266                 $association = $this->_getAssociation($full_name);
00267                 if ($association !== null) {
00268                     $matching_associations[] = array($association->issued,
00269                                                      $association);
00270                 }
00271             }
00272 
00273             $issued = array();
00274             $assocs = array();
00275             foreach ($matching_associations as $key => $assoc) {
00276                 $issued[$key] = $assoc[0];
00277                 $assocs[$key] = $assoc[1];
00278             }
00279 
00280             array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
00281                             $matching_associations);
00282 
00283             // return the most recently issued one.
00284             if ($matching_associations) {
00285                 list($issued, $assoc) = $matching_associations[0];
00286                 return $assoc;
00287             } else {
00288                 return null;
00289             }
00290         }
00291     }
00292 
00293     /**
00294      * @access private
00295      */
00296     function _getAssociation($filename)
00297     {
00298         if (!$this->active) {
00299             trigger_error("FileStore no longer active", E_USER_ERROR);
00300             return null;
00301         }
00302 
00303         $assoc_file = @fopen($filename, 'rb');
00304 
00305         if ($assoc_file === false) {
00306             return null;
00307         }
00308 
00309         $assoc_s = fread($assoc_file, filesize($filename));
00310         fclose($assoc_file);
00311 
00312         if (!$assoc_s) {
00313             return null;
00314         }
00315 
00316         $association =
00317             Auth_OpenID_Association::deserialize('Auth_OpenID_Association',
00318                                                 $assoc_s);
00319 
00320         if (!$association) {
00321             Auth_OpenID_FileStore::_removeIfPresent($filename);
00322             return null;
00323         }
00324 
00325         if ($association->getExpiresIn() == 0) {
00326             Auth_OpenID_FileStore::_removeIfPresent($filename);
00327             return null;
00328         } else {
00329             return $association;
00330         }
00331     }
00332 
00333     /**
00334      * Remove an association if it exists. Do nothing if it does not.
00335      *
00336      * @return bool $success
00337      */
00338     function removeAssociation($server_url, $handle)
00339     {
00340         if (!$this->active) {
00341             trigger_error("FileStore no longer active", E_USER_ERROR);
00342             return null;
00343         }
00344 
00345         $assoc = $this->getAssociation($server_url, $handle);
00346         if ($assoc === null) {
00347             return false;
00348         } else {
00349             $filename = $this->getAssociationFilename($server_url, $handle);
00350             return Auth_OpenID_FileStore::_removeIfPresent($filename);
00351         }
00352     }
00353 
00354     /**
00355      * Return whether this nonce is present. As a side effect, mark it
00356      * as no longer present.
00357      *
00358      * @return bool $present
00359      */
00360     function useNonce($server_url, $timestamp, $salt)
00361     {
00362         global $Auth_OpenID_SKEW;
00363 
00364         if (!$this->active) {
00365             trigger_error("FileStore no longer active", E_USER_ERROR);
00366             return null;
00367         }
00368 
00369         if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
00370             return False;
00371         }
00372 
00373         if ($server_url) {
00374             list($proto, $rest) = explode('://', $server_url, 2);
00375         } else {
00376             $proto = '';
00377             $rest = '';
00378         }
00379 
00380         $parts = explode('/', $rest, 2);
00381         $domain = $this->_filenameEscape($parts[0]);
00382         $url_hash = $this->_safe64($server_url);
00383         $salt_hash = $this->_safe64($salt);
00384 
00385         $filename = sprintf('%08x-%s-%s-%s-%s', $timestamp, $proto,
00386                             $domain, $url_hash, $salt_hash);
00387         $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $filename;
00388 
00389         $result = @fopen($filename, 'x');
00390 
00391         if ($result === false) {
00392             return false;
00393         } else {
00394             fclose($result);
00395             return true;
00396         }
00397     }
00398 
00399     /**
00400      * Remove expired entries from the database. This is potentially
00401      * expensive, so only run when it is acceptable to take time.
00402      *
00403      * @access private
00404      */
00405     function _allAssocs()
00406     {
00407         $all_associations = array();
00408 
00409         $association_filenames =
00410             Auth_OpenID_FileStore::_listdir($this->association_dir);
00411 
00412         foreach ($association_filenames as $association_filename) {
00413             $association_file = fopen($association_filename, 'rb');
00414 
00415             if ($association_file !== false) {
00416                 $assoc_s = fread($association_file,
00417                                  filesize($association_filename));
00418                 fclose($association_file);
00419 
00420                 // Remove expired or corrupted associations
00421                 $association =
00422                   Auth_OpenID_Association::deserialize(
00423                          'Auth_OpenID_Association', $assoc_s);
00424 
00425                 if ($association === null) {
00426                     Auth_OpenID_FileStore::_removeIfPresent(
00427                                                  $association_filename);
00428                 } else {
00429                     if ($association->getExpiresIn() == 0) {
00430                         $all_associations[] = array($association_filename,
00431                                                     $association);
00432                     }
00433                 }
00434             }
00435         }
00436 
00437         return $all_associations;
00438     }
00439 
00440     function clean()
00441     {
00442         if (!$this->active) {
00443             trigger_error("FileStore no longer active", E_USER_ERROR);
00444             return null;
00445         }
00446 
00447         $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
00448         $now = time();
00449 
00450         // Check all nonces for expiry
00451         foreach ($nonces as $nonce) {
00452             if (!Auth_OpenID_checkTimestamp($nonce, $now)) {
00453                 $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
00454                 Auth_OpenID_FileStore::_removeIfPresent($filename);
00455             }
00456         }
00457 
00458         foreach ($this->_allAssocs() as $pair) {
00459             list($assoc_filename, $assoc) = $pair;
00460             if ($assoc->getExpiresIn() == 0) {
00461                 Auth_OpenID_FileStore::_removeIfPresent($assoc_filename);
00462             }
00463         }
00464     }
00465 
00466     /**
00467      * @access private
00468      */
00469     function _rmtree($dir)
00470     {
00471         if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) {
00472             $dir .= DIRECTORY_SEPARATOR;
00473         }
00474 
00475         if ($handle = opendir($dir)) {
00476             while ($item = readdir($handle)) {
00477                 if (!in_array($item, array('.', '..'))) {
00478                     if (is_dir($dir . $item)) {
00479 
00480                         if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) {
00481                             return false;
00482                         }
00483                     } else if (is_file($dir . $item)) {
00484                         if (!unlink($dir . $item)) {
00485                             return false;
00486                         }
00487                     }
00488                 }
00489             }
00490 
00491             closedir($handle);
00492 
00493             if (!@rmdir($dir)) {
00494                 return false;
00495             }
00496 
00497             return true;
00498         } else {
00499             // Couldn't open directory.
00500             return false;
00501         }
00502     }
00503 
00504     /**
00505      * @access private
00506      */
00507     function _mkstemp($dir)
00508     {
00509         foreach (range(0, 4) as $i) {
00510             $name = tempnam($dir, "php_openid_filestore_");
00511 
00512             if ($name !== false) {
00513                 return $name;
00514             }
00515         }
00516         return false;
00517     }
00518 
00519     /**
00520      * @access private
00521      */
00522     function _mkdtemp($dir)
00523     {
00524         foreach (range(0, 4) as $i) {
00525             $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) .
00526                 "-" . strval(rand(1, time()));
00527             if (!mkdir($name, 0700)) {
00528                 return false;
00529             } else {
00530                 return $name;
00531             }
00532         }
00533         return false;
00534     }
00535 
00536     /**
00537      * @access private
00538      */
00539     function _listdir($dir)
00540     {
00541         $handle = opendir($dir);
00542         $files = array();
00543         while (false !== ($filename = readdir($handle))) {
00544             if (!in_array($filename, array('.', '..'))) {
00545                 $files[] = $dir . DIRECTORY_SEPARATOR . $filename;
00546             }
00547         }
00548         return $files;
00549     }
00550 
00551     /**
00552      * @access private
00553      */
00554     function _isFilenameSafe($char)
00555     {
00556         $_Auth_OpenID_filename_allowed = Auth_OpenID_letters .
00557             Auth_OpenID_digits . ".";
00558         return (strpos($_Auth_OpenID_filename_allowed, $char) !== false);
00559     }
00560 
00561     /**
00562      * @access private
00563      */
00564     function _safe64($str)
00565     {
00566         $h64 = base64_encode(Auth_OpenID_SHA1($str));
00567         $h64 = str_replace('+', '_', $h64);
00568         $h64 = str_replace('/', '.', $h64);
00569         $h64 = str_replace('=', '', $h64);
00570         return $h64;
00571     }
00572 
00573     /**
00574      * @access private
00575      */
00576     function _filenameEscape($str)
00577     {
00578         $filename = "";
00579         $b = Auth_OpenID::toBytes($str);
00580 
00581         for ($i = 0; $i < count($b); $i++) {
00582             $c = $b[$i];
00583             if (Auth_OpenID_FileStore::_isFilenameSafe($c)) {
00584                 $filename .= $c;
00585             } else {
00586                 $filename .= sprintf("_%02X", ord($c));
00587             }
00588         }
00589         return $filename;
00590     }
00591 
00592     /**
00593      * Attempt to remove a file, returning whether the file existed at
00594      * the time of the call.
00595      *
00596      * @access private
00597      * @return bool $result True if the file was present, false if not.
00598      */
00599     function _removeIfPresent($filename)
00600     {
00601         return @unlink($filename);
00602     }
00603 
00604     function cleanupAssociations()
00605     {
00606         $removed = 0;
00607         foreach ($this->_allAssocs() as $pair) {
00608             list($assoc_filename, $assoc) = $pair;
00609             if ($assoc->getExpiresIn() == 0) {
00610                 $this->_removeIfPresent($assoc_filename);
00611                 $removed += 1;
00612             }
00613         }
00614         return $removed;
00615     }
00616 }
00617 
00618 ?>