b2evolution PHP Cross Reference Blogging Systems

Source: /inc/_core/model/_pagecache.class.php - 721 lines - 20838 bytes - Summary - Text - Print

Description: This file implements the PageCache class, which caches HTML pages genereated by the app. This file is part of the evoCore framework - {@link http://evocore.net/} See also {@link http://sourceforge.net/projects/evocms/}.

   1  <?php
   2  /**
   3   * This file implements the PageCache class, which caches HTML pages genereated by the app.
   4   *
   5   * This file is part of the evoCore framework - {@link http://evocore.net/}
   6   * See also {@link http://sourceforge.net/projects/evocms/}.
   7   *
   8   * @copyright (c)2003-2014 by Francois Planque - {@link http://fplanque.com/}
   9   * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
  10   *
  11   * {@internal License choice
  12   * - If you have received this file as part of a package, please find the license.txt file in
  13   *   the same folder or the closest folder above for complete license terms.
  14   * - If you have received this file individually (e-g: from http://evocms.cvs.sourceforge.net/)
  15   *   then you must choose one of the following licenses before using the file:
  16   *   - GNU General Public License 2 (GPL) - http://www.opensource.org/licenses/gpl-license.php
  17   *   - Mozilla Public License 1.1 (MPL) - http://www.opensource.org/licenses/mozilla1.1.php
  18   * }}
  19   *
  20   * {@internal Open Source relicensing agreement:
  21   * }}
  22   *
  23   * @package evocore
  24   *
  25   * @version $Id: _pagecache.class.php 6136 2014-03-08 07:59:48Z manuel $ }}}
  26   *
  27   */
  28  if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  29  
  30  
  31  /**
  32   * Page Cache.
  33   *
  34   * @package evocore
  35   */
  36  class PageCache
  37  {
  38    /**
  39       * How old can a cached object get before we consider it outdated
  40       */
  41      var $max_age_seconds = 900;  // 15 minutes for now
  42  
  43    /**
  44       * After how many bytes should we output sth live while collecting cache content:
  45       */
  46      var $output_chunk_size = 2000;
  47  
  48      /**
  49       * By default we consider caching not to be enabled
  50       */
  51      var $is_enabled = false;
  52  
  53      /**
  54       *
  55       */
  56      var $ads_collcache_path;
  57  
  58      /**
  59       * Filename of cache for current page
  60       */
  61      var $cache_filepath;
  62      /**
  63       * Progressively caching the content of the current page:
  64       */
  65      var $cached_page_content = '';
  66      /**
  67       * Are we currently recording cache contents
  68       */
  69      var $is_collecting = false;
  70  
  71      /**
  72       * Cache Blog. It should be NULL if this is not a blog PageCache.
  73       */
  74      var $cache_Blog = NULL;
  75  
  76      /**
  77       * Constructor
  78       *
  79       * @param Blog to use, can be NULL
  80       */
  81  	function PageCache( $Blog = NULL )
  82      {
  83          global $Debuglog;
  84          global $Settings;
  85          global $cache_path, $pagecache_max_age;
  86  
  87          if( is_null($Blog) )
  88          {    // Cache for "other" "genereic" "special" pages:
  89              $this->ads_collcache_path = $cache_path.'general/';
  90  
  91              if( ( ! empty( $Settings ) ) && ( ! $Settings->get('general_cache_enabled') ) )
  92              {    // We do NOT want caching for this collection
  93                  $Debuglog->add( 'General cache not enabled.', 'pagecache' );
  94              }
  95              else
  96              {
  97                  $this->is_enabled = true;
  98              }
  99          }
 100          else
 101          {    // Cache for a specific Blog/Collection:
 102              // We need to set this even if cache is not enabled (yet) bc it's used for creating:
 103              $this->ads_collcache_path = $cache_path.'c'.$Blog->ID.'/';
 104              $this->cache_Blog = $Blog;
 105  
 106              if( ! $Blog->get_setting('cache_enabled') )
 107              {    // We do NOT want caching for this collection
 108                  $Debuglog->add( 'Cache not enabled for this blog.', 'pagecache' );
 109              }
 110              else
 111              {
 112                  $this->is_enabled = true;
 113              }
 114          }
 115  
 116          $this->max_age_seconds = $pagecache_max_age;
 117      }
 118  
 119  
 120      /**
 121       * Get path to file for current URL
 122       *
 123       * @todo fp> We may need to add some keys like the locale or the charset, I'm not sure.
 124       */
 125  	function get_af_filecache_path()
 126      {
 127          global $Debuglog;
 128          global $ReqURL;
 129  
 130          // We want the cache for the current URL
 131          if( empty( $this->cache_filepath ) )
 132          {
 133               $Debuglog->add( 'URL being cached: '.$ReqURL, 'pagecache' );
 134  
 135               $this->cache_filepath = $this->gen_filecache_path( $ReqURL );
 136  
 137               $Debuglog->add( 'Cache file: '.$this->cache_filepath, 'pagecache' );
 138          }
 139  
 140           return $this->cache_filepath;
 141      }
 142  
 143  
 144      /**
 145       * Generate path for caching $url.
 146       * @param string URL
 147       * @return string
 148       */
 149  	function gen_filecache_path( $url )
 150      {
 151          global $skin;
 152  
 153          if( !empty( $skin ) )
 154          { // add skin folder into the end of the url before creating the hash to have different cache files for different skins
 155              $url .= $skin;
 156          }
 157          $url_hash = md5($url);    // fp> is this teh fastest way to hash this into something not too obvious to guess?
 158          // echo $url_hash;
 159  
 160          return $this->ads_collcache_path.$url_hash.'.page';
 161      }
 162  
 163  
 164      /**
 165       * Invalidate a particular page from the cache
 166       *
 167       * @param URL of the page to be invalidated
 168       */
 169  	function invalidate( $url )
 170      {
 171          global $Debuglog;
 172  
 173          // echo 'Invalidating:'.$url;
 174          $Debuglog->add( 'Invalidating:'.$url, 'pagecache' );
 175  
 176          // What would be the cache file for the current URL?
 177          $af_cache_file = $this->gen_filecache_path( $url );
 178  
 179          @unlink( $af_cache_file );
 180      }
 181  
 182  
 183      /**
 184       * @return boolean true if cache has been successfully created
 185       */
 186  	function cache_create( $clear = true )
 187      {
 188          global $cache_path;
 189  
 190          // Create by using the filemanager's default chmod. TODO> we may not want to make these publicly readable
 191          if( ! mkdir_r( $this->ads_collcache_path, NULL ) )
 192          {
 193              return false;
 194          }
 195  
 196          if( $clear )
 197          { // Clear contents of folder, if any:
 198              cleardir_r( $this->ads_collcache_path );
 199          }
 200  
 201          // Create htaccess file with deny rules
 202          if( ! create_htaccess_deny( $cache_path ) )
 203          {
 204              return false;
 205          }
 206  
 207          return true;
 208      }
 209  
 210  
 211      /**
 212       * Delete all cache files
 213       */
 214  	function cache_delete()
 215      {
 216          rmdir_r( $this->ads_collcache_path );
 217      }
 218  
 219  
 220      /**
 221       * Check if cache contents are available, otherwise start collecting output to be cached
 222       *
 223       * @return true if we found and have echoed content from the cache
 224       */
 225  	function check()
 226      {
 227          global $Debuglog;
 228          global $disp;
 229  
 230          global $Messages;
 231  
 232          if( ! $this->is_enabled )
 233          {    // We do NOT want caching for this page
 234              $Debuglog->add( 'Cache not enabled. No lookup nor caching performed.', 'pagecache' );
 235              return false;
 236          }
 237  
 238          if( $disp == '404' )
 239          {    // We do NOT want caching for 404 pages (illimited possibilities!)
 240              $Debuglog->add( 'Never cache 404s!', 'pagecache' );
 241              return false;
 242          }
 243  
 244          if( $disp == 'login' || $disp == 'register' || $disp == 'lostpassword' )
 245          {    // We do NOT want caching for in-skin login, register and lostpassord pages
 246              $Debuglog->add( 'Never cache the in-skin login and register pages!', 'pagecache' );
 247              return false;
 248          }
 249  
 250          // In the following cases, the page may sometimes be cached, sometimes be not...
 251          // We may need an etag to later determine if the client cache is the same as the server cache:
 252  
 253          // Send etag:
 254          header_etag( gen_current_page_etag() );
 255  
 256          if( is_logged_in() )
 257          {    // We do NOT want caching when a user is logged in (private data)
 258              $Debuglog->add( 'Never cache pages for/from logged in members!', 'pagecache' );
 259              return false;
 260          }
 261  
 262          if( $Messages->count() )
 263          {    // There are some messages do be displayed. That means the user has done some action.
 264              // We do want to display those messages.
 265              // There may also be more... like a "comment pending review" etc...
 266              // DO NOT CACHE and do not present a cached page.
 267              $Debuglog->add( 'Not caching because we have messages!', 'pagecache' );
 268              return false;
 269          }
 270  
 271  
 272          // TODO: fp> If the user has submitted a comment, we might actually want to invalidate the cache...
 273  
 274  
 275          if( $this->retrieve() )
 276          { // We could retrieve:
 277              return true;
 278          }
 279  
 280  
 281          $this->is_collecting = true;
 282  
 283          $Debuglog->add( 'Collecting started', 'pagecache' );
 284  
 285          ob_start( array( & $this, 'output_handler'), $this->output_chunk_size );
 286  
 287          return false;
 288      }
 289  
 290  
 291      /**
 292       * Retrieve and output cache for current URL.
 293       *
 294       * @return boolean true if we could retrieve
 295       */
 296  	function retrieve()
 297      {
 298          global $Debuglog;
 299          global $ReqURL;
 300          global $servertimenow;
 301          global $Timer;
 302          global $Settings;
 303  
 304          // What would be the cache file for the current URL?
 305          $af_cache_file = $this->get_af_filecache_path();
 306  
 307  
 308          /*
 309          // fstat() is interesting because it gives the last access time... use that for purging...
 310          * Tblue> Note: Many server admins mount partitions with the "noatime"
 311          *              option, which disables atime updates and thus speeds
 312          *              up disk access - that means the atime is not reliable,
 313          *              better use the mtime (modification time).
 314          if( $fh = @fopen( $af_cache_file, 'r', false ) )
 315          {
 316              $fstat = fstat( $fh );
 317              pre_dump( $fstat );
 318              fclose( $fh );
 319          }
 320          */
 321  
 322          $Timer->resume( 'Read cache file' );
 323          $lines = @file( $af_cache_file, false );
 324          $Timer->pause( 'Read cache file' );
 325  
 326          if( $this->cache_Blog != NULL )
 327          {
 328              $last_invalidation_timestamp = $this->cache_Blog->get_setting( 'last_invalidation_timestamp' );
 329              if( $last_invalidation_timestamp == 0 )
 330              {
 331                  $this->cache_Blog->set_setting( 'last_invalidation_timestamp', $servertimenow );
 332                  $this->cache_Blog->dbupdate();
 333              }
 334          }
 335          else
 336          {
 337              $last_invalidation_timestamp = $Settings->get( 'last_invalidation_timestamp' );
 338              if( $last_invalidation_timestamp == 0 )
 339              {
 340                  $Settings->set( 'last_invalidation_timestamp', $servertimenow );
 341                  $Settings->dbupdate();
 342              }
 343          }
 344  
 345          // fp> note we are using empty() so that we detect both the case where there is no file and the case where the file
 346          // might have ended up empty because PHP crashed while writing to it or sth like that...
 347          if( ! empty($lines) )
 348          {    // We have data in the cache!
 349              $Debuglog->add( 'Retrieving from cache!', 'pagecache' );
 350  
 351              $Timer->resume( 'Cache file processing' );
 352  
 353              // Retrieved cached URL:
 354              $retrieved_url = trim($lines[0]);
 355              unset($lines[0]);
 356              if( $retrieved_url != $ReqURL )
 357              {
 358                  $Debuglog->add( 'Cached file URL ['.$retrieved_url.'] does not match current URL, aborting retrieve.', 'pagecache' );
 359                  return false;
 360              }
 361  
 362              // timestamp of cache generation:
 363              $retrieved_ts = trim($lines[1]);
 364              unset($lines[1]);
 365              $cache_age = $servertimenow-$retrieved_ts;
 366              $Debuglog->add( 'Cache age: '.floor($cache_age/60).' min '.($cache_age % 60).' sec', 'pagecache' );
 367              if( ( $cache_age > $this->max_age_seconds ) || ( $last_invalidation_timestamp > $retrieved_ts ) )
 368              {    // Cache has expired
 369                  return false;
 370              }
 371  
 372              $i = 1;
 373              $optional_headers = array();
 374              // Go through optional header lines
 375              // Optional headers are separated from the file header with an empty line.
 376              while( $optional_header_line = trim($lines[++$i]) )
 377              {
 378                  // All optional header name value must be separated with ':'
 379                  if( strpos( $optional_header_line, ':' ) === false )
 380                  {
 381                      $Debuglog->add( 'Cached file format not recognized, aborting retrieve.', 'pagecache' );
 382                      return false;
 383                  }
 384                  list( $header_name, $header_value ) = explode( ":", $optional_header_line );
 385                  // Optional header name and value must not be empty
 386                  $header_name = trim( $header_name );
 387                  $header_value = trim( $header_value );
 388                  if( empty( $header_name ) || empty( $header_value ) )
 389                  {
 390                      $Debuglog->add( 'Cached file format not recognized, aborting retrieve.', 'pagecache' );
 391                      return false;
 392                  }
 393                  $optional_headers[$header_name] = $header_value;
 394                  unset($lines[$i]);
 395              }
 396              // unset the empty line
 397              unset($lines[$i]);
 398  
 399              // count item views happening on this page:
 400              if( isset( $optional_headers[ 'item_IDs_on_this_page' ] ) )
 401              {
 402                  global $shutdown_count_item_views;
 403                  $shutdown_count_item_views = explode( ',', $optional_headers[ 'item_IDs_on_this_page' ] );
 404              }
 405  
 406              // Check if the request has an If-Modified-Since date
 407              if( array_key_exists( 'HTTP_IF_MODIFIED_SINCE', $_SERVER) )
 408              {
 409                  $if_modified_since = strtotime( preg_replace('/;.*$/','',$_SERVER['HTTP_IF_MODIFIED_SINCE']) );
 410                  if( $retrieved_ts <= $if_modified_since )
 411                  {    // Cached version is equal to (or older than) $if_modified since; contents probably not modified...
 412  
 413                      // It is still possible that in between we have sent logged-in versions (including evobar) of the page
 414                      // and that the browser has an evobar version of the page in cache. Let's verify this before sending a 304...
 415  
 416                      // We do this with an ETag header (another solution may be the Vary header)
 417                      if( array_key_exists( 'HTTP_IF_NONE_MATCH', $_SERVER) )
 418                      {
 419                          $if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'];
 420                          // pre_dump($if_none_match, gen_current_page_etag() );
 421                          if( $if_none_match == gen_current_page_etag() )
 422                          {    // Ok, this seems to be really the same:
 423                              header_http_response( '304 Not Modified' );
 424                              exit(0);
 425                          }
 426                      }
 427                  }
 428              }
 429  
 430              // Page was modified, revert $shutdown_count_item_views set
 431              $shutdown_count_item_views = array();
 432  
 433              // ============== Ready to send cached version of the page =================
 434  
 435              // Send no cache header including last modified date:
 436              header_nocache( $retrieved_ts );
 437  
 438              // Go through headers that were saved in the cache:
 439              // $i was already set
 440              while( $headerline = trim($lines[++$i]) )
 441              {
 442                  header( $headerline );
 443                  unset($lines[$i]);
 444              }
 445              unset($lines[$i]);
 446  
 447              // SEND CONTENT!
 448              $body = implode('',$lines);
 449  
 450              $Timer->pause( 'Cache file processing' );
 451  
 452              $Timer->resume( 'Sending cached content' );
 453  
 454              // Echo a first chunk (see explanation below)
 455              $buffer_size = 12000;  // Empiric value, you can make it smaller if you show me screenshots of better timings with a smaller value
 456              echo substr( $body, 0, $buffer_size );
 457  
 458              ob_start();
 459              // fp> So why do we want an ob_start here?
 460              // fp> Because otherwise echo will "hang" until all the data gets passed through apache (on default Apache install with default SendBufferSize)
 461              // fp> With ob_start() the script will terminate much faster and the total exec time of the script will look much smaller.
 462              // fp> This doesn't actually improve the speed of the transmission, it just lets the PHP script exit earlier
 463              // fp> DRAWBACK: shutdown will be executed *before* the "ob" data is actually sent :'(
 464              // fp> This is why we send a first chunk of data before ob_start(). shutdown can occur while that data is sent. then the remainder is sent.
 465              // Inspiration: http://wonko.com/post/seeing_poor_performance_using_phps_echo_statement_heres_why
 466              //              http://fplanque.com/dev/linux/how-to-log-request-processing-times-in-apache
 467              //              http://fplanque.com/dev/linux/why-echo-is-slow-in-php-how-to-make-it-really-fast
 468              // fp> TODO: do something similar during page cache collection.
 469  
 470              echo substr( $body, $buffer_size );
 471              // ob_end_flush(); // fp> WARNING: Putting an end flush here would just kill the benefit of the ob_start() above.
 472  
 473              $Timer->pause( 'Sending cached content' );
 474  
 475              return true;
 476          }
 477  
 478          return false;
 479      }
 480  
 481  
 482    /**
 483       * This is called every x bytes to provide real time output
 484       */
 485  	function output_handler( $buffer )
 486      {
 487          $this->cached_page_content .= $buffer;
 488          return $buffer;
 489      }
 490  
 491  
 492      /**
 493       * We are going to output personal data and we want to abort collecting the data for the cache.
 494       *
 495       * @param boolean flush already collected data
 496       */
 497  	function abort_collect( $flush = true )
 498      {
 499          global $Debuglog;
 500  
 501          if( ! $this->is_collecting )
 502          {    // We are not collecting anyway
 503              return;
 504          }
 505  
 506           $Debuglog->add( 'Aborting cache data collection...', 'pagecache' );
 507  
 508           if( $flush )
 509           { // Flush the output buffer and turn off buffering
 510              ob_end_flush();
 511           }
 512           else
 513           { // Erase the output buffer and turn off buffering
 514              ob_end_clean();
 515           }
 516  
 517          // We are no longer collecting...
 518          $this->is_collecting = false;
 519      }
 520  
 521  
 522      /**
 523       * End collecting output to be cached
 524       */
 525  	function end_collect()
 526      {
 527          global $Debuglog;
 528  
 529          if( ! $this->is_collecting )
 530          {    // We are not collecting
 531              return;
 532          }
 533  
 534          ob_end_flush();
 535  
 536          // echo ' *** cache end *** ';
 537          // echo $this->cached_page_content;
 538  
 539          // What would be the cache file for the current URL?
 540          $af_cache_file = $this->get_af_filecache_path();
 541  
 542          // fp> 'x' mode should either give an exclusive write lock or fail
 543          // fp> TODO: this here should be ok, but it would be even better with locking the file when we start collecting cache
 544          if( ! $fh = @fopen( $af_cache_file.'.tmp', 'x', false ) )
 545          {
 546              $Debuglog->add( 'Could not open cache file!', 'pagecache' );
 547          }
 548          else
 549          {
 550              // Put the URL of the page we are caching into the cache. You can never be too paranoid!
 551              // People can change their domain names, folder structures, etc... AND you cannot trust the hash to give a
 552              // different file name in 100.00% of the cases! Serving a page for a different URL would be REEEEEALLLY BAAAAAAD!
 553              global $ReqURL;
 554              $file_head = $ReqURL."\n";
 555  
 556              // Put the time of the page generation into the file (btw this is the time of when we started this script)
 557              global $servertimenow;
 558              $file_head .= $servertimenow."\n";
 559  
 560              // set optional header line for item view count
 561              global $shutdown_count_item_views;
 562              if( !empty( $shutdown_count_item_views ) )
 563              {
 564                  $file_head .= 'item_IDs_on_this_page:'.implode( ',', $shutdown_count_item_views )."\n";
 565              }
 566              global $skin;
 567              if( !empty( $skin ) )
 568              { // add current skin folder into the cache file header
 569                  $file_head .= 'skin:'.$skin."\n";
 570              }
 571  
 572               $file_head .= "\n";
 573  
 574              // We need to write the content type!
 575              global $content_type_header;
 576              if( !empty($content_type_header) )
 577              {
 578                  $file_head .= $content_type_header."\n";
 579              }
 580  
 581              $file_head .= "\n";
 582  
 583              fwrite( $fh, $file_head.$this->cached_page_content );
 584              fclose( $fh );
 585  
 586              // Now atomically replace old cache with new cache (at least on Linux)
 587              if( ! @rename( $af_cache_file.'.tmp', $af_cache_file ) )
 588              {    // Rename failed, we are probably on windows PHP <= 5.2.5... http://bugs.php.net/bug.php?id=44805
 589                  // we have to split this:
 590                  $Debuglog->add( 'Renaming of cache file failed. (Windows?)', 'pagecache' );
 591                  // Kill cache:
 592                  unlink( $af_cache_file );
 593                  // Now, some other process might start to try caching (and will likely give up since the .tmp file already exists)
 594                  if( ! @rename( $af_cache_file.'.tmp', $af_cache_file ) )
 595                  { // Hide errors bc another PHP process could have beaten us to writing a new file there
 596                      // Anyways, we still could not rename, let's drop the .tmp file:
 597                      unlink( $af_cache_file.'.tmp' );
 598                  }
 599                  else
 600                  {
 601                      $Debuglog->add( 'Cache updated... after unlink+rename!', 'pagecache' );
 602                  }
 603              }
 604              else
 605              {
 606                  $Debuglog->add( 'Cache updated!', 'pagecache' );
 607              }
 608          }
 609      }
 610  
 611  
 612      /**
 613       * Check if the file with the given file path should be removed during prune page cache
 614       *
 615       * @static
 616       *
 617       * @param $file_path
 618       * @return boolean true if the file should be removed, false otherwise
 619       */
 620  	function checkDelete( $file_path )
 621      {
 622          if( strpos( $file_path, 'CVS' ) !== false )
 623          { // skip CVS folders - This could be more specific
 624              return false;
 625          }
 626          // get file name from path
 627          $file_name = basename($file_path);
 628  
 629          // Note: index.html pages are in the cache to hide the contents from browsers in case the webserver whould should a listing
 630          if( ( $file_name == 'index.html' ) || ( substr( $file_name, 0, 1 ) == '.' ) || ( $file_name == 'sample.htaccess' ) )
 631          { // this file is index.html or sample.htaccess or it is hidden, should not delete it.
 632              return false;
 633          }
 634  
 635          global $localtimenow;
 636          $datediff = $localtimenow - filemtime( $file_path );
 637          if( $datediff < 86400 /* 60*60*24 = 24 hour*/)
 638          { // the file is not older then 24 hour, should not delete it.
 639              return false;
 640          }
 641  
 642          // the file can be deleted
 643          return true;
 644      }
 645  
 646  
 647      /**
 648       * Delete those files from the given directory, which results true value on checkDelete function
 649       *
 650       * @static
 651       *
 652       * @param string directory path
 653       * @param string the first error occured deleting the directory content
 654       */
 655  	function deleteDirContent( $path, & $first_error )
 656      {
 657          $path = trailing_slash( $path );
 658  
 659          if( $dir = @opendir($path) )
 660          {
 661              while( ( $file = readdir($dir) ) !== false )
 662              {
 663                  if( $file == '.' || $file == '..' )
 664                  {
 665                      continue;
 666                  }
 667                  $file_path = $path.$file;
 668                  if( is_dir($file_path) )
 669                  {
 670                      PageCache::deleteDirContent( $file_path, $first_error );
 671                  }
 672                  else
 673                  {
 674                      if( PageCache::checkDelete( $file_path ) )
 675                      {
 676                          if( ( ! @unlink( $file_path ) ) && ($first_error == '') )
 677                          { // deleting the file failed: return error
 678                              //$error = error_get_last(); // XXX: requires PHP 5.2
 679                              //$error['message'];
 680                              $first_error = sprintf( T_('Some files could not be deleted (including: %s).'), $file);
 681                          }
 682                      }
 683                  }
 684              }
 685          }
 686          else
 687          {
 688              if( $first_error == '' )
 689              {
 690                  $first_error = sprintf( T_('Can not access directory: %s.'), $path );;
 691              }
 692          }
 693      }
 694  
 695  
 696      /**
 697       * Delete any file that is older than 24 hours from the whole /cache folder (recursively)
 698       * except index.html files and hiddenfiles (starting with .)
 699       *
 700       * @static
 701       *
 702       * @return string empty string on success, error message otherwise.
 703       */
 704  	function prune_page_cache()
 705      {
 706          global $cache_path;
 707  
 708          load_funcs( 'tools/model/_system.funcs.php' );
 709          // check and try to repair cache folders
 710          system_check_caches();
 711  
 712          $path = trailing_slash( $cache_path );
 713  
 714          $first_error = '';
 715          PageCache::deleteDirContent( $path, $first_error );
 716  
 717          return $first_error;
 718      }
 719  }
 720  
 721  ?>

title

Description

title

Description

title

Description

title

title

Body