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 310 and 311] [Versions 39 and 311]

   1  <?php
   2  /**
   3   * Copyright 2012-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 2012-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Imap_Client
  12   */
  13  
  14  /**
  15   * Client sorting methods for the Socket driver.
  16   *
  17   * NOTE: This class is NOT intended to be accessed outside of a Base object.
  18   * There is NO guarantees that the API of this class will not change across
  19   * versions.
  20   *
  21   * @author    Michael Slusarz <slusarz@horde.org>
  22   * @category  Horde
  23   * @copyright 2012-2017 Horde LLC
  24   * @internal
  25   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  26   * @package   Imap_Client
  27   */
  28  class Horde_Imap_Client_Socket_ClientSort
  29  {
  30      /**
  31       * Collator object to use for sotring.
  32       *
  33       * @var Collator
  34       */
  35      protected $_collator;
  36  
  37      /**
  38       * Socket object.
  39       *
  40       * @var Horde_Imap_Client_Socket
  41       */
  42      protected $_socket;
  43  
  44      /**
  45       * Constructor.
  46       *
  47       * @param Horde_Imap_Client_Socket $socket  Socket object.
  48       */
  49      public function __construct(Horde_Imap_Client_Socket $socket)
  50      {
  51          $this->_socket = $socket;
  52  
  53          if (class_exists('Collator')) {
  54              $this->_collator = new Collator(null);
  55          }
  56      }
  57  
  58      /**
  59       * Sort search results client side if the server does not support the SORT
  60       * IMAP extension (RFC 5256).
  61       *
  62       * @param Horde_Imap_Client_Ids $res  The search results.
  63       * @param array $opts                 The options to _search().
  64       *
  65       * @return array  The sort results.
  66       *
  67       * @throws Horde_Imap_Client_Exception
  68       */
  69      public function clientSort($res, $opts)
  70      {
  71          if (!count($res)) {
  72              return $res;
  73          }
  74  
  75          /* Generate the FETCH command needed. */
  76          $query = new Horde_Imap_Client_Fetch_Query();
  77  
  78          foreach ($opts['sort'] as $val) {
  79              switch ($val) {
  80              case Horde_Imap_Client::SORT_ARRIVAL:
  81                  $query->imapDate();
  82                  break;
  83  
  84              case Horde_Imap_Client::SORT_DATE:
  85                  $query->imapDate();
  86                  $query->envelope();
  87                  break;
  88  
  89              case Horde_Imap_Client::SORT_CC:
  90              case Horde_Imap_Client::SORT_DISPLAYFROM:
  91              case Horde_Imap_Client::SORT_DISPLAYTO:
  92              case Horde_Imap_Client::SORT_FROM:
  93              case Horde_Imap_Client::SORT_SUBJECT:
  94              case Horde_Imap_Client::SORT_TO:
  95                  $query->envelope();
  96                  break;
  97  
  98              case Horde_Imap_Client::SORT_SEQUENCE:
  99                  $query->seq();
 100                  break;
 101  
 102              case Horde_Imap_Client::SORT_SIZE:
 103                  $query->size();
 104                  break;
 105              }
 106          }
 107  
 108          if (!count($query)) {
 109              return $res;
 110          }
 111  
 112          $mbox = $this->_socket->currentMailbox();
 113          $fetch_res = $this->_socket->fetch(isset($mbox['mailbox']) ? $mbox['mailbox'] : null, $query, array(
 114              'ids' => $res
 115          ));
 116  
 117          return $this->_clientSortProcess($res->ids, $fetch_res, $opts['sort']);
 118      }
 119  
 120      /**
 121       * If server does not support the THREAD IMAP extension (RFC 5256), do
 122       * ORDEREDSUBJECT threading on the client side.
 123       *
 124       * @param Horde_Imap_Client_Fetch_Results $data  Fetch results.
 125       * @param boolean $uids                          Are IDs UIDs?
 126       *
 127       * @return array  The thread sort results.
 128       */
 129      public function threadOrderedSubject(Horde_Imap_Client_Fetch_Results $data,
 130                                           $uids)
 131      {
 132          $dates = $this->_getSentDates($data, $data->ids());
 133          $out = $sorted = $tsort = array();
 134  
 135          foreach ($data as $k => $v) {
 136              $subject = strval(new Horde_Imap_Client_Data_BaseSubject($v->getEnvelope()->subject));
 137              $sorted[$subject][$k] = $dates[$k];
 138          }
 139  
 140          /* Step 1: Sort by base subject (already done).
 141           * Step 2: Sort by sent date within each thread. */
 142          foreach (array_keys($sorted) as $key) {
 143              $this->_stableAsort($sorted[$key]);
 144              $tsort[$key] = reset($sorted[$key]);
 145          }
 146  
 147          /* Step 3: Sort by the sent date of the first message in the
 148           * thread. */
 149          $this->_stableAsort($tsort);
 150  
 151          /* Now, $tsort contains the order of the threads, and each thread
 152           * is sorted in $sorted. */
 153          foreach (array_keys($tsort) as $key) {
 154              $keys = array_keys($sorted[$key]);
 155              $out[$keys[0]] = array(
 156                  $keys[0] => 0
 157              ) + array_fill_keys(array_slice($keys, 1) , 1);
 158          }
 159  
 160          return new Horde_Imap_Client_Data_Thread(
 161              $out,
 162              $uids ? 'uid' : 'sequence'
 163          );
 164      }
 165  
 166      /**
 167       */
 168      protected function _clientSortProcess($res, $fetch_res, $sort)
 169      {
 170          /* The initial sort is on the entire set. */
 171          $slices = array(0 => $res);
 172          $reverse = false;
 173  
 174          foreach ($sort as $val) {
 175              if ($val == Horde_Imap_Client::SORT_REVERSE) {
 176                  $reverse = true;
 177                  continue;
 178              }
 179  
 180              $slices_list = $slices;
 181              $slices = array();
 182  
 183              foreach ($slices_list as $slice_start => $slice) {
 184                  $sorted = array();
 185  
 186                  switch ($val) {
 187                  case Horde_Imap_Client::SORT_SEQUENCE:
 188                      /* There is no requirement that IDs be returned in
 189                       * sequence order (see RFC 4549 [4.3.1]). So we must sort
 190                       * ourselves. */
 191                      $sorted = array_flip($slice);
 192                      ksort($sorted, SORT_NUMERIC);
 193                      break;
 194  
 195                  case Horde_Imap_Client::SORT_SIZE:
 196                      foreach ($slice as $num) {
 197                          $sorted[$num] = $fetch_res[$num]->getSize();
 198                      }
 199                      asort($sorted, SORT_NUMERIC);
 200                      break;
 201  
 202                  case Horde_Imap_Client::SORT_DISPLAYFROM:
 203                  case Horde_Imap_Client::SORT_DISPLAYTO:
 204                      $field = ($val == Horde_Imap_Client::SORT_DISPLAYFROM)
 205                          ? 'from'
 206                          : 'to';
 207  
 208                      foreach ($slice as $num) {
 209                          $ob = $fetch_res[$num]->getEnvelope()->$field;
 210                          $sorted[$num] = ($addr_ob = $ob[0])
 211                              ? $addr_ob->personal ?: $addr_ob->mailbox
 212                              : null;
 213                      }
 214  
 215                      $this->_sortString($sorted);
 216                      break;
 217  
 218                  case Horde_Imap_Client::SORT_CC:
 219                  case Horde_Imap_Client::SORT_FROM:
 220                  case Horde_Imap_Client::SORT_TO:
 221                      if ($val == Horde_Imap_Client::SORT_CC) {
 222                          $field = 'cc';
 223                      } elseif ($val == Horde_Imap_Client::SORT_FROM) {
 224                          $field = 'from';
 225                      } else {
 226                          $field = 'to';
 227                      }
 228  
 229                      foreach ($slice as $num) {
 230                          $tmp = $fetch_res[$num]->getEnvelope()->$field;
 231                          $sorted[$num] = count($tmp)
 232                              ? $tmp[0]->mailbox
 233                              : null;
 234                      }
 235  
 236                      $this->_sortString($sorted);
 237                      break;
 238  
 239                  case Horde_Imap_Client::SORT_ARRIVAL:
 240                      $sorted = $this->_getSentDates($fetch_res, $slice, true);
 241                      asort($sorted, SORT_NUMERIC);
 242                      break;
 243  
 244                  case Horde_Imap_Client::SORT_DATE:
 245                      // Date sorting rules in RFC 5256 [2.2]
 246                      $sorted = $this->_getSentDates($fetch_res, $slice);
 247                      asort($sorted, SORT_NUMERIC);
 248                      break;
 249  
 250                  case Horde_Imap_Client::SORT_SUBJECT:
 251                      // Subject sorting rules in RFC 5256 [2.1]
 252                      foreach ($slice as $num) {
 253                          $sorted[$num] = strval(new Horde_Imap_Client_Data_BaseSubject($fetch_res[$num]->getEnvelope()->subject));
 254                      }
 255  
 256                      $this->_sortString($sorted);
 257                      break;
 258                  }
 259  
 260                  // At this point, keys of $sorted are sequence/UID and values
 261                  // are the sort strings
 262                  if (!empty($sorted)) {
 263                      if ($reverse) {
 264                          $sorted = array_reverse($sorted, true);
 265                      }
 266  
 267                      if (count($sorted) === count($res)) {
 268                          $res = array_keys($sorted);
 269                      } else {
 270                          array_splice($res, $slice_start, count($slice), array_keys($sorted));
 271                      }
 272  
 273                      // Check for ties.
 274                      $last = $start = null;
 275                      $i = 0;
 276                      $todo = array();
 277  
 278                      foreach ($sorted as $k => $v) {
 279                          if (is_null($last) || ($last != $v)) {
 280                              if ($i) {
 281                                  $todo[] = array($start, $i);
 282                                  $i = 0;
 283                              }
 284                              $last = $v;
 285                              $start = $k;
 286                          } else {
 287                              ++$i;
 288                          }
 289                      }
 290                      if ($i) {
 291                          $todo[] = array($start, $i);
 292                      }
 293  
 294                      foreach ($todo as $v) {
 295                          $slices[array_search($v[0], $res)] = array_keys(
 296                              array_slice(
 297                                  $sorted,
 298                                  array_search($v[0], $sorted),
 299                                  $v[1] + 1,
 300                                  true
 301                              )
 302                          );
 303                      }
 304                  }
 305              }
 306  
 307              $reverse = false;
 308          }
 309  
 310          return $res;
 311      }
 312  
 313      /**
 314       * Get the sent dates for purposes of SORT/THREAD sorting under RFC 5256
 315       * [2.2].
 316       *
 317       * @param Horde_Imap_Client_Fetch_Results $data  Data returned from
 318       *                                               fetch() that includes
 319       *                                               both date and envelope
 320       *                                               items.
 321       * @param array $ids                             The IDs to process.
 322       * @param boolean $internal                      Only use internal date?
 323       *
 324       * @return array  A mapping of IDs -> UNIX timestamps.
 325       */
 326      protected function _getSentDates(Horde_Imap_Client_Fetch_Results $data,
 327                                       $ids, $internal = false)
 328      {
 329          $dates = array();
 330  
 331          foreach ($ids as $num) {
 332              $dt = ($internal || !isset($data[$num]->getEnvelope()->date))
 333                  // RFC 5256 [3] & 3501 [6.4.4]: disregard timezone when
 334                  // using internaldate.
 335                  ? $data[$num]->getImapDate()
 336                  : $data[$num]->getEnvelope()->date;
 337              $dates[$num] = $dt->format('U');
 338          }
 339  
 340          return $dates;
 341      }
 342  
 343      /**
 344       * Stable asort() function.
 345       *
 346       * PHP's asort() (BWT) is not a stable sort - identical values have no
 347       * guarantee of key order. Use Schwartzian Transform instead. See:
 348       * http://notmysock.org/blog/php/schwartzian-transform.html
 349       *
 350       * @param array &$a  Array to sort.
 351       */
 352      protected function _stableAsort(&$a)
 353      {
 354          array_walk($a, function(&$v, $k) { $v = array($v, $k); });
 355          asort($a);
 356          array_walk($a, function(&$v, $k) { $v = $v[0]; });
 357      }
 358  
 359      /**
 360       * Sort an array of strings based on current locale.
 361       *
 362       * @param array &$sorted  Array of strings.
 363       */
 364      protected function _sortString(&$sorted)
 365      {
 366          if (empty($this->_collator)) {
 367              asort($sorted, SORT_LOCALE_STRING);
 368          } else {
 369              $this->_collator->asort($sorted, Collator::SORT_STRING);
 370          }
 371      }
 372  
 373  }