Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]

   1  <?php
   2  /**
   3   * Copyright 2005-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * @category  Horde
   9   * @copyright 2005-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Imap_Client
  12   */
  13  
  14  /**
  15   * A Horde_Cache implementation for caching IMAP/POP data.
  16   * Requires the Horde_Cache package.
  17   *
  18   * @author    Michael Slusarz <slusarz@horde.org>
  19   * @category  Horde
  20   * @copyright 2005-2017 Horde LLC
  21   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  22   * @package   Imap_Client
  23   */
  24  class Horde_Imap_Client_Cache_Backend_Cache
  25  extends Horde_Imap_Client_Cache_Backend
  26  {
  27      /** Cache structure version. */
  28      const VERSION = 3;
  29  
  30      /**
  31       * The cache object.
  32       *
  33       * @var Horde_Cache
  34       */
  35      protected $_cache;
  36  
  37      /**
  38       * The working data for the current pageload.  All changes take place to
  39       * this data.
  40       *
  41       * @var array
  42       */
  43      protected $_data = array();
  44  
  45      /**
  46       * The list of cache slices loaded.
  47       *
  48       * @var array
  49       */
  50      protected $_loaded = array();
  51  
  52      /**
  53       * The mapping of UIDs to slices.
  54       *
  55       * @var array
  56       */
  57      protected $_slicemap = array();
  58  
  59      /**
  60       * The list of items to update:
  61       *   - add: (array) List of IDs that were added.
  62       *   - slice: (array) List of slices that were modified.
  63       *   - slicemap: (boolean) Was slicemap info changed?
  64       *
  65       * @var array
  66       */
  67      protected $_update = array();
  68  
  69      /**
  70       * Constructor.
  71       *
  72       * @param array $params  Configuration parameters:
  73       * <pre>
  74       *   - REQUIRED Parameters:
  75       *     - cacheob: (Horde_Cache) The cache object to use.
  76       *
  77       *   - Optional Parameters:
  78       *     - lifetime: (integer) The lifetime of the cache data (in seconds).
  79       *                 DEFAULT: 1 week (604800 seconds)
  80       *     - slicesize: (integer) The slicesize to use.
  81       *                  DEFAULT: 50
  82       * </pre>
  83       */
  84      public function __construct(array $params = array())
  85      {
  86          // Default parameters.
  87          $params = array_merge(array(
  88              'lifetime' => 604800,
  89              'slicesize' => 50
  90          ), array_filter($params));
  91  
  92          if (!isset($params['cacheob'])) {
  93              throw new InvalidArgumentException('Missing cacheob parameter.');
  94          }
  95  
  96          foreach (array('lifetime', 'slicesize') as $val) {
  97              $params[$val] = intval($params[$val]);
  98          }
  99  
 100          parent::__construct($params);
 101      }
 102  
 103      /**
 104       * Initialization tasks.
 105       */
 106      protected function _initOb()
 107      {
 108          $this->_cache = $this->_params['cacheob'];
 109          register_shutdown_function(array($this, 'save'));
 110      }
 111  
 112      /**
 113       * Updates the cache.
 114       */
 115      public function save()
 116      {
 117          $lifetime = $this->_params['lifetime'];
 118  
 119          foreach ($this->_update as $mbox => $val) {
 120              $s = &$this->_slicemap[$mbox];
 121  
 122              try {
 123                  if (!empty($val['add'])) {
 124                      if ($s['c'] <= $this->_params['slicesize']) {
 125                          $val['slice'][] = $s['i'];
 126                          $this->_loadSlice($mbox, $s['i']);
 127                      }
 128                      $val['slicemap'] = true;
 129  
 130                      foreach (array_keys(array_flip($val['add'])) as $uid) {
 131                          if ($s['c']++ > $this->_params['slicesize']) {
 132                              $s['c'] = 0;
 133                              $val['slice'][] = ++$s['i'];
 134                              $this->_loadSlice($mbox, $s['i']);
 135                          }
 136                          $s['s'][$uid] = $s['i'];
 137                      }
 138                  }
 139  
 140                  if (!empty($val['slice'])) {
 141                      $d = &$this->_data[$mbox];
 142                      $val['slicemap'] = true;
 143  
 144                      foreach (array_keys(array_flip($val['slice'])) as $slice) {
 145                          $data = array();
 146                          foreach (array_keys($s['s'], $slice) as $uid) {
 147                              $data[$uid] = is_array($d[$uid])
 148                                  ? serialize($d[$uid])
 149                                  : $d[$uid];
 150                          }
 151                          $this->_cache->set($this->_getCid($mbox, $slice), serialize($data), $lifetime);
 152                      }
 153                  }
 154  
 155                  if (!empty($val['slicemap'])) {
 156                      $this->_cache->set($this->_getCid($mbox, 'slicemap'), serialize($s), $lifetime);
 157                  }
 158              } catch (Horde_Exception $e) {
 159              }
 160          }
 161  
 162          $this->_update = array();
 163      }
 164  
 165      /**
 166       */
 167      public function get($mailbox, $uids, $fields, $uidvalid)
 168      {
 169          $ret = array();
 170          $this->_loadUids($mailbox, $uids, $uidvalid);
 171  
 172          if (empty($this->_data[$mailbox])) {
 173              return $ret;
 174          }
 175  
 176          if (!empty($fields)) {
 177              $fields = array_flip($fields);
 178          }
 179          $ptr = &$this->_data[$mailbox];
 180  
 181          foreach (array_intersect($uids, array_keys($ptr)) as $val) {
 182              if (is_string($ptr[$val])) {
 183                  try {
 184                      $ptr[$val] = @unserialize($ptr[$val]);
 185                  } catch (Exception $e) {}
 186              }
 187  
 188              $ret[$val] = (empty($fields) || empty($ptr[$val]))
 189                  ? $ptr[$val]
 190                  : array_intersect_key($ptr[$val], $fields);
 191          }
 192  
 193          return $ret;
 194      }
 195  
 196      /**
 197       */
 198      public function getCachedUids($mailbox, $uidvalid)
 199      {
 200          $this->_loadSliceMap($mailbox, $uidvalid);
 201          return array_unique(array_merge(
 202              array_keys($this->_slicemap[$mailbox]['s']),
 203              (isset($this->_update[$mailbox]) ? $this->_update[$mailbox]['add'] : array())
 204          ));
 205      }
 206  
 207      /**
 208       */
 209      public function set($mailbox, $data, $uidvalid)
 210      {
 211          $update = array_keys($data);
 212  
 213          try {
 214              $this->_loadUids($mailbox, $update, $uidvalid);
 215          } catch (Horde_Imap_Client_Exception $e) {
 216              // Ignore invalidity - just start building the new cache
 217          }
 218  
 219          $d = &$this->_data[$mailbox];
 220          $s = &$this->_slicemap[$mailbox]['s'];
 221          $add = $updated = array();
 222  
 223          foreach ($data as $k => $v) {
 224              if (isset($d[$k])) {
 225                  if (is_string($d[$k])) {
 226                      try {
 227                          $d[$k] = @unserialize($d[$k]);
 228                      } catch (Exception $e) {}
 229                  }
 230                  $d[$k] = is_array($d[$k])
 231                      ? array_merge($d[$k], $v)
 232                      : $v;
 233                  if (isset($s[$k])) {
 234                      $updated[$s[$k]] = true;
 235                  }
 236              } else {
 237                  $d[$k] = $v;
 238                  $add[] = $k;
 239              }
 240          }
 241  
 242          $this->_toUpdate($mailbox, 'add', $add);
 243          $this->_toUpdate($mailbox, 'slice', array_keys($updated));
 244      }
 245  
 246      /**
 247       */
 248      public function getMetaData($mailbox, $uidvalid, $entries)
 249      {
 250          $this->_loadSliceMap($mailbox, $uidvalid);
 251  
 252          return empty($entries)
 253              ? $this->_slicemap[$mailbox]['d']
 254              : array_intersect_key($this->_slicemap[$mailbox]['d'], array_flip($entries));
 255      }
 256  
 257      /**
 258       */
 259      public function setMetaData($mailbox, $data)
 260      {
 261          $this->_loadSliceMap($mailbox, isset($data['uidvalid']) ? $data['uidvalid'] : null);
 262          $this->_slicemap[$mailbox]['d'] = array_merge($this->_slicemap[$mailbox]['d'], $data);
 263          $this->_toUpdate($mailbox, 'slicemap', true);
 264      }
 265  
 266      /**
 267       */
 268      public function deleteMsgs($mailbox, $uids)
 269      {
 270          if (empty($uids)) {
 271              return;
 272          }
 273  
 274          $this->_loadSliceMap($mailbox);
 275  
 276          $slicemap = &$this->_slicemap[$mailbox];
 277          $deleted = array_intersect_key($slicemap['s'], array_flip($uids));
 278  
 279          if (isset($this->_update[$mailbox])) {
 280              $this->_update[$mailbox]['add'] = array_diff(
 281                  $this->_update[$mailbox]['add'],
 282                  $uids
 283              );
 284          }
 285  
 286          if (empty($deleted)) {
 287              return;
 288          }
 289  
 290          $this->_loadUids($mailbox, array_keys($deleted));
 291          $d = &$this->_data[$mailbox];
 292  
 293          foreach (array_keys($deleted) as $id) {
 294              unset($d[$id], $slicemap['s'][$id]);
 295          }
 296  
 297          foreach (array_unique($deleted) as $slice) {
 298              /* Get rid of slice if less than 10% of capacity. */
 299              if (($slice != $slicemap['i']) &&
 300                  ($slice_uids = array_keys($slicemap['s'], $slice)) &&
 301                  ($this->_params['slicesize'] * 0.1) > count($slice_uids)) {
 302                  $this->_toUpdate($mailbox, 'add', $slice_uids);
 303                  $this->_cache->expire($this->_getCid($mailbox, $slice));
 304                  foreach ($slice_uids as $val) {
 305                      unset($slicemap['s'][$val]);
 306                  }
 307              } else {
 308                  $this->_toUpdate($mailbox, 'slice', array($slice));
 309              }
 310          }
 311      }
 312  
 313      /**
 314       */
 315      public function deleteMailbox($mailbox)
 316      {
 317          $this->_loadSliceMap($mailbox);
 318          $this->_deleteMailbox($mailbox);
 319      }
 320  
 321      /**
 322       */
 323      public function clear($lifetime)
 324      {
 325          $this->_cache->clear();
 326          $this->_data = $this->_loaded = $this->_slicemap = $this->_update = array();
 327      }
 328  
 329      /**
 330       * Create the unique ID used to store the data in the cache.
 331       *
 332       * @param string $mailbox  The mailbox to cache.
 333       * @param string $slice    The cache slice.
 334       *
 335       * @return string  The cache ID.
 336       */
 337      protected function _getCid($mailbox, $slice)
 338      {
 339          return implode('|', array(
 340              'horde_imap_client',
 341              $this->_params['username'],
 342              $mailbox,
 343              $this->_params['hostspec'],
 344              $this->_params['port'],
 345              $slice,
 346              self::VERSION
 347          ));
 348      }
 349  
 350      /**
 351       * Delete a mailbox from the cache.
 352       *
 353       * @param string $mbox  The mailbox to delete.
 354       */
 355      protected function _deleteMailbox($mbox)
 356      {
 357          foreach (array_merge(array_keys(array_flip($this->_slicemap[$mbox]['s'])), array('slicemap')) as $slice) {
 358              $cid = $this->_getCid($mbox, $slice);
 359              $this->_cache->expire($cid);
 360              unset($this->_loaded[$cid]);
 361          }
 362  
 363          unset(
 364              $this->_data[$mbox],
 365              $this->_slicemap[$mbox],
 366              $this->_update[$mbox]
 367          );
 368      }
 369  
 370      /**
 371       * Load UIDs by regenerating from the cache.
 372       *
 373       * @param string $mailbox    The mailbox to load.
 374       * @param array $uids        The UIDs to load.
 375       * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
 376       */
 377      protected function _loadUids($mailbox, $uids, $uidvalid = null)
 378      {
 379          if (!isset($this->_data[$mailbox])) {
 380              $this->_data[$mailbox] = array();
 381          }
 382  
 383          $this->_loadSliceMap($mailbox, $uidvalid);
 384  
 385          if (!empty($uids)) {
 386              foreach (array_unique(array_intersect_key($this->_slicemap[$mailbox]['s'], array_flip($uids))) as $slice) {
 387                  $this->_loadSlice($mailbox, $slice);
 388              }
 389          }
 390      }
 391  
 392      /**
 393       * Load UIDs from a cache slice.
 394       *
 395       * @param string $mailbox  The mailbox to load.
 396       * @param integer $slice   The slice to load.
 397       */
 398      protected function _loadSlice($mailbox, $slice)
 399      {
 400          $cache_id = $this->_getCid($mailbox, $slice);
 401  
 402          if (!empty($this->_loaded[$cache_id])) {
 403              return;
 404          }
 405  
 406          if (($data = $this->_cache->get($cache_id, 0)) !== false) {
 407              try {
 408                  $data = @unserialize($data);
 409              } catch (Exception $e) {}
 410          }
 411  
 412          if (($data !== false) && is_array($data)) {
 413              $this->_data[$mailbox] += $data;
 414              $this->_loaded[$cache_id] = true;
 415          } else {
 416              $ptr = &$this->_slicemap[$mailbox];
 417  
 418              // Slice data is corrupt; remove from slicemap.
 419              foreach (array_keys($ptr['s'], $slice) as $val) {
 420                  unset($ptr['s'][$val]);
 421              }
 422  
 423              if ($slice == $ptr['i']) {
 424                  $ptr['c'] = 0;
 425              }
 426          }
 427      }
 428  
 429      /**
 430       * Load the slicemap for a given mailbox.  The slicemap contains
 431       * the uidvalidity information, the UIDs->slice lookup table, and any
 432       * metadata that needs to be saved for the mailbox.
 433       *
 434       * @param string $mailbox    The mailbox.
 435       * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
 436       */
 437      protected function _loadSliceMap($mailbox, $uidvalid = null)
 438      {
 439          if (!isset($this->_slicemap[$mailbox]) &&
 440              (($data = $this->_cache->get($this->_getCid($mailbox, 'slicemap'), 0)) !== false)) {
 441              try {
 442                  if (($slice = @unserialize($data)) &&
 443                      is_array($slice)) {
 444                      $this->_slicemap[$mailbox] = $slice;
 445                  }
 446              } catch (Exception $e) {}
 447          }
 448  
 449          if (isset($this->_slicemap[$mailbox])) {
 450              $ptr = &$this->_slicemap[$mailbox];
 451              if (is_null($ptr['d']['uidvalid'])) {
 452                  $ptr['d']['uidvalid'] = $uidvalid;
 453                  return;
 454              } elseif (!is_null($uidvalid) &&
 455                        ($ptr['d']['uidvalid'] != $uidvalid)) {
 456                  $this->_deleteMailbox($mailbox);
 457              } else {
 458                  return;
 459              }
 460          }
 461  
 462          $this->_slicemap[$mailbox] = array(
 463              // Tracking count for purposes of determining slices
 464              'c' => 0,
 465              // Metadata storage
 466              // By default includes UIDVALIDITY of mailbox.
 467              'd' => array('uidvalid' => $uidvalid),
 468              // The ID of the last slice.
 469              'i' => 0,
 470              // The slice list.
 471              's' => array()
 472          );
 473      }
 474  
 475      /**
 476       * Add update entry for a mailbox.
 477       *
 478       * @param string $mailbox  The mailbox.
 479       * @param string $type     'add', 'slice', or 'slicemap'.
 480       * @param mixed $data      The data to update.
 481       */
 482      protected function _toUpdate($mailbox, $type, $data)
 483      {
 484          if (!isset($this->_update[$mailbox])) {
 485              $this->_update[$mailbox] = array(
 486                  'add' => array(),
 487                  'slice' => array()
 488              );
 489          }
 490  
 491          $this->_update[$mailbox][$type] = ($type == 'slicemap')
 492              ? $data
 493              : array_merge($this->_update[$mailbox][$type], $data);
 494      }
 495  
 496      /* Serializable methods. */
 497  
 498      /**
 499       */
 500      public function serialize()
 501      {
 502          $this->save();
 503          return parent::serialize();
 504      }
 505  
 506  }