Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   1  <?php
   2  /**
   3   * Copyright 2013-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 2013-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Imap_Client
  12   */
  13  
  14  /**
  15   * A MongoDB database implementation for caching IMAP/POP data.
  16   *
  17   * Requires the Horde_Mongo class.
  18   *
  19   * @author    Michael Slusarz <slusarz@horde.org>
  20   * @category  Horde
  21   * @copyright 2013-2017 Horde LLC
  22   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  23   * @package   Imap_Client
  24   */
  25  class Horde_Imap_Client_Cache_Backend_Mongo
  26  extends Horde_Imap_Client_Cache_Backend
  27  implements Horde_Mongo_Collection_Index
  28  {
  29      /** Mongo collection names. */
  30      const BASE = 'horde_imap_client_cache_data';
  31      const MD = 'horde_imap_client_cache_metadata';
  32      const MSG = 'horde_imap_client_cache_message';
  33  
  34      /** Mongo field names: BASE collection. */
  35      const BASE_HOSTSPEC = 'hostspec';
  36      const BASE_MAILBOX = 'mailbox';
  37      const BASE_MODIFIED = 'modified';
  38      const BASE_PORT = 'port';
  39      const BASE_UID = 'data';
  40      const BASE_USERNAME = 'username';
  41  
  42      /** Mongo field names: MD collection. */
  43      const MD_DATA = 'data';
  44      const MD_FIELD = 'field';
  45      const MD_UID = 'uid';
  46  
  47      /** Mongo field names: MSG collection. */
  48      const MSG_DATA = 'data';
  49      const MSG_MSGUID = 'msguid';
  50      const MSG_UID = 'uid';
  51  
  52      /**
  53       * The MongoDB object for the cache data.
  54       *
  55       * @var MongoDB
  56       */
  57      protected $_db;
  58  
  59      /**
  60       * The list of indices.
  61       *
  62       * @var array
  63       */
  64      protected $_indices = array(
  65          self::BASE => array(
  66              'base_index_1' => array(
  67                  self::BASE_HOSTSPEC => 1,
  68                  self::BASE_MAILBOX => 1,
  69                  self::BASE_PORT => 1,
  70                  self::BASE_USERNAME => 1,
  71              )
  72          ),
  73          self::MSG => array(
  74              'msg_index_1' => array(
  75                  self::MSG_MSGUID => 1,
  76                  self::MSG_UID => 1
  77              )
  78          )
  79      );
  80  
  81      /**
  82       * Constructor.
  83       *
  84       * @param array $params  Configuration parameters:
  85       * <pre>
  86       *   - REQUIRED parameters:
  87       *     - mongo_db: (Horde_Mongo_Client) A MongoDB client object.
  88       * </pre>
  89       */
  90      public function __construct(array $params = array())
  91      {
  92          if (!isset($params['mongo_db'])) {
  93              throw new InvalidArgumentException('Missing mongo_db parameter.');
  94          }
  95  
  96          parent::__construct($params);
  97      }
  98  
  99      /**
 100       */
 101      protected function _initOb()
 102      {
 103          $this->_db = $this->_params['mongo_db']->selectDB(null);
 104      }
 105  
 106      /**
 107       */
 108      public function get($mailbox, $uids, $fields, $uidvalid)
 109      {
 110          $this->getMetaData($mailbox, $uidvalid, array('uidvalid'));
 111  
 112          if (!($uid = $this->_getUid($mailbox))) {
 113              return array();
 114          }
 115  
 116          $out = array();
 117          $query = array(
 118              self::MSG_MSGUID => array('$in' => array_map('strval', $uids)),
 119              self::MSG_UID => $uid
 120          );
 121  
 122          try {
 123              $cursor = $this->_db->selectCollection(self::MSG)->find(
 124                  $query,
 125                  array(self::MSG_DATA => true, self::MSG_MSGUID => true)
 126              );
 127              foreach ($cursor as $val) {
 128                  try {
 129                      $out[$val[self::MSG_MSGUID]] = $this->_value($val[self::MSG_DATA]);
 130                  } catch (Exception $e) {}
 131              }
 132          } catch (MongoException $e) {}
 133  
 134          return $out;
 135      }
 136  
 137      /**
 138       */
 139      public function getCachedUids($mailbox, $uidvalid)
 140      {
 141          $this->getMetaData($mailbox, $uidvalid, array('uidvalid'));
 142  
 143          if (!($uid = $this->_getUid($mailbox))) {
 144              return array();
 145          }
 146  
 147          $out = array();
 148          $query = array(
 149              self::MSG_UID => $uid
 150          );
 151  
 152          try {
 153              $cursor = $this->_db->selectCollection(self::MSG)->find(
 154                  $query, array(self::MSG_MSGUID => true)
 155              );
 156              foreach ($cursor as $val) {
 157                  $out[] = $val[self::MSG_MSGUID];
 158              }
 159          } catch (MongoException $e) {}
 160  
 161          return $out;
 162      }
 163  
 164      /**
 165       */
 166      public function set($mailbox, $data, $uidvalid)
 167      {
 168          if ($uid = $this->_getUid($mailbox)) {
 169              $res = $this->get($mailbox, array_keys($data), array(), $uidvalid);
 170          } else {
 171              $res = array();
 172              $uid = $this->_createUid($mailbox);
 173          }
 174  
 175          $coll = $this->_db->selectCollection(self::MSG);
 176  
 177          foreach ($data as $key => $val) {
 178              try {
 179                  if (isset($res[$key])) {
 180                      $coll->update(array(
 181                          self::MSG_MSGUID => strval($key),
 182                          self::MSG_UID => $uid
 183                      ), array(
 184                          self::MSG_DATA => $this->_value(array_merge($res[$key], $val)),
 185                          self::MSG_MSGUID => strval($key),
 186                          self::MSG_UID => $uid
 187                      ));
 188                  } else {
 189                      $doc = array(
 190                          self::MSG_DATA => $this->_value($val),
 191                          self::MSG_MSGUID => strval($key),
 192                          self::MSG_UID => $uid
 193                      );
 194                      $coll->insert($doc);
 195                  }
 196              } catch (MongoException $e) {}
 197          }
 198  
 199          /* Update modified time. */
 200          try {
 201              $this->_db->selectCollection(self::BASE)->update(
 202                  array(self::BASE_UID => $uid),
 203                  array(self::BASE_MODIFIED => time())
 204              );
 205          } catch (MongoException $e) {}
 206  
 207          /* Update uidvalidity. */
 208          $this->setMetaData($mailbox, array('uidvalid' => $uidvalid));
 209      }
 210  
 211      /**
 212       */
 213      public function getMetaData($mailbox, $uidvalid, $entries)
 214      {
 215          if (!($uid = $this->_getUid($mailbox))) {
 216              return array();
 217          }
 218  
 219          $out = array();
 220          $query = array(
 221              self::MD_UID => $uid
 222          );
 223  
 224          if (!empty($entries)) {
 225              $entries[] = 'uidvalid';
 226              $query[self::MD_FIELD] = array(
 227                  '$in' => array_unique($entries)
 228              );
 229          }
 230  
 231          try {
 232              $cursor = $this->_db->selectCollection(self::MD)->find(
 233                  $query,
 234                  array(self::MD_DATA => true, self::MD_FIELD => true)
 235              );
 236              foreach ($cursor as $val) {
 237                  try {
 238                      $out[$val[self::MD_FIELD]] = $this->_value($val[self::MD_DATA]);
 239                  } catch (Exception $e) {}
 240              }
 241  
 242              if (is_null($uidvalid) ||
 243                  !isset($out['uidvalid']) ||
 244                  ($out['uidvalid'] == $uidvalid)) {
 245                  return $out;
 246              }
 247  
 248              $this->deleteMailbox($mailbox);
 249          } catch (MongoException $e) {}
 250  
 251          return array();
 252      }
 253  
 254      /**
 255       */
 256      public function setMetaData($mailbox, $data)
 257      {
 258          if (!($uid = $this->_getUid($mailbox))) {
 259              $uid = $this->_createUid($mailbox);
 260          }
 261  
 262          $coll = $this->_db->selectCollection(self::MD);
 263  
 264          foreach ($data as $key => $val) {
 265              try {
 266                  $coll->update(
 267                      array(
 268                          self::MD_FIELD => $key,
 269                          self::MD_UID => $uid
 270                      ),
 271                      array(
 272                          self::MD_DATA => $this->_value($val),
 273                          self::MD_FIELD => $key,
 274                          self::MD_UID => $uid
 275                      ),
 276                      array('upsert' => true)
 277                  );
 278              } catch (MongoException $e) {}
 279          }
 280      }
 281  
 282      /**
 283       */
 284      public function deleteMsgs($mailbox, $uids)
 285      {
 286          if (!empty($uids) && ($uid = $this->_getUid($mailbox))) {
 287              try {
 288                  $this->_db->selectCollection(self::MSG)->remove(array(
 289                      self::MSG_MSGUID => array(
 290                          '$in' => array_map('strval', $uids)
 291                      ),
 292                      self::MSG_UID => $uid
 293                  ));
 294              } catch (MongoException $e) {}
 295          }
 296      }
 297  
 298      /**
 299       */
 300      public function deleteMailbox($mailbox)
 301      {
 302          if (!($uid = $this->_getUid($mailbox))) {
 303              return;
 304          }
 305  
 306          foreach (array(self::BASE, self::MD, self::MSG) as $val) {
 307              try {
 308                  $this->_db->selectCollection($val)
 309                      ->remove(array('uid' => $uid));
 310              } catch (MongoException $e) {}
 311          }
 312      }
 313  
 314      /**
 315       */
 316      public function clear($lifetime)
 317      {
 318          if (is_null($lifetime)) {
 319              foreach (array(self::BASE, self::MD, self::MSG) as $val) {
 320                  $this->_db->selectCollection($val)->drop();
 321              }
 322              return;
 323          }
 324  
 325          $query = array(
 326              self::BASE_MODIFIED => array('$lt' => (time() - $lifetime))
 327          );
 328          $uids = array();
 329  
 330          try {
 331              $cursor = $this->_db->selectCollection(self::BASE)->find($query);
 332              foreach ($cursor as $val) {
 333                  $uids[] = strval($val['_id']);
 334              }
 335          } catch (MongoException $e) {}
 336  
 337          if (empty($uids)) {
 338              return;
 339          }
 340  
 341          foreach (array(self::BASE, self::MD, self::MSG) as $val) {
 342              try {
 343                  $this->_db->selectCollection($val)
 344                      ->remove(array('uid' => array('$in' => $uids)));
 345              } catch (MongoException $e) {}
 346          }
 347      }
 348  
 349      /**
 350       * Return the UID for a mailbox/user/server combo.
 351       *
 352       * @param string $mailbox  Mailbox name.
 353       *
 354       * @return string  UID from base table.
 355       */
 356      protected function _getUid($mailbox)
 357      {
 358          $query = array(
 359              self::BASE_HOSTSPEC => $this->_params['hostspec'],
 360              self::BASE_MAILBOX => $mailbox,
 361              self::BASE_PORT => $this->_params['port'],
 362              self::BASE_USERNAME => $this->_params['username']
 363          );
 364  
 365          try {
 366              if ($result = $this->_db->selectCollection(self::BASE)->findOne($query)) {
 367                  return strval($result['_id']);
 368              }
 369          } catch (MongoException $e) {}
 370  
 371          return null;
 372      }
 373  
 374      /**
 375       * Create and return the UID for a mailbox/user/server combo.
 376       *
 377       * @param string $mailbox  Mailbox name.
 378       *
 379       * @return string  UID from base table.
 380       */
 381      protected function _createUid($mailbox)
 382      {
 383          $doc = array(
 384              self::BASE_HOSTSPEC => $this->_params['hostspec'],
 385              self::BASE_MAILBOX => $mailbox,
 386              self::BASE_PORT => $this->_params['port'],
 387              self::BASE_USERNAME => $this->_params['username']
 388          );
 389          $this->_db->selectCollection(self::BASE)->insert($doc);
 390  
 391          return $this->_getUid($mailbox);
 392      }
 393  
 394      /**
 395       * Convert data from/to storage format.
 396       *
 397       * @param mixed|MongoBinData $data  The data object.
 398       *
 399       * @return mixed|MongoBinData  The converted data.
 400       */
 401      protected function _value($data)
 402      {
 403          static $compress;
 404  
 405          if (!isset($compress)) {
 406              $compress = new Horde_Compress_Fast();
 407          }
 408  
 409          return ($data instanceof MongoBinData)
 410              ? @unserialize($compress->decompress($data->bin))
 411              : new MongoBinData(
 412                  $compress->compress(serialize($data)), MongoBinData::BYTE_ARRAY
 413              );
 414      }
 415  
 416      /* Horde_Mongo_Collection_Index methods. */
 417  
 418      /**
 419       */
 420      public function checkMongoIndices()
 421      {
 422          foreach ($this->_indices as $key => $val) {
 423              if (!$this->_params['mongo_db']->checkIndices($key, $val)) {
 424                  return false;
 425              }
 426          }
 427  
 428          return true;
 429      }
 430  
 431      /**
 432       */
 433      public function createMongoIndices()
 434      {
 435          foreach ($this->_indices as $key => $val) {
 436              $this->_params['mongo_db']->createIndices($key, $val);
 437          }
 438      }
 439  
 440  }