Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  /*
   3   * Copyright 2008 Google Inc.
   4   *
   5   * Licensed under the Apache License, Version 2.0 (the "License");
   6   * you may not use this file except in compliance with the License.
   7   * You may obtain a copy of the License at
   8   *
   9   *     http://www.apache.org/licenses/LICENSE-2.0
  10   *
  11   * Unless required by applicable law or agreed to in writing, software
  12   * distributed under the License is distributed on an "AS IS" BASIS,
  13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14   * See the License for the specific language governing permissions and
  15   * limitations under the License.
  16   */
  17  
  18  if (!class_exists('Google_Client')) {
  19    require_once dirname(__FILE__) . '/../autoload.php';
  20  }
  21  
  22  /*
  23   * This class implements a basic on disk storage. While that does
  24   * work quite well it's not the most elegant and scalable solution.
  25   * It will also get you into a heap of trouble when you try to run
  26   * this in a clustered environment.
  27   *
  28   * @author Chris Chabot <chabotc@google.com>
  29   */
  30  #[AllowDynamicProperties]
  31  class Google_Cache_File extends Google_Cache_Abstract
  32  {
  33    const MAX_LOCK_RETRIES = 10;
  34    private $path;
  35    private $fh;
  36  
  37    /**
  38     * @var Google_Client the current client
  39     */
  40    private $client;
  41  
  42    public function __construct(Google_Client $client)
  43    {
  44      $this->client = $client;
  45      $this->path = $this->client->getClassConfig($this, 'directory');
  46    }
  47  
  48    public function get($key, $expiration = false)
  49    {
  50      $storageFile = $this->getCacheFile($key);
  51      $data = false;
  52  
  53      if (!file_exists($storageFile)) {
  54        $this->client->getLogger()->debug(
  55            'File cache miss',
  56            array('key' => $key, 'file' => $storageFile)
  57        );
  58        return false;
  59      }
  60  
  61      if ($expiration) {
  62        $mtime = filemtime($storageFile);
  63        if ((time() - $mtime) >= $expiration) {
  64          $this->client->getLogger()->debug(
  65              'File cache miss (expired)',
  66              array('key' => $key, 'file' => $storageFile)
  67          );
  68          $this->delete($key);
  69          return false;
  70        }
  71      }
  72  
  73      if ($this->acquireReadLock($storageFile)) {
  74        if (filesize($storageFile) > 0) {
  75          $data = fread($this->fh, filesize($storageFile));
  76          $data =  unserialize($data);
  77        } else {
  78          $this->client->getLogger()->debug(
  79              'Cache file was empty',
  80              array('file' => $storageFile)
  81          );
  82        }
  83        $this->unlock($storageFile);
  84      }
  85  
  86      $this->client->getLogger()->debug(
  87          'File cache hit',
  88          array('key' => $key, 'file' => $storageFile, 'var' => $data)
  89      );
  90  
  91      return $data;
  92    }
  93  
  94    public function set($key, $value)
  95    {
  96      $storageFile = $this->getWriteableCacheFile($key);
  97      if ($this->acquireWriteLock($storageFile)) {
  98        // We serialize the whole request object, since we don't only want the
  99        // responseContent but also the postBody used, headers, size, etc.
 100        $data = serialize($value);
 101        $result = fwrite($this->fh, $data);
 102        $this->unlock($storageFile);
 103  
 104        $this->client->getLogger()->debug(
 105            'File cache set',
 106            array('key' => $key, 'file' => $storageFile, 'var' => $value)
 107        );
 108      } else {
 109        $this->client->getLogger()->notice(
 110            'File cache set failed',
 111            array('key' => $key, 'file' => $storageFile)
 112        );
 113      }
 114    }
 115  
 116    public function delete($key)
 117    {
 118      $file = $this->getCacheFile($key);
 119      if (file_exists($file) && !unlink($file)) {
 120        $this->client->getLogger()->error(
 121            'File cache delete failed',
 122            array('key' => $key, 'file' => $file)
 123        );
 124        throw new Google_Cache_Exception("Cache file could not be deleted");
 125      }
 126  
 127      $this->client->getLogger()->debug(
 128          'File cache delete',
 129          array('key' => $key, 'file' => $file)
 130      );
 131    }
 132  
 133    private function getWriteableCacheFile($file)
 134    {
 135      return $this->getCacheFile($file, true);
 136    }
 137  
 138    private function getCacheFile($file, $forWrite = false)
 139    {
 140      return $this->getCacheDir($file, $forWrite) . '/' . md5($file);
 141    }
 142  
 143    private function getCacheDir($file, $forWrite)
 144    {
 145      // use the first 2 characters of the hash as a directory prefix
 146      // this should prevent slowdowns due to huge directory listings
 147      // and thus give some basic amount of scalability
 148      $storageDir = $this->path . '/' . substr(md5($file), 0, 2);
 149      if ($forWrite && ! is_dir($storageDir)) {
 150        if (! mkdir($storageDir, 0700, true)) {
 151          $this->client->getLogger()->error(
 152              'File cache creation failed',
 153              array('dir' => $storageDir)
 154          );
 155          throw new Google_Cache_Exception("Could not create storage directory: $storageDir");
 156        }
 157      }
 158      return $storageDir;
 159    }
 160  
 161    private function acquireReadLock($storageFile)
 162    {
 163      return $this->acquireLock(LOCK_SH, $storageFile);
 164    }
 165  
 166    private function acquireWriteLock($storageFile)
 167    {
 168      $rc = $this->acquireLock(LOCK_EX, $storageFile);
 169      if (!$rc) {
 170        $this->client->getLogger()->notice(
 171            'File cache write lock failed',
 172            array('file' => $storageFile)
 173        );
 174        $this->delete($storageFile);
 175      }
 176      return $rc;
 177    }
 178  
 179    private function acquireLock($type, $storageFile)
 180    {
 181      $mode = $type == LOCK_EX ? "w" : "r";
 182      $this->fh = fopen($storageFile, $mode);
 183      if (!$this->fh) {
 184        $this->client->getLogger()->error(
 185            'Failed to open file during lock acquisition',
 186            array('file' => $storageFile)
 187        );
 188        return false;
 189      }
 190      if ($type == LOCK_EX) {
 191        chmod($storageFile, 0600);
 192      }
 193      $count = 0;
 194      while (!flock($this->fh, $type | LOCK_NB)) {
 195        // Sleep for 10ms.
 196        usleep(10000);
 197        if (++$count < self::MAX_LOCK_RETRIES) {
 198          return false;
 199        }
 200      }
 201      return true;
 202    }
 203  
 204    public function unlock($storageFile)
 205    {
 206      if ($this->fh) {
 207        flock($this->fh, LOCK_UN);
 208      }
 209    }
 210  }