eZ Publish  [trunk]
mysqli.php
Go to the documentation of this file.
00001 <?php
00002 /**
00003  * File containing the eZDBFileHandlerMysqlBackend class.
00004  *
00005  * @copyright Copyright (C) 1999-2012 eZ Systems AS. All rights reserved.
00006  * @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2
00007  * @version //autogentag//
00008  * @package kernel
00009  */
00010 
00011 if ( !defined( 'TABLE_METADATA' ) )
00012     define( 'TABLE_METADATA', 'ezdbfile' );
00013 
00014 if ( !defined( 'TABLE_DATA' ) )
00015     define( 'TABLE_DATA', 'ezdbfile_data' );
00016 
00017 /*
00018 CREATE TABLE ezdbfile (
00019   datatype      VARCHAR(255)   NOT NULL DEFAULT 'application/octet-stream',
00020   name          TEXT          NOT NULL,
00021   name_trunk    TEXT          NOT NULL,
00022   name_hash     VARCHAR(34)   NOT NULL DEFAULT '',
00023   scope         VARCHAR(20)   NOT NULL DEFAULT '',
00024   size          BIGINT(20)    UNSIGNED NOT NULL DEFAULT '0',
00025   mtime         INT(11)       NOT NULL DEFAULT '0',
00026   expired       BOOL          NOT NULL DEFAULT '0',
00027   PRIMARY KEY (name_hash),
00028   INDEX ezdbfile_name (name(250)),
00029   INDEX ezdbfile_name_trunk (name_trunk(250)),
00030   INDEX ezdbfile_mtime (mtime),
00031   INDEX ezdbfile_expired_name (expired, name(250))
00032 ) ENGINE=InnoDB;
00033 
00034 
00035 CREATE TABLE ezdbfile_data (
00036   name_hash VARCHAR(34)   NOT NULL DEFAULT '',
00037   offset    INT(11) UNSIGNED NOT NULL,
00038   filedata  BLOB          NOT NULL,
00039   PRIMARY KEY (name_hash, offset),
00040   CONSTRAINT ezdbfile_fk1 FOREIGN KEY (name_hash) REFERENCES ezdbfile (name_hash) ON DELETE CASCADE
00041 ) ENGINE=InnoDB;
00042  */
00043 
00044 class eZDBFileHandlerMysqliBackend
00045 {
00046     /// @todo check when parent does use $newLink param
00047     function _connect( /*$newLink = false*/ )
00048     {
00049         $siteINI = eZINI::instance( 'site.ini' );
00050         if ( !isset( $GLOBALS['eZDBFileHandlerMysqlBackend_dbparams'] ) )
00051         {
00052             $fileINI = eZINI::instance( 'file.ini' );
00053 
00054             $params['host']       = $fileINI->variable( 'ClusteringSettings', 'DBHost' );
00055             $params['port']       = $fileINI->variable( 'ClusteringSettings', 'DBPort' );
00056             $params['socket']     = $fileINI->variable( 'ClusteringSettings', 'DBSocket' );
00057             $params['dbname']     = $fileINI->variable( 'ClusteringSettings', 'DBName' );
00058             $params['user']       = $fileINI->variable( 'ClusteringSettings', 'DBUser' );
00059             $params['pass']       = $fileINI->variable( 'ClusteringSettings', 'DBPassword' );
00060             $params['chunk_size'] = $fileINI->variable( 'ClusteringSettings', 'DBChunkSize' );
00061 
00062             $params['max_connect_tries'] = $fileINI->variable( 'ClusteringSettings', 'DBConnectRetries' );
00063             $params['max_execute_tries'] = $fileINI->variable( 'ClusteringSettings', 'DBExecuteRetries' );
00064 
00065             $params['sql_output'] = $siteINI->variable( "DatabaseSettings", "SQLOutput" ) == "enabled";
00066 
00067             $params['cache_generation_timeout'] = $siteINI->variable( "ContentSettings", "CacheGenerationTimeout" );
00068 
00069             $GLOBALS['eZDBFileHandlerMysqlBackend_dbparams'] = $params;
00070         }
00071         else
00072             $params = $GLOBALS['eZDBFileHandlerMysqlBackend_dbparams'];
00073         $this->dbparams = $params;
00074 
00075         $maxTries = $params['max_connect_tries'];
00076         $tries = 0;
00077         eZDebug::accumulatorStart( 'mysql_cluster_connect', 'mysql_cluster_total', 'Cluster_database_connection' );
00078         while ( $tries < $maxTries )
00079         {
00080             /// @todo what if port is null, '' ??? to be tested
00081             if ( $this->db = mysqli_connect( $params['host'], $params['user'], $params['pass'], $params['dbname'], $params['port'] ) )
00082                 break;
00083             ++$tries;
00084         }
00085         eZDebug::accumulatorStop( 'mysql_cluster_connect' );
00086         if ( !$this->db )
00087             return $this->_die( "Unable to connect to storage server" );
00088 
00089         $charset = trim( $siteINI->variable( 'DatabaseSettings', 'Charset' ) );
00090         if ( $charset === '' )
00091         {
00092             $charset = eZTextCodec::internalCharset();
00093         }
00094 
00095         if ( $charset )
00096         {
00097             if ( !mysqli_set_charset( $this->db, eZMySQLCharset::mapTo( $charset ) ) )
00098             {
00099                 return $this->_die( "Failed to set Database charset to $charset." );
00100             }
00101         }
00102     }
00103 
00104     /**
00105      * Disconnects the handler from the database
00106      */
00107     public function _disconnect()
00108     {
00109         if ( $this->db !== null )
00110         {
00111             mysqli_close( $this->db );
00112             $this->db = null;
00113         }
00114     }
00115 
00116     function _copy( $srcFilePath, $dstFilePath, $fname = false )
00117     {
00118         if ( $fname )
00119             $fname .= "::_copy($srcFilePath, $dstFilePath)";
00120         else
00121             $fname = "_copy($srcFilePath, $dstFilePath)";
00122 
00123         // fetch source file metadata
00124         $metaData = $this->_fetchMetadata( $srcFilePath, $fname );
00125         if ( !$metaData ) // if source file does not exist then do nothing.
00126             return false;
00127         return $this->_protect( array( $this, "_copyInner" ), $fname,
00128                                 $srcFilePath, $dstFilePath, $fname, $metaData );
00129     }
00130 
00131     function _copyInner( $srcFilePath, $dstFilePath, $fname, $metaData )
00132     {
00133         $this->_delete( $dstFilePath, true, $fname );
00134 
00135         $datatype        = $metaData['datatype'];
00136         $filePathHash    = md5( $dstFilePath );
00137         $scope           = $metaData['scope'];
00138         $contentLength   = $metaData['size'];
00139         $fileMTime       = $metaData['mtime'];
00140         $nameTrunk       = self::nameTrunk( $dstFilePath, $scope );
00141 
00142         // Copy file metadata.
00143         if ( $this->_insertUpdate( TABLE_METADATA,
00144                                    array( 'datatype'=> $datatype,
00145                                           'name' => $dstFilePath,
00146                                           'name_trunk' => $nameTrunk,
00147                                           'name_hash' => $filePathHash,
00148                                           'scope' => $scope,
00149                                           'size' => $contentLength,
00150                                           'mtime' => $fileMTime,
00151                                           'expired' => ($fileMTime < 0) ? 1 : 0 ),
00152                                    "datatype=VALUES(datatype), scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00153                                    $fname ) === false )
00154         {
00155             return $this->_fail( $srcFilePath, "Failed to insert file metadata on copying." );
00156         }
00157 
00158         // Copy file data.
00159 
00160         $sql = "SELECT filedata, offset FROM " . TABLE_DATA . " WHERE name_hash=" . $this->_md5( $srcFilePath ) . " ORDER BY offset";
00161         if ( !$res = $this->_query( $sql, $fname ) )
00162         {
00163             eZDebug::writeError( "Failed to fetch source file '$srcFilePath' data on copying.", __METHOD__ );
00164             return false;
00165         }
00166 
00167         $offset = 0;
00168         while ( $row = mysqli_fetch_row( $res ) )
00169         {
00170             // make the data mysql insert safe.
00171             $binarydata = $row[0];
00172             $expectedOffset = $row[1];
00173             if ( $expectedOffset != $offset )
00174             {
00175                 eZDebug::writeError( "The fetched offset value '$expectedOffset' does not match the computed one for the file '$srcFilePath', aborting copy.",
00176                                      __METHOD__ );
00177                 return false;
00178             }
00179 
00180             if ( $this->_insertUpdate( TABLE_DATA,
00181                                        array( 'name_hash' => $filePathHash,
00182                                               'offset' => $offset,
00183                                               'filedata' => $binarydata ),
00184                                        "filedata=VALUES(filedata)",
00185                                        $fname ) === false )
00186             {
00187                 return $this->_fail( "Failed to insert data row while copying file." );
00188             }
00189             $offset += strlen( $binarydata );
00190         }
00191         mysqli_free_result( $res );
00192         if ( $offset != $contentLength )
00193         {
00194             eZDebug::writeError( "The size of the fetched data '$offset' does not match the expected size '$contentLength' for the file '$srcFilePath', aborting copy.",
00195                                  __METHOD__ );
00196             return false;
00197         }
00198 
00199         // Get rid of unused/old offset data.
00200         $result = $this->_cleanupFiledata( $dstFilePath, $contentLength, $fname );
00201         if ( $this->_isFailure( $result ) )
00202             return $result;
00203 
00204         return true;
00205     }
00206 
00207     /*!
00208      Purges meta-data and file-data for the file entry named $filePath from the database.
00209      */
00210     function _purge( $filePath, $onlyExpired = false, $expiry = false, $fname = false )
00211     {
00212         if ( $fname )
00213             $fname .= "::_purge($filePath)";
00214         else
00215             $fname = "_purge($filePath)";
00216         $sql = "DELETE FROM " . TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath );
00217         if ( $expiry !== false )
00218             $sql .= " AND mtime < " . (int)$expiry;
00219         elseif ( $onlyExpired )
00220             $sql .= " AND expired = 1";
00221         if ( !$this->_query( $sql, $fname ) )
00222             return $this->_fail( "Purging file metadata for $filePath failed" );
00223         return true;
00224     }
00225 
00226     /*!
00227      Purges meta-data and file-data for the matching files.
00228      Matching is done by passing the string $like to the LIKE statement in the SQL.
00229      */
00230     function _purgeByLike( $like, $onlyExpired = false, $limit = 50, $expiry = false, $fname = false )
00231     {
00232         if ( $fname )
00233             $fname .= "::_purgeByLike($like, $onlyExpired)";
00234         else
00235             $fname = "_purgeByLike($like, $onlyExpired)";
00236         $sql = "DELETE FROM " . TABLE_METADATA . " WHERE name LIKE " . $this->_quote( $like, true );
00237         if ( $expiry !== false )
00238             $sql .= " AND mtime < " . (int)$expiry;
00239         elseif ( $onlyExpired )
00240             $sql .= " AND expired = 1";
00241         if ( $limit )
00242             $sql .= " LIMIT $limit";
00243         if ( !$this->_query( $sql, $fname ) )
00244             return $this->_fail( "Purging file metadata by like statement $like failed" );
00245         return mysqli_affected_rows( $this->db );
00246     }
00247 
00248     function _delete( $filePath, $insideOfTransaction = false, $fname = false )
00249     {
00250         if ( $fname )
00251             $fname .= "::_delete($filePath)";
00252         else
00253             $fname = "_delete($filePath)";
00254         if ( $insideOfTransaction )
00255         {
00256             $res = $this->_deleteInner( $filePath, $fname );
00257             if ( !$res || $res instanceof eZMySQLBackendError )
00258             {
00259                 $this->_handleErrorType( $res );
00260             }
00261         }
00262         else
00263             return $this->_protect( array( $this, '_deleteInner' ), $fname,
00264                                     $filePath, $insideOfTransaction, $fname );
00265     }
00266 
00267     function _deleteInner( $filePath, $fname )
00268     {
00269         if ( !$this->_query( "UPDATE " . TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1 WHERE name_hash=" . $this->_md5( $filePath ), $fname ) )
00270             return $this->_fail( "Deleting file $filePath failed" );
00271         return true;
00272     }
00273 
00274     function _deleteByLike( $like, $fname = false )
00275     {
00276         if ( $fname )
00277             $fname .= "::_deleteByLike($like)";
00278         else
00279             $fname = "_deleteByLike($like)";
00280         return $this->_protect( array( $this, '_deleteByLikeInner' ), $fname,
00281                                 $like, $fname );
00282     }
00283 
00284     function _deleteByLikeInner( $like, $fname )
00285     {
00286         $sql = "UPDATE " . TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name like ". $this->_quote( $like, true );
00287         if ( !$res = $this->_query( $sql, $fname ) )
00288         {
00289             return $this->_fail( "Failed to delete files by like: '$like'" );
00290         }
00291         return true;
00292     }
00293 
00294     function _deleteByRegex( $regex, $fname = false )
00295     {
00296         if ( $fname )
00297             $fname .= "::_deleteByRegex($regex)";
00298         else
00299             $fname = "_deleteByRegex($regex)";
00300         return $this->_protect( array( $this, '_deleteByRegexInner' ), $fname,
00301                                 $regex, $fname );
00302     }
00303 
00304     function _deleteByRegexInner( $regex, $fname )
00305     {
00306         $sql = "UPDATE " . TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name REGEXP " . $this->_quote( $regex );
00307         if ( !$res = $this->_query( $sql, $fname ) )
00308         {
00309             return $this->_fail( "Failed to delete files by regex: '$regex'" );
00310         }
00311         return true;
00312     }
00313 
00314     function _deleteByWildcard( $wildcard, $fname = false )
00315     {
00316         if ( $fname )
00317             $fname .= "::_deleteByWildcard($wildcard)";
00318         else
00319             $fname = "_deleteByWildcard($wildcard)";
00320         return $this->_protect( array( $this, '_deleteByWildcardInner' ), $fname,
00321                                 $wildcard, $fname );
00322     }
00323 
00324     function _deleteByWildcardInner( $wildcard, $fname )
00325     {
00326         // Convert wildcard to regexp.
00327         $regex = '^' . mysqli_real_escape_string( $this->db, $wildcard ) . '$';
00328 
00329         $regex = str_replace( array( '.'  ),
00330                               array( '\.' ),
00331                               $regex );
00332 
00333         $regex = str_replace( array( '?', '*',  '{', '}', ',' ),
00334                               array( '.', '.*', '(', ')', '|' ),
00335                               $regex );
00336 
00337         $sql = "UPDATE " . TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name REGEXP '$regex'";
00338         if ( !$res = $this->_query( $sql, $fname ) )
00339         {
00340             return $this->_fail( "Failed to delete files by wildcard: '$wildcard'" );
00341         }
00342         return true;
00343     }
00344 
00345     function _deleteByDirList( $dirList, $commonPath, $commonSuffix, $fname = false )
00346     {
00347         if ( $fname )
00348             $fname .= "::_deleteByDirList(" . join( ", ", $dirList ) . ", $commonPath, $commonSuffix)";
00349         else
00350             $fname = "_deleteByDirList(" . join( ", ", $dirList ) . ", $commonPath, $commonSuffix)";
00351         return $this->_protect( array( $this, '_deleteByDirListInner' ), $fname,
00352                                 $dirList, $commonPath, $commonSuffix, $fname );
00353     }
00354 
00355     function _deleteByDirListInner( $dirList, $commonPath, $commonSuffix, $fname )
00356     {
00357         foreach ( $dirList as $dirItem )
00358         {
00359             if ( strstr( $commonPath, '/cache/content' ) !== false or strstr( $commonPath, '/cache/template-block' ) !== false )
00360             {
00361                 $where = "WHERE name_trunk = '$commonPath/$dirItem/$commonSuffix'";
00362             }
00363             else
00364             {
00365                 $where = "WHERE name LIKE ".$this->_quote( "$commonPath/$dirItem/$commonSuffix%", true );
00366             }
00367             $sql = "UPDATE " . TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\n$where";
00368             if ( !$res = $this->_query( $sql, $fname ) )
00369             {
00370                 eZDebug::writeError( "Failed to delete files in dir: '$commonPath/$dirItem/$commonSuffix%'", __METHOD__ );
00371             }
00372         }
00373         return true;
00374     }
00375 
00376 
00377     function _exists( $filePath, $fname = false, $ignoreExpiredFiles = true )
00378     {
00379         if ( $fname )
00380             $fname .= "::_exists($filePath)";
00381         else
00382             $fname = "_exists($filePath)";
00383         $row = $this->_selectOneRow( "SELECT name, mtime FROM " . TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath ),
00384                                      $fname, "Failed to check file '$filePath' existance: ", true );
00385         if ( $row === false )
00386             return false;
00387 
00388         if ( $ignoreExpiredFiles )
00389             $rc = $row[1] >= 0;
00390         else
00391             $rc = true;
00392 
00393         return $rc;
00394     }
00395 
00396     function __mkdir_p( $dir )
00397     {
00398         // create parent directories
00399         $dirElements = explode( '/', $dir );
00400         if ( count( $dirElements ) == 0 )
00401             return true;
00402 
00403         $result = true;
00404         $currentDir = $dirElements[0];
00405 
00406         if ( $currentDir != '' && !file_exists( $currentDir ) && !eZDir::mkdir( $currentDir, false ))
00407             return false;
00408 
00409         for ( $i = 1; $i < count( $dirElements ); ++$i )
00410         {
00411             $dirElement = $dirElements[$i];
00412             if ( strlen( $dirElement ) == 0 )
00413                 continue;
00414 
00415             $currentDir .= '/' . $dirElement;
00416 
00417             if ( !file_exists( $currentDir ) && !eZDir::mkdir( $currentDir, false ) )
00418                 return false;
00419 
00420             $result = true;
00421         }
00422 
00423         return $result;
00424     }
00425 
00426     /**
00427      * Fetches the file $filePath from the database, saving it locally with its
00428      * original name, or $uniqueName if given
00429      *
00430      * @param string $filePath
00431      * @param string $uniqueName
00432      * @return the file physical path, or false if fetch failed
00433      */
00434     function _fetch( $filePath, $uniqueName = false )
00435     {
00436         $metaData = $this->_fetchMetadata( $filePath );
00437         if ( !$metaData )
00438         {
00439             eZDebug::writeError( "File '$filePath' does not exist while trying to fetch.", __METHOD__ );
00440             return false;
00441         }
00442         $contentLength = $metaData['size'];
00443 
00444         $sql = "SELECT filedata, offset FROM " . TABLE_DATA . " WHERE name_hash=" . $this->_md5( $filePath ) . " ORDER BY offset";
00445         if ( !$res = $this->_query( $sql, "_fetch($filePath)" ) )
00446         {
00447             eZDebug::writeError( "Failed to fetch file data for file '$filePath'.", __METHOD__ );
00448             return false;
00449         }
00450 
00451         if( !mysqli_num_rows( $res ) )
00452         {
00453             eZDebug::writeError( "No rows in file '$filePath' being fetched.", __METHOD__ );
00454             mysqli_free_result( $res );
00455             return false;
00456         }
00457 
00458         // create temporary file
00459         if ( strrpos( $filePath, '.' ) > 0 )
00460             $tmpFilePath = substr_replace( $filePath, getmypid().'tmp', strrpos( $filePath, '.' ), 0  );
00461         else
00462             $tmpFilePath = $filePath . '.' . getmypid().'tmp';
00463         $this->__mkdir_p( dirname( $tmpFilePath ) );
00464 
00465         if ( !( $fp = fopen( $tmpFilePath, 'wb' ) ) )
00466         {
00467             eZDebug::writeError( "Cannot write to '$tmpFilePath' while fetching file.", __METHOD__ );
00468             return false;
00469         }
00470 
00471         $offset = 0;
00472         while ( $row = mysqli_fetch_row( $res ) )
00473         {
00474             $expectedOffset = $row[1];
00475             if ( $expectedOffset != $offset )
00476             {
00477                 eZDebug::writeError( "The fetched offset value '$expectedOffset' does not match the computed one for the file '$filePath', aborting fetch.", __METHOD__ );
00478                 fclose( $fp );
00479                 @unlink( $filePath );
00480                 return false;
00481             }
00482             fwrite( $fp, $row[0] );
00483             $offset += strlen( $row[0] );
00484         }
00485         if ( $offset != $contentLength )
00486         {
00487             mysqli_free_result( $res );
00488             eZDebug::writeError( "The size of the fetched data '$offset' does not match the expected size '$contentLength' for the file '$filePath', aborting fetch.", __METHOD__ );
00489             fclose( $fp );
00490             @unlink( $filePath );
00491             return false;
00492         }
00493 
00494         fclose( $fp );
00495         mysqli_free_result( $res );
00496 
00497         // Make sure all data is written correctly
00498         clearstatcache();
00499         $tmpSize = filesize( $tmpFilePath );
00500         if ( $tmpSize != $metaData['size'] )
00501         {
00502             eZDebug::writeError( "Size ($tmpSize) of written data for file '$tmpFilePath' does not match expected size " . $metaData['size'], __METHOD__ );
00503             return false;
00504         }
00505 
00506         if ( ! $uniqueName === true )
00507         {
00508             eZFile::rename( $tmpFilePath, $filePath, false, eZFile::CLEAN_ON_FAILURE | eZFile::APPEND_DEBUG_ON_FAILURE );
00509         }
00510         else
00511         {
00512             $filePath = $tmpFilePath;
00513         }
00514 
00515         return $filePath;
00516     }
00517 
00518     function _fetchContents( $filePath, $fname = false )
00519     {
00520         if ( $fname )
00521             $fname .= "::_fetchContents($filePath)";
00522         else
00523             $fname = "_fetchContents($filePath)";
00524         $metaData = $this->_fetchMetadata( $filePath, $fname );
00525         if ( !$metaData )
00526         {
00527             eZDebug::writeError( "File '$filePath' does not exist while trying to fetch its contents.", __METHOD__ );
00528             return false;
00529         }
00530         $contentLength = $metaData['size'];
00531 
00532 //        $fileID = $metaData['id'];
00533         $sql = "SELECT filedata, offset FROM " . TABLE_DATA . " WHERE name_hash=" . $this->_md5( $filePath ) . " ORDER BY offset";
00534         if ( !$res = $this->_query( $sql, $fname ) )
00535         {
00536             eZDebug::writeError( "Failed to fetch file data for the file '$filePath'.", __METHOD__ );
00537             return false;
00538         }
00539 
00540         $contents = '';
00541         $offset   = 0;
00542         while ( $row = mysqli_fetch_row( $res ) )
00543         {
00544             $expectedOffset = $row[1];
00545             if ( $expectedOffset != $offset )
00546             {
00547                 mysqli_free_result( $res );
00548                 eZDebug::writeError( "The fetched offset value '$expectedOffset' does not match the computed one for the file '$filePath', aborting.", __METHOD__ );
00549                 return false;
00550             }
00551             $contents .= $row[0];
00552             $offset += strlen( $row[0] );
00553         }
00554         mysqli_free_result( $res );
00555         if ( $offset != $contentLength )
00556         {
00557             eZDebug::writeError( "The size of the fetched data '$offset' does not match the expected size '$contentLength' for the file '$filePath', aborting.", __METHOD__ );
00558             return false;
00559         }
00560 
00561         return $contents;
00562     }
00563 
00564     /**
00565      * \return file metadata, or false if the file does not exist in database.
00566      */
00567     function _fetchMetadata( $filePath, $fname = false )
00568     {
00569         if ( $fname )
00570             $fname .= "::_fetchMetadata($filePath)";
00571         else
00572             $fname = "_fetchMetadata($filePath)";
00573         $sql = "SELECT * FROM " . TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath );
00574         return $this->_selectOneAssoc( $sql, $fname,
00575                                        "Failed to retrieve file metadata: $filePath",
00576                                        true );
00577     }
00578 
00579     function _linkCopy( $srcPath, $dstPath, $fname = false )
00580     {
00581         if ( $fname )
00582             $fname .= "::_linkCopy($srcPath,$dstPath)";
00583         else
00584             $fname = "_linkCopy($srcPath,$dstPath)";
00585         return $this->_copy( $srcPath, $dstPath, $fname );
00586     }
00587 
00588     /**
00589      * Sends a binary file's content to the client
00590      *
00591      * @param string $filePath File path
00592      * @param int $startOffset Starting offset
00593      * @param false|int $length Length to transmit, false means everything
00594      * @param false|string $fname The function name that started the query
00595      */
00596     function _passThrough( $filePath, $startOffset = 0, $length = false, $fname = false )
00597     {
00598         if ( $fname )
00599             $fname .= "::_passThrough($filePath)";
00600         else
00601             $fname = "_passThrough($filePath)";
00602 
00603         $where = array();
00604         $dbChunkSize = $this->dbparams['chunk_size'];
00605         $dbStartOffset = ( $startOffset != 0 ) ? (int) ( floor( $startOffset / $dbChunkSize ) * $dbChunkSize ) : 0;
00606         if ( $dbStartOffset !== 0 )
00607         {
00608             $where[] = "offset >= {$dbStartOffset}";
00609         }
00610 
00611         if ( $length !== false )
00612         {
00613             $where[] = "offset <= " . ( $length + $startOffset - 1 );
00614             $endOffset = $length + $startOffset - 1;
00615         }
00616         else
00617         {
00618             $metaData = $this->_fetchMetadata( $filePath, $fname );
00619             if ( !$metaData )
00620             {
00621                 return false;
00622             }
00623             $endOffset = $metaData['size'] - 1;
00624             unset( $metaData );
00625         }
00626 
00627         if ( !$res =
00628             $this->_query(
00629                 "SELECT offset, filedata FROM " . TABLE_DATA . " WHERE name_hash=" . $this->_md5( $filePath ) .
00630                 ( !empty( $where ) ? " AND " . implode( " AND ", $where ) : "" ) . " " .
00631                 "ORDER BY offset",
00632                 $fname
00633             ) )
00634         {
00635             eZDebug::writeError( "Failed to fetch file data for file '$filePath'.", __METHOD__ );
00636             return false;
00637         }
00638 
00639         while ( $row = mysqli_fetch_assoc( $res ) )
00640         {
00641             // The first byte to send is part of this first chunk
00642             if ( $row['offset'] < $startOffset )
00643             {
00644                 echo substr(
00645                     $row['filedata'],
00646                     $startOffset - $row['offset'],
00647                     // we need the +1 as this is a length, not an offset
00648                     $endOffset - $startOffset + 1
00649                 );
00650             }
00651             // The last byte to send is part of this last chunk
00652             else if ( $row['offset'] + $dbChunkSize > $endOffset )
00653             {
00654                 echo substr(
00655                     $row['filedata'],
00656                     0,
00657                     // we need the +1 as this is a length, not an offset
00658                     $endOffset - $row['offset'] + 1
00659                 );
00660             }
00661             else
00662             {
00663                 echo $row['filedata'];
00664             }
00665         }
00666         mysqli_free_result( $res );
00667         return true;
00668     }
00669 
00670     function _rename( $srcFilePath, $dstFilePath )
00671     {
00672         if ( strcmp( $srcFilePath, $dstFilePath ) == 0 )
00673             return;
00674 
00675         // fetch source file metadata
00676         $metaData = $this->_fetchMetadata( $srcFilePath );
00677         if ( !$metaData ) // if source file does not exist then do nothing.
00678             return false;
00679 
00680         $this->_begin( __METHOD__ );
00681 
00682         $srcFilePathStr  = mysqli_real_escape_string( $this->db, $srcFilePath );
00683         $dstFilePathStr  = mysqli_real_escape_string( $this->db, $dstFilePath );
00684         $dstNameTrunkStr = mysqli_real_escape_string( $this->db, self::nameTrunk( $dstFilePath, $metaData['scope'] ) );
00685 
00686 //        $srcFilePathHash = mysql_real_escape_string( $metaData['name_hash'] );
00687 //        $dstFilePathHash = mysql_real_escape_string( md5( $dstFilePath ) );
00688 
00689         // Mark entry for update to lock it
00690         $sql = "SELECT * FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr') FOR UPDATE";
00691         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00692         {
00693             eZDebug::writeError( "Failed locking file '$srcFilePath'", __METHOD__ );
00694             $this->_rollback( __METHOD__ );
00695             return false;
00696         }
00697 
00698         if ( $this->_exists( $dstFilePath, false, false ) )
00699             $this->_purge( $dstFilePath, false );
00700 
00701         // Create a new meta-data entry for the new file to make foreign keys happy.
00702         $sql = "INSERT INTO " . TABLE_METADATA . " (name, name_trunk, name_hash, datatype, scope, size, mtime, expired) SELECT '$dstFilePathStr' AS name, '$dstNameTrunkStr' as name_trunk, MD5('$dstFilePathStr') AS name_hash, datatype, scope, size, mtime, expired FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr')";
00703         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00704         {
00705             eZDebug::writeError( "Failed making new file entry '$dstFilePath'", __METHOD__ );
00706             $this->_rollback( __METHOD__ );
00707             return false;
00708         }
00709 
00710         // Update data chunks to refer to the new file entry.
00711         $sql = "UPDATE " . TABLE_DATA . " SET name_hash=MD5('$dstFilePathStr') WHERE name_hash=MD5('$srcFilePathStr')";
00712         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00713         {
00714             eZDebug::writeError( "Failed renaming file '$srcFilePath' to '$dstFilePath'", __METHOD__ );
00715             $this->_rollback( __METHOD__ );
00716             return false;
00717         }
00718 
00719         // Remove old entry
00720         $sql = "DELETE FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr')";
00721         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00722         {
00723             eZDebug::writeError( "Failed removing old file '$srcFilePath'", __METHOD__ );
00724             $this->_rollback( __METHOD__ );
00725             return false;
00726         }
00727 
00728         $this->_commit( __METHOD__ );
00729 
00730         return true;
00731     }
00732 
00733     function _store( $filePath, $datatype, $scope, $fname = false )
00734     {
00735         if ( !is_readable( $filePath ) )
00736         {
00737             eZDebug::writeError( "Unable to store file '$filePath' since it is not readable.", __METHOD__ );
00738             return;
00739         }
00740         if ( $fname )
00741             $fname .= "::_store($filePath, $datatype, $scope)";
00742         else
00743             $fname = "_store($filePath, $datatype, $scope)";
00744 
00745         $this->_protect( array( $this, '_storeInner' ), $fname,
00746                          $filePath, $datatype, $scope, $fname );
00747     }
00748 
00749     function _storeInner( $filePath, $datatype, $scope, $fname )
00750     {
00751         // Insert file metadata.
00752         clearstatcache();
00753         $fileMTime = filemtime( $filePath );
00754         $contentLength = filesize( $filePath );
00755         $filePathHash = md5( $filePath );
00756         $nameTrunk = self::nameTrunk( $filePath, $scope );
00757 
00758         if ( $this->_insertUpdate( TABLE_METADATA,
00759                                    array( 'datatype' => $datatype,
00760                                           'name' => $filePath,
00761                                           'name_trunk' => $nameTrunk,
00762                                           'name_hash' => $filePathHash,
00763                                           'scope' => $scope,
00764                                           'size' => $contentLength,
00765                                           'mtime' => $fileMTime,
00766                                           'expired' => ($fileMTime < 0) ? 1 : 0 ),
00767                                    "datatype=VALUES(datatype), scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00768                                    $fname ) === false )
00769         {
00770             return $this->_fail( "Failed to insert file metadata while storing. Possible race condition" );
00771         }
00772 
00773         // Insert file contents.
00774         if ( !$fp = @fopen( $filePath, 'rb' ) )
00775         {
00776             return $this->_fail( "Cannot read '$filePath'.", __METHOD__ );
00777         }
00778 
00779         $chunkSize = $this->dbparams['chunk_size'];
00780         $offset = 0;
00781         while ( !feof( $fp ) )
00782         {
00783             // make the data mysql insert safe.
00784             $binarydata = fread( $fp, $chunkSize );
00785 
00786             if ( $this->_insertUpdate( TABLE_DATA,
00787                                        array( 'name_hash' => $filePathHash,
00788                                               'offset' => $offset,
00789                                               'filedata' => $binarydata ),
00790                                        "filedata=VALUES(filedata)",
00791                                        $fname ) === false )
00792             {
00793                 return $this->_fail( "Failed to insert file data row while storing. Possible race condition", __METHOD__ );
00794             }
00795             $offset += strlen( $binarydata );
00796         }
00797         fclose( $fp );
00798 
00799         // Get rid of unused/old offset data.
00800         $result = $this->_cleanupFiledata( $filePath, $contentLength, $fname );
00801         if ( $this->_isFailure( $result ) )
00802             return $result;
00803 
00804         return true;
00805     }
00806 
00807     function _storeContents( $filePath, $contents, $scope, $datatype, $mtime = false, $fname = false )
00808     {
00809         if ( $fname )
00810             $fname .= "::_storeContents($filePath, ..., $scope, $datatype)";
00811         else
00812             $fname = "_storeContents($filePath, ..., $scope, $datatype)";
00813 
00814         $this->_protect( array( $this, '_storeContentsInner' ), $fname,
00815                          $filePath, $contents, $scope, $datatype, $mtime, $fname );
00816     }
00817 
00818     function _storeContentsInner( $filePath, $contents, $scope, $datatype, $curTime, $fname )
00819     {
00820         // Insert file metadata.
00821         $contentLength = strlen( $contents );
00822         $filePathHash = md5( $filePath );
00823         $nameTrunk = self::nameTrunk( $filePath, $scope );
00824         if ( $curTime === false )
00825             $curTime = time();
00826 
00827         if ( $this->_insertUpdate( TABLE_METADATA,
00828                                     array( 'datatype' => $datatype,
00829                                            'name' => $filePath,
00830                                            'name_trunk' => $nameTrunk,
00831                                            'name_hash' => $filePathHash,
00832                                            'scope' => $scope,
00833                                            'size' => $contentLength,
00834                                            'mtime' => $curTime,
00835                                            'expired' => ($curTime < 0) ? 1 : 0 ),
00836                                     "datatype=VALUES(datatype), name_trunk='$nameTrunk', scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00837                                    $fname ) === false )
00838         {
00839             return $this->_fail( "Failed to insert file metadata while storing contents. Possible race condition" );
00840         }
00841 
00842         // Insert file contents.
00843         $chunkSize = $this->dbparams['chunk_size'];
00844         for ( $pos = 0; $pos < $contentLength; $pos += $chunkSize )
00845         {
00846             $chunk = substr( $contents, $pos, $chunkSize );
00847 
00848             if ( $this->_insertUpdate( TABLE_DATA,
00849                                        array( 'name_hash' => $filePathHash,
00850                                               'offset'   => $pos,
00851                                               'filedata' => $chunk ),
00852                                        "filedata=VALUES(filedata)",
00853                                        $fname ) === false )
00854             {
00855                 return $this->_fail( "Failed to insert file data row while storing contents. Possible race condition" );
00856             }
00857         }
00858 
00859         // Get rid of unused/old offset data.
00860         $result = $this->_cleanupFiledata( $filePath, $contentLength, $fname );
00861         if ( $this->_isFailure( $result ) )
00862             return $result;
00863 
00864         return true;
00865     }
00866 
00867     function _getFileList( $scopes = false, $excludeScopes = false )
00868     {
00869         $query = 'SELECT name FROM ' . TABLE_METADATA;
00870 
00871         if ( is_array( $scopes ) && count( $scopes ) > 0 )
00872         {
00873             $query .= ' WHERE scope ';
00874             if ( $excludeScopes )
00875                 $query .= 'NOT ';
00876             $query .= "IN ('" . implode( "', '", $scopes ) . "')";
00877         }
00878 
00879         $rslt = $this->_query( $query, "_getFileList( array( " . implode( ', ', $scopes ) . " ), $excludeScopes )" );
00880         if ( !$rslt )
00881         {
00882             eZDebug::writeDebug( 'Unable to get file list', __METHOD__ );
00883             return false;
00884         }
00885 
00886         $filePathList = array();
00887         while ( $row = mysqli_fetch_row( $rslt ) )
00888             $filePathList[] = $row[0];
00889 
00890         mysqli_free_result( $rslt );
00891         return $filePathList;
00892     }
00893 
00894 //////////////////////////////////////
00895 //         Helper methods
00896 //////////////////////////////////////
00897 
00898     function _die( $msg, $sql = null )
00899     {
00900         if ( $this->db )
00901         {
00902             eZDebug::writeError( $sql, "$msg" . mysqli_error( $this->db ) );
00903         }
00904         else
00905         {
00906             /// @todo to be fixed: will this generate a warning?
00907             eZDebug::writeError( $sql, "$msg: " . mysqli_error() );
00908         }
00909     }
00910 
00911     /*!
00912     Performs an insert of the given items in $array.
00913 
00914     \param $table Name of table to execute insert on.
00915     \param $array Associative array with data to insert, the keys are the field names and the values will be quoted according to type.
00916     \param $fname Name of caller.
00917      */
00918     function _insert( $table, $array, $fname )
00919     {
00920         $keys = array_keys( $array );
00921         $query = "INSERT INTO $table (" . join( ", ", $keys ) . ") VALUES (" . $this->_sqlList( $array ) . ")";
00922         $res = $this->_query( $query, $fname );
00923         if ( !$res )
00924         {
00925             return false;
00926         }
00927         return mysqli_insert_id( $this->db );
00928     }
00929 
00930     /*!
00931     Performs an insert of the given items in $array, if entry specified already exists the $update SQL is executed
00932     to update the entry.
00933 
00934     \param $table Name of table to execute insert on.
00935     \param $array Associative array with data to insert, the keys are the field names and the values will be quoted according to type.
00936     \param $update Partial update SQL which is executed when entry exists.
00937     \param $fname Name of caller.
00938      */
00939     function _insertUpdate( $table, $array, $update, $fname, $reportError = true )
00940     {
00941         $keys = array_keys( $array );
00942         $query = "INSERT INTO $table (" . join( ", ", $keys ) . ") VALUES (" . $this->_sqlList( $array ) . ")\nON DUPLICATE KEY UPDATE $update";
00943         $res = $this->_query( $query, $fname, $reportError );
00944         if ( !$res )
00945         {
00946             return false;
00947         }
00948         return mysqli_insert_id( $this->db );
00949     }
00950 
00951     /*!
00952      Formats a list of entries as an SQL list which is separated by commas.
00953      Each entry in the list is quoted using _quote().
00954      */
00955     function _sqlList( $array )
00956     {
00957         $text = "";
00958         $sep = "";
00959         foreach ( $array as $e )
00960         {
00961             $text .= $sep;
00962             $text .= $this->_quote( $e );
00963             $sep = ", ";
00964         }
00965         return $text;
00966     }
00967 
00968     /**
00969      * Common select method for doing a SELECT query which is passed in $query and fetching one row from the result.
00970      * If there are more than one row it will fail and exit, if 0 it returns false.
00971      *
00972      * @param string $fname The function name that started the query, should contain relevant arguments in the text.
00973      * @param mixed  $error Sent to _error() in case of errors
00974      * @param bool   $debug If true it will display the fetched row in addition to the SQL.
00975      *
00976      * @return array|false A numerical array, or false if 0 or more than 1 rows
00977      */
00978     function _selectOneRow( $query, $fname, $error = false, $debug = false )
00979     {
00980         return $this->_selectOne( $query, $fname, $error, $debug, "mysqli_fetch_row" );
00981     }
00982 
00983     /*!
00984      Common select method for doing a SELECT query which is passed in $query and
00985      fetching one row from the result.
00986      If there are more than one row it will fail and exit, if 0 it returns false.
00987      The returned row is an associative array.
00988 
00989      \param $fname The function name that started the query, should contain relevant arguments in the text.
00990      \param $error Sent to _error() in case of errors
00991      \param $debug If true it will display the fetched row in addition to the SQL.
00992      */
00993     function _selectOneAssoc( $query, $fname, $error = false, $debug = false )
00994     {
00995         return $this->_selectOne( $query, $fname, $error, $debug, "mysqli_fetch_assoc" );
00996     }
00997 
00998     /**
00999      * Common select method for doing a SELECT query which is passed in $query and fetching one row from the result.
01000      * If there are more than one row it will fail and exit, if 0 it returns false.
01001      *
01002      * @param string $fname The function name that started the query, should contain relevant arguments in the text.
01003      * @param mixed  $error Sent to _error() in case of errors
01004      * @param bool   $debug If true it will display the fetched row in addition to the SQL.
01005      * @param string $fetchCall The callback to fetch the row.
01006      */
01007     function _selectOne( $query, $fname, $error = false, $debug = false, $fetchCall )
01008     {
01009         eZDebug::accumulatorStart( 'mysql_cluster_query', 'mysql_cluster_total', 'Mysql_cluster_queries' );
01010         $time = microtime( true );
01011 
01012         $res = mysqli_query( $this->db, $query );
01013         if ( !$res )
01014         {
01015             $this->_error( $query, $fname, $error );
01016             eZDebug::accumulatorStop( 'mysql_cluster_query' );
01017             return false;
01018         }
01019 
01020         $this->_report( $query, $fname, microtime( true ) - $time );
01021 
01022         // we test the return value of mysqli_num_rows and not mysql_fetch, unlike in the mysql handler,
01023         // since fetch will return null and not false if there are no results
01024         $nRows = mysqli_num_rows( $res );
01025         if ( $nRows > 1 )
01026         {
01027             eZDebug::writeError( 'Duplicate entries found', $fname );
01028             eZDebug::accumulatorStop( 'mysql_cluster_query' );
01029             // @todo Throw an exception
01030             return false;
01031         }
01032         elseif ( $nRows === 0 )
01033         {
01034             eZDebug::accumulatorStop( 'mysql_cluster_query' );
01035             return false;
01036         }
01037 
01038         $row = $fetchCall( $res );
01039         mysqli_free_result( $res );
01040         if ( $debug )
01041             $query = "SQL for _selectOneAssoc:\n" . $query . "\n\nRESULT:\n" . var_export( $row, true );
01042 
01043         eZDebug::accumulatorStop( 'mysql_cluster_query' );
01044 
01045         return $row;
01046     }
01047 
01048     /*!
01049       Starts a new transaction by executing a BEGIN call.
01050       If a transaction is already started nothing is executed.
01051      */
01052     function _begin( $fname = false )
01053     {
01054         if ( $fname )
01055             $fname .= "::_begin";
01056         else
01057             $fname = "_begin";
01058         $this->transactionCount++;
01059         if ( $this->transactionCount == 1 )
01060             $this->_query( "BEGIN", $fname );
01061     }
01062 
01063     /*!
01064       Stops a current transaction and commits the changes by executing a COMMIT call.
01065       If the current transaction is a sub-transaction nothing is executed.
01066      */
01067     function _commit( $fname = false )
01068     {
01069         if ( $fname )
01070             $fname .= "::_commit";
01071         else
01072             $fname = "_commit";
01073         $this->transactionCount--;
01074         if ( $this->transactionCount == 0 )
01075             $this->_query( "COMMIT", $fname );
01076     }
01077 
01078     /*!
01079       Stops a current transaction and discards all changes by executing a ROLLBACK call.
01080       If the current transaction is a sub-transaction nothing is executed.
01081      */
01082     function _rollback( $fname = false )
01083     {
01084         if ( $fname )
01085             $fname .= "::_rollback";
01086         else
01087             $fname = "_rollback";
01088         $this->transactionCount--;
01089         if ( $this->transactionCount == 0 )
01090             $this->_query( "ROLLBACK", $fname );
01091     }
01092 
01093     /*!
01094      Frees a previously open shared-lock by performing a rollback on the current transaction.
01095 
01096      Note: There is not checking to see if a lock is started, and if
01097            locking was done in an existing transaction nothing will happen.
01098      */
01099     function _freeSharedLock( $fname = false )
01100     {
01101         if ( $fname )
01102             $fname .= "::_freeSharedLock";
01103         else
01104             $fname = "_freeSharedLock";
01105         $this->_rollback( $fname );
01106     }
01107 
01108     /*!
01109      Frees a previously open exclusive-lock by commiting the current transaction.
01110 
01111      Note: There is not checking to see if a lock is started, and if
01112            locking was done in an existing transaction nothing will happen.
01113      */
01114     function _freeExclusiveLock( $fname = false )
01115     {
01116         if ( $fname )
01117             $fname .= "::_freeExclusiveLock";
01118         else
01119             $fname = "_freeExclusiveLock";
01120         $this->_commit( $fname );
01121     }
01122 
01123     /*!
01124      Locks the file entry for exclusive write access.
01125 
01126      The locking is performed by trying to insert the entry with mtime
01127      set to -1, which means that file is not to be used. If it exists
01128      the mtime will be negated to mark it as deleted. This insert/update
01129      procedure will perform an exclusive lock of the row (InnoDB feature).
01130 
01131      Note: All reads of the row must be done with LOCK IN SHARE MODE.
01132      */
01133     function _exclusiveLock( $filePath, $fname = false )
01134     {
01135         if ( $fname )
01136             $fname .= "::_exclusiveLock($filePath)";
01137         else
01138             $fname = "_exclusiveLock($filePath)";
01139         $this->_begin( $fname );
01140         $data = array( 'name' => $filePath,
01141                        'name_hash' => md5( $filePath ),
01142                        'expired' => 1,
01143                        'mtime' => -1 ); // -1 is used to reserve this entry.
01144         $tries = 0;
01145         $maxTries = $this->dbparams['max_execute_tries'];
01146         while ( $tries < $maxTries )
01147         {
01148             $this->_insertUpdate( TABLE_METADATA,
01149                                   $data,
01150                                   "mtime=-ABS(mtime), expired=1",
01151                                   $fname,
01152                                   false ); // turn off error reporting
01153             $errno = mysqli_errno( $this->db );
01154             if ( $errno == 1205 || // Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
01155                  $errno == 1213 )  // Error: 1213 SQLSTATE: 40001 (ER_LOCK_DEADLOCK)
01156             {
01157                 $tries++;
01158                 continue;
01159             }
01160             else if ( $errno == 0 )
01161             {
01162                 return true;
01163             }
01164             break;
01165         }
01166         return $this->_fail( "Failed to perform exclusive lock on file $filePath" );
01167     }
01168 
01169     /**
01170      * Uses a secondary database connection to check outside the transaction scope
01171      * if a file has been generated during the current process execution
01172      * @param string $filePath
01173      * @param int $expiry
01174      * @param int $curtime
01175      * @param int $ttl
01176      * @param string $fname
01177      * @return bool false if the file exists and is not expired, true otherwise
01178      */
01179     function _verifyExclusiveLock( $filePath, $expiry, $curtime, $ttl, $fname = false )
01180     {
01181         // we need to create a new backend connection in order to be outside the
01182         // current transaction scope
01183         if ( $this->backendVerify === null )
01184         {
01185             $backendclass = get_class( $this );
01186             $this->backendVerify = new $backendclass( $filePath );
01187             $this->backendVerify->_connect( true );
01188         }
01189 
01190         // we then check the file metadata in this scope to see if it was created
01191         // in between
01192         $metaData = $this->backendVerify->_fetchMetadata( $filePath );
01193         if ( $metaData !== false )
01194         {
01195             if ( !eZDBFileHandler::isFileExpired( $filePath, $metaData['mtime'], max( $curtime, $expiry ), $curtime, $ttl ) )
01196             {
01197                 eZDebugSetting::writeDebug( 'kernel-clustering', "DBFile '$filePath' is valid and not expired", __METHOD__ );
01198                 return false;
01199             }
01200         }
01201         return true;
01202     }
01203 
01204     function _sharedLock( $filePath, $fname = false )
01205     {
01206         if ( $fname )
01207             $fname .= "::_sharedLock($filePath)";
01208         else
01209             $fname = "_sharedLock($filePath)";
01210         if ( $this->transactionCount == 0 )
01211             $this->_begin( $fname );
01212         $tries = 0;
01213         $maxTries = $this->dbparams['max_execute_tries'];
01214         while ( $tries < $maxTries )
01215         {
01216             $res = $this->_query( "SELECT * FROM " . TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath ) . " LOCK IN SHARE MODE", $fname, false ); // turn off error reporting
01217             $errno = mysqli_errno( $this->db );
01218             if ( $errno == 1205 || // Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
01219                  $errno == 1213 )  // Error: 1213 SQLSTATE: 40001 (ER_LOCK_DEADLOCK)
01220             {
01221                 $tries++;
01222                 continue;
01223             }
01224             break;
01225         }
01226         if ( !$res )
01227             return $this->_fail( "Failed to perform shared lock on file $filePath" );
01228         return mysqli_fetch_assoc( $res );
01229     }
01230 
01231     /*!
01232      Protects a custom function with SQL queries in a database transaction,
01233      if the function reports an error the transaction is ROLLBACKed.
01234 
01235      The first argument to the _protect() is the callback and the second is the name of the function (for query reporting). The remainder of arguments are sent to the callback.
01236 
01237      A return value of false from the callback is considered a failure, any other value is returned from _protect(). For extended error handling call _fail() and return the value.
01238      */
01239     function _protect()
01240     {
01241         $args = func_get_args();
01242         $callback = array_shift( $args );
01243         $fname    = array_shift( $args );
01244 
01245         $maxTries = $this->dbparams['max_execute_tries'];
01246         $tries = 0;
01247         while ( $tries < $maxTries )
01248         {
01249             $this->_begin( $fname );
01250 
01251             $result = call_user_func_array( $callback, $args );
01252 
01253             $errno = mysqli_errno( $this->db );
01254             if ( $errno == 1205 || // Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
01255                  $errno == 1213 )  // Error: 1213 SQLSTATE: 40001 (ER_LOCK_DEADLOCK)
01256             {
01257                 $tries++;
01258                 $this->_rollback( $fname );
01259                 continue;
01260             }
01261 
01262             if ( $result === false )
01263             {
01264                 $this->_rollback( $fname );
01265                 return false;
01266             }
01267             elseif ( $result instanceof eZMySQLBackendError )
01268             {
01269                 eZDebug::writeError( $result->errorValue, $result->errorText );
01270                 $this->_rollback( $fname );
01271                 return false;
01272             }
01273 
01274             break; // All is good, so break out of loop
01275         }
01276 
01277         $this->_commit( $fname );
01278         return $result;
01279     }
01280 
01281     function _handleErrorType( $res )
01282     {
01283         if ( $res === false )
01284         {
01285             eZDebug::writeError( "SQL failed" );
01286         }
01287         elseif ( $res instanceof eZMySQLBackendError )
01288         {
01289             eZDebug::writeError( $res->errorValue, $res->errorText );
01290         }
01291     }
01292 
01293     /*!
01294      Checks if $result is a failure type and returns true if so, false otherwise.
01295 
01296      A failure is either the value false or an error object of type eZMySQLBackendError.
01297      */
01298     function _isFailure( $result )
01299     {
01300         if ( $result === false || ($result instanceof eZMySQLBackendError ) )
01301         {
01302             return true;
01303         }
01304         return false;
01305     }
01306 
01307     /*!
01308      Helper method for removing leftover file data rows for the file path $filePath.
01309      Note: This should be run after insert/updating filedata entries.
01310 
01311      Entries which are after $contentLength or which have different chunk offset than
01312      the defined chunk_size in $dbparams will be removed.
01313 
01314      \param $filePath The file path which was inserted/updated
01315      \param $contentLength The length of the file data
01316      \parma $fname Name of the function caller
01317      */
01318     function _cleanupFiledata( $filePath, $contentLength, $fname )
01319     {
01320         $chunkSize = $this->dbparams['chunk_size'];
01321         $sql = "DELETE FROM " . TABLE_DATA . " WHERE name_hash = " . $this->_md5( $filePath ) . " AND (offset % $chunkSize != 0 OR offset > $contentLength)";
01322         if ( !$this->_query( $sql, $fname ) )
01323             return $this->_fail( "Failed to remove old file data." );
01324 
01325         return true;
01326     }
01327 
01328     /*!
01329      Creates an error object which can be read by some backend functions.
01330 
01331      \param $value The value which is sent to the debug system.
01332      \param $text The text/header for the value.
01333      */
01334     function _fail( $value, $text = false )
01335     {
01336         $value .= "\n" . mysqli_errno( $this->db ) . ": " . mysqli_error( $this->db );
01337         return new eZMySQLBackendError( $value, $text );
01338     }
01339 
01340     /*!
01341      Performs mysql query and returns mysql result.
01342      Times the sql execution, adds accumulator timings and reports SQL to debug.
01343 
01344      \param $fname The function name that started the query, should contain relevant arguments in the text.
01345      */
01346     function _query( $query, $fname = false, $reportError = true )
01347     {
01348         eZDebug::accumulatorStart( 'mysql_cluster_query', 'mysql_cluster_total', 'Mysql_cluster_queries' );
01349         $time = microtime( true );
01350 
01351         $res = mysqli_query( $this->db, $query );
01352         if ( !$res && $reportError )
01353         {
01354             $this->_error( $query, $fname );
01355         }
01356         $numRows = 0;
01357         if ( is_object( $res ) )
01358         {
01359             $numRows = mysqli_num_rows( $res );
01360         }
01361 
01362         $time = microtime( true ) - $time;
01363         eZDebug::accumulatorStop( 'mysql_cluster_query' );
01364 
01365         $this->_report( $query, $fname, $time, $numRows );
01366         return $res;
01367     }
01368 
01369     /**
01370      * Make sure that $value is escaped and qouted according to type and returned
01371      * as a string.
01372      *
01373      * @param string $value a SQL parameter to escape
01374      * @param bool $escapeUnderscoreWildcards Set to true to escape underscores as well to avoid them to act as wildcards
01375      *                                        Highly recommended for LIKE statements !
01376      * @return string a string that can safely be used in SQL queries
01377      */
01378     function _quote( $value, $escapeUnderscoreWildcards = false )
01379     {
01380         if ( $value === null )
01381         {
01382             return 'NULL';
01383         }
01384         elseif ( is_integer( $value ) )
01385         {
01386             return (string)$value;
01387         }
01388         else
01389         {
01390            if ( $escapeUnderscoreWildcards )
01391                 return "'" . addcslashes( mysqli_real_escape_string( $this->db, $value ), "_" ) . "'";
01392            else
01393                 return "'" . mysqli_real_escape_string( $this->db, $value ) . "'";
01394         }
01395     }
01396 
01397     /*!
01398      Make sure that $value is escaped and qouted and turned into and MD5.
01399      The returned value can directly be put into SQLs.
01400      */
01401     function _md5( $value )
01402     {
01403         return "MD5('" . mysqli_real_escape_string( $this->db, $value ) . "')";
01404     }
01405 
01406     /*!
01407      Prints error message $error to debug system.
01408 
01409      \param $query The query that was attempted, will be printed if $error is \c false
01410      \param $fname The function name that started the query, should contain relevant arguments in the text.
01411      \param $error The error message, if this is an array the first element is the value to dump and the second the error header (for eZDebug::writeNotice). If this is \c false a generic message is shown.
01412      */
01413     function _error( $query, $fname, $error = "Failed to execute SQL for function:" )
01414     {
01415         if ( $error === false )
01416         {
01417             $error = "Failed to execute SQL for function:";
01418         }
01419         else if ( is_array( $error ) )
01420         {
01421             $fname = $error[1];
01422             $error = $error[0];
01423         }
01424 
01425         eZDebug::writeError( "$error\n" . mysqli_errno( $this->db ) . ': ' . mysqli_error( $this->db ), $fname );
01426     }
01427 
01428     /**
01429      * Report SQL $query to debug system.
01430      *
01431      * @param string $fname The function name that started the query, should contain relevant arguments in the text.
01432      * @param int    $timeTaken Number of seconds the query + related operations took (as float).
01433      * @param int $numRows Number of affected rows.
01434      */
01435     function _report( $query, $fname, $timeTaken, $numRows = false )
01436     {
01437         if ( !$this->dbparams['sql_output'] )
01438             return;
01439 
01440         $rowText = '';
01441         if ( $numRows !== false )
01442             $rowText = "$numRows rows, ";
01443         static $numQueries = 0;
01444         if ( strlen( $fname ) == 0 )
01445             $fname = "_query";
01446         $backgroundClass = ($this->transactionCount > 0  ? "debugtransaction transactionlevel-$this->transactionCount" : "");
01447         eZDebug::writeNotice( "$query", "cluster::mysql::{$fname}[{$rowText}" . number_format( $timeTaken, 3 ) . " ms] query number per page:" . $numQueries++, $backgroundClass );
01448     }
01449 
01450     /**
01451      * Attempts to begin cache generation by creating a new file named as the
01452      * given filepath, suffixed with .generating. If the file already exists,
01453      * insertion is not performed and false is returned (means that the file
01454      * is already being generated)
01455      * @param string $filePath
01456      * @return array array with 2 indexes: 'result', containing either ok or ko,
01457      *         and another index that depends on the result:
01458      *         - if result == 'ok', the 'mtime' index contains the generating
01459      *           file's mtime
01460      *         - if result == 'ko', the 'remaining' index contains the remaining
01461      *           generation time (time until timeout) in seconds
01462      */
01463     function _startCacheGeneration( $filePath, $generatingFilePath )
01464     {
01465         $fname = "_startCacheGeneration( {$filePath} )";
01466 
01467         $nameHash = $this->_md5( $generatingFilePath );
01468         $mtime = time();
01469 
01470         $insertData = array( 'name' => "'" . mysqli_real_escape_string( $this->db, $generatingFilePath ) . "'",
01471                              'name_trunk' => "'" . mysqli_real_escape_string( $this->db, $generatingFilePath ) . "'",
01472                              'name_hash' => $nameHash,
01473                              'scope' => "''",
01474                              'datatype' => "''",
01475                              'mtime' => $mtime,
01476                              'expired' => 0 );
01477         $query = 'INSERT INTO ' . TABLE_METADATA . ' ( '. implode(', ', array_keys( $insertData ) ) . ' ) ' .
01478                  "VALUES(" . implode( ', ', $insertData ) . ")";
01479 
01480         if ( !$this->_query( $query, "_startCacheGeneration( $filePath )", false ) )
01481         {
01482             $errno = mysqli_errno( $this->db );
01483             if ( $errno != 1062 )
01484             {
01485                 eZDebug::writeError( "Unexpected error #$errno when trying to start cache generation on $filePath (".mysqli_error( $this->db ).")", __METHOD__ );
01486                 eZDebug::writeDebug( $query, '$query' );
01487 
01488                 // @todo Make this an actual error, maybe an exception
01489                 return array( 'res' => 'ko' );
01490             }
01491             // error 1062 is expected, since it means duplicate key (file is being generated)
01492             else
01493             {
01494                 // generation timout check
01495                 $query = "SELECT mtime FROM " . TABLE_METADATA . " WHERE name_hash = {$nameHash}";
01496                 $row = $this->_selectOneRow( $query, $fname, false, false );
01497 
01498                 // file has been renamed, i.e it is no longer a .generating file
01499                 if( $row and !isset( $row[0] ) )
01500                     return array( 'result' => 'ok', 'mtime' => $mtime );
01501 
01502                 $remainingGenerationTime = $this->remainingCacheGenerationTime( $row );
01503                 if ( $remainingGenerationTime < 0 )
01504                 {
01505                     $previousMTime = $row[0];
01506 
01507                     eZDebugSetting::writeDebug( 'kernel-clustering', "$filePath generation has timedout (timeout={$this->dbparams['cache_generation_timeout']}), taking over", __METHOD__ );
01508                     $updateQuery = "UPDATE " . TABLE_METADATA . " SET mtime = {$mtime} WHERE name_hash = {$nameHash} AND mtime = {$previousMTime}";
01509                     eZDebug::writeDebug( $updateQuery, '$updateQuery' );
01510 
01511                     // we run the query manually since the default _query won't
01512                     // report affected rows
01513                     $res = mysqli_query( $this->db, $updateQuery );
01514                     if ( ( $res !== false ) and mysqli_affected_rows( $this->db ) == 1 )
01515                     {
01516                         return array( 'result' => 'ok', 'mtime' => $mtime );
01517                     }
01518                     else
01519                     {
01520                         // @todo This would require an actual error handling
01521                         eZDebug::writeError( "An error occured taking over timedout generating cache file $generatingFilePath (".mysqli_error( $this->db ).")", __METHOD__ );
01522                         return array( 'result' => 'error' );
01523                     }
01524                 }
01525                 else
01526                 {
01527                     return array( 'result' => 'ko', 'remaining' => $remainingGenerationTime );
01528                 }
01529             }
01530         }
01531         else
01532         {
01533             return array( 'result' => 'ok', 'mtime' => $mtime );
01534         }
01535     }
01536 
01537     /**
01538      * Ends the cache generation for the current file: moves the (meta)data for
01539      * the .generating file to the actual file, and removed the .generating
01540      * @param string $filePath
01541      * @return bool
01542      */
01543     function _endCacheGeneration( $filePath, $generatingFilePath, $rename )
01544     {
01545         $fname = "_endCacheGeneration( $filePath )";
01546 
01547         eZDebugSetting::writeDebug( 'kernel-clustering', $filePath, __METHOD__ );
01548 
01549         // if no rename is asked, the .generating file is just removed
01550         if ( $rename === false )
01551         {
01552             if ( !$this->_query( "DELETE FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')" ) )
01553             {
01554                 eZDebug::writeError( "Failed removing metadata entry for '$generatingFilePath'", $fname );
01555                 return false;
01556             }
01557             else
01558             {
01559                 return true;
01560             }
01561         }
01562         else
01563         {
01564             $this->_begin( $fname );
01565 
01566             // both files are locked for update
01567             if ( !$res = $this->_query( "SELECT * FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath') FOR UPDATE", $fname, true ) )
01568             {
01569                 $this->_rollback( $fname );
01570                 return false;
01571             }
01572             $generatingMetaData = mysqli_fetch_assoc( $res );
01573 
01574             $res = $this->_query( "SELECT * FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$filePath') FOR UPDATE", $fname, false );
01575             // the original file does not exist: we move the generating file
01576             if ( mysqli_num_rows( $res ) == 0 )
01577             {
01578                 $metaData = $generatingMetaData;
01579                 $metaData['name'] = $filePath;
01580                 $metaData['name_hash'] = md5( $filePath );
01581                 $metaData['name_trunk'] = $this->nameTrunk( $filePath, $metaData['scope'] );
01582                 $insertSQL = "INSERT INTO " . TABLE_METADATA . " ( " . implode( ', ', array_keys( $metaData ) ) . " ) " .
01583                              "VALUES( " . $this->_sqlList( $metaData ) . ")";
01584                 if ( !$this->_query( $insertSQL, $fname, true ) )
01585                 {
01586                     $this->_rollback( $fname );
01587                     return false;
01588                 }
01589                 if ( !$this->_query( "UPDATE " . TABLE_DATA . " SET name_hash=MD5('$filePath') WHERE name_hash=MD5('$generatingFilePath')", $fname, true ) )
01590                 {
01591                     $this->_rollback( $fname );
01592                     return false;
01593                 }
01594                 $this->_query( "DELETE FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')", $fname, true );
01595             }
01596             // the original file exists: we move the generating data to this file
01597             // and update it
01598             else
01599             {
01600                 $this->_query( "DELETE FROM " . TABLE_DATA . " WHERE name_hash=MD5('$filePath')", $fname, false );
01601                 if ( !$this->_query( "UPDATE " . TABLE_DATA . " SET name_hash=MD5('$filePath') WHERE name_hash=MD5('$generatingFilePath')", $fname, true ) )
01602                 {
01603                     $this->_rollback( $fname );
01604                     return false;
01605                 }
01606 
01607                 $mtime = $generatingMetaData['mtime'];
01608                 $filesize = $generatingMetaData['size'];
01609                 if ( !$this->_query( "UPDATE " . TABLE_METADATA . " SET mtime = '{$mtime}', expired = 0, size = '{$filesize}' WHERE name_hash=MD5('$filePath')", $fname, true ) )
01610                 {
01611                     $this->_rollback( $fname );
01612                     return false;
01613                 }
01614                 $this->_query( "DELETE FROM " . TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')", $fname, true );
01615             }
01616 
01617             $this->_commit( $fname );
01618         }
01619 
01620         return true;
01621     }
01622 
01623     /**
01624      * Checks if generation has timed out by looking for the .generating file
01625      * and comparing its timestamp to the one assigned when the file was created
01626      *
01627      * @param string $generatingFilePath
01628      * @param int    $generatingFileMtime
01629      *
01630      * @return bool true if the file didn't timeout, false otherwise
01631      */
01632     function _checkCacheGenerationTimeout( $generatingFilePath, $generatingFileMtime )
01633     {
01634         $fname = "_checkCacheGenerationTimeout( $generatingFilePath, $generatingFileMtime )";
01635         eZDebugSetting::writeDebug( 'kernel-clustering', "Checking for timeout of '$generatingFilePath' with mtime $generatingFileMtime", $fname );
01636 
01637         // reporting
01638         eZDebug::accumulatorStart( 'mysql_cluster_query', 'mysql_cluster_total', 'Mysql_cluster_queries' );
01639         $time = microtime( true );
01640 
01641         $nameHash = $this->_md5( $generatingFilePath );
01642         $newMtime = time();
01643 
01644         // The update query will only succeed if the mtime wasn't changed in between
01645         $query = "UPDATE " . TABLE_METADATA . " SET mtime = $newMtime WHERE name_hash = {$nameHash} AND mtime = $generatingFileMtime";
01646         $res = mysqli_query( $this->db, $query );
01647         if ( !$res )
01648         {
01649             $this->_error( $query, $fname );
01650             return false;
01651         }
01652         $numRows = mysqli_affected_rows( $this->db );
01653 
01654         // reporting. Manual here since we don't use _query
01655         $time = microtime( true ) - $time;
01656         $this->_report( $query, $fname, $time, $numRows );
01657 
01658         // no rows affected or row updated with the same value
01659         // f.e a cache-block which takes less than 1 sec to get generated
01660         // if a line has been updated by the same  values, mysqli_affected_rows
01661         // returns 0, and updates nothing, we need to extra check this,
01662         if( $numRows == 0 )
01663         {
01664             $query = "SELECT mtime FROM " . TABLE_METADATA . " WHERE name_hash = {$nameHash}";
01665             $res = mysqli_query( $this->db, $query );
01666             if ( !$res )
01667                 return false;
01668             $row = mysqli_fetch_row( $res );
01669             if( isset( $row[0] ) and $row[0] == $generatingFileMtime );
01670                 return true;
01671 
01672             return false;
01673         }
01674         // rows affected: mtime has changed, or row has been removed
01675         if ( $numRows == 1 )
01676         {
01677             return true;
01678         }
01679         else
01680         {
01681             eZDebugSetting::writeDebug( 'kernel-clustering', "No rows affected by query '$query', record has been modified", __METHOD__ );
01682             return false;
01683         }
01684     }
01685 
01686     /**
01687      * Aborts the cache generation process by removing the .generating file
01688      * @param string $filePath Real cache file path
01689      * @param string $generatingFilePath .generating cache file path
01690      * @return void
01691      */
01692     function _abortCacheGeneration( $generatingFilePath )
01693     {
01694         $sql = "DELETE FROM " . TABLE_METADATA . " WHERE name_hash = " . $this->_md5( $generatingFilePath );
01695         $this->_query( $sql, "_abortCacheGeneration( '$generatingFilePath' )" );
01696     }
01697 
01698     /**
01699      * Returns the name_trunk for a file path
01700      * @param string $filePath
01701      * @param string $scope
01702      * @return string
01703      */
01704     static function nameTrunk( $filePath, $scope )
01705     {
01706         switch ( $scope )
01707         {
01708             case 'viewcache':
01709             {
01710                 $nameTrunk = substr( $filePath, 0, strrpos( $filePath, '-' ) + 1 );
01711             } break;
01712 
01713             case 'template-block':
01714             {
01715                 $templateBlockCacheDir = eZTemplateCacheBlock::templateBlockCacheDir();
01716                 $templateBlockPath = str_replace( $templateBlockCacheDir, '', $filePath );
01717                 if ( strstr( $templateBlockPath, 'subtree/' ) !== false )
01718                 {
01719                     // 6 = strlen( 'cache/' );
01720                     $len = strlen( $templateBlockCacheDir ) + strpos( $templateBlockPath, 'cache/' ) + 6;
01721                     $nameTrunk = substr( $filePath, 0, $len  );
01722                 }
01723                 else
01724                 {
01725                     $nameTrunk = $filePath;
01726                 }
01727             } break;
01728 
01729             default:
01730             {
01731                 $nameTrunk = $filePath;
01732             }
01733         }
01734         return $nameTrunk;
01735     }
01736 
01737     /**
01738      * Returns the remaining time, in seconds, before the generating file times
01739      * out
01740      *
01741      * @param resource $fileRow
01742      *
01743      * @return int Remaining generation seconds. A negative value indicates a timeout.
01744      */
01745     private function remainingCacheGenerationTime( $row )
01746     {
01747         if( !isset( $row[0] ) )
01748             return -1;
01749 
01750         return ( $row[0] + $this->dbparams['cache_generation_timeout'] ) - time();
01751     }
01752 
01753     /**
01754      * Returns the list of expired files
01755      *
01756      * @param array $scopes Array of scopes to consider. At least one.
01757      * @param int $limit Max number of items. Set to false for unlimited.
01758      * @param int $expiry Number of seconds, only items older than this will be returned.
01759      *
01760      * @return array(filepath)
01761      *
01762      * @since 4.3
01763      */
01764     public function expiredFilesList( $scopes, $limit = array( 0, 100 ), $expiry = false )
01765     {
01766         if ( count( $scopes ) == 0 )
01767             throw new ezcBaseValueException( 'scopes', $scopes, "array of scopes", "parameter" );
01768 
01769         $scopeString = $this->_sqlList( $scopes );
01770         $query = "SELECT name FROM " . TABLE_METADATA . " WHERE expired = 1 AND scope IN( $scopeString )";
01771         if ( $expiry !== false )
01772         {
01773             $query .= ' AND mtime < ' . (time() - $expiry);
01774         }
01775         if ( $limit !== false )
01776         {
01777             $query .= " LIMIT {$limit[0]}, {$limit[1]}";
01778         }
01779         $res = $this->_query( $query, __METHOD__ );
01780         $filePathList = array();
01781         while ( $row = mysqli_fetch_row( $res ) )
01782             $filePathList[] = $row[0];
01783         mysqli_free_result( $res );
01784 
01785         return $filePathList;
01786     }
01787 
01788     public $db   = null;
01789     public $numQueries = 0;
01790     public $transactionCount = 0;
01791     public $dbparams;
01792     private $backendVerify = null;
01793 }
01794 
01795 ?>