Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   1  <?php
   2  /*
   3   * Copyright 2015-present MongoDB, 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   *   https://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  namespace MongoDB;
  19  
  20  use Exception;
  21  use MongoDB\BSON\Serializable;
  22  use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
  23  use MongoDB\Driver\Manager;
  24  use MongoDB\Driver\ReadPreference;
  25  use MongoDB\Driver\Server;
  26  use MongoDB\Driver\Session;
  27  use MongoDB\Driver\WriteConcern;
  28  use MongoDB\Exception\InvalidArgumentException;
  29  use MongoDB\Exception\RuntimeException;
  30  use MongoDB\Operation\ListCollections;
  31  use MongoDB\Operation\WithTransaction;
  32  use ReflectionClass;
  33  use ReflectionException;
  34  
  35  use function assert;
  36  use function end;
  37  use function get_object_vars;
  38  use function in_array;
  39  use function is_array;
  40  use function is_object;
  41  use function is_string;
  42  use function key;
  43  use function MongoDB\BSON\fromPHP;
  44  use function MongoDB\BSON\toPHP;
  45  use function reset;
  46  use function substr;
  47  
  48  /**
  49   * Check whether all servers support executing a write stage on a secondary.
  50   *
  51   * @internal
  52   * @param Server[] $servers
  53   */
  54  function all_servers_support_write_stage_on_secondary(array $servers): bool
  55  {
  56      /* Write stages on secondaries are technically supported by FCV 4.4, but the
  57       * CRUD spec requires all 5.0+ servers since FCV is not tracked by SDAM. */
  58      static $wireVersionForWriteStageOnSecondary = 13;
  59  
  60      foreach ($servers as $server) {
  61          // We can assume that load balancers only front 5.0+ servers
  62          if ($server->getType() === Server::TYPE_LOAD_BALANCER) {
  63              continue;
  64          }
  65  
  66          if (! server_supports_feature($server, $wireVersionForWriteStageOnSecondary)) {
  67              return false;
  68          }
  69      }
  70  
  71      return true;
  72  }
  73  
  74  /**
  75   * Applies a type map to a document.
  76   *
  77   * This function is used by operations where it is not possible to apply a type
  78   * map to the cursor directly because the root document is a command response
  79   * (e.g. findAndModify).
  80   *
  81   * @internal
  82   * @param array|object $document Document to which the type map will be applied
  83   * @param array        $typeMap  Type map for BSON deserialization.
  84   * @return array|object
  85   * @throws InvalidArgumentException
  86   */
  87  function apply_type_map_to_document($document, array $typeMap)
  88  {
  89      if (! is_array($document) && ! is_object($document)) {
  90          throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
  91      }
  92  
  93      return toPHP(fromPHP($document), $typeMap);
  94  }
  95  
  96  /**
  97   * Generate an index name from a key specification.
  98   *
  99   * @internal
 100   * @param array|object $document Document containing fields mapped to values,
 101   *                               which denote order or an index type
 102   * @throws InvalidArgumentException
 103   */
 104  function generate_index_name($document): string
 105  {
 106      if ($document instanceof Serializable) {
 107          $document = $document->bsonSerialize();
 108      }
 109  
 110      if (is_object($document)) {
 111          $document = get_object_vars($document);
 112      }
 113  
 114      if (! is_array($document)) {
 115          throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
 116      }
 117  
 118      $name = '';
 119  
 120      foreach ($document as $field => $type) {
 121          $name .= ($name != '' ? '_' : '') . $field . '_' . $type;
 122      }
 123  
 124      return $name;
 125  }
 126  
 127  /**
 128   * Return a collection's encryptedFields from the encryptedFieldsMap
 129   * autoEncryption driver option (if available).
 130   *
 131   * @internal
 132   * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
 133   * @see Collection::drop
 134   * @see Database::createCollection
 135   * @see Database::dropCollection
 136   * @return array|object|null
 137   */
 138  function get_encrypted_fields_from_driver(string $databaseName, string $collectionName, Manager $manager)
 139  {
 140      $encryptedFieldsMap = (array) $manager->getEncryptedFieldsMap();
 141  
 142      return $encryptedFieldsMap[$databaseName . '.' . $collectionName] ?? null;
 143  }
 144  
 145  /**
 146   * Return a collection's encryptedFields option from the server (if any).
 147   *
 148   * @internal
 149   * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
 150   * @see Collection::drop
 151   * @see Database::dropCollection
 152   * @return array|object|null
 153   */
 154  function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server)
 155  {
 156      // No-op if the encryptedFieldsMap autoEncryption driver option was omitted
 157      if ($manager->getEncryptedFieldsMap() === null) {
 158          return null;
 159      }
 160  
 161      $collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server);
 162  
 163      foreach ($collectionInfoIterator as $collectionInfo) {
 164          /* Note: ListCollections applies a typeMap that converts BSON documents
 165           * to PHP arrays. This should not be problematic as encryptedFields here
 166           * is only used by drop helpers to obtain names of supporting encryption
 167           * collections. */
 168          return $collectionInfo['options']['encryptedFields'] ?? null;
 169      }
 170  
 171      return null;
 172  }
 173  
 174  /**
 175   * Return whether the first key in the document starts with a "$" character.
 176   *
 177   * This is used for differentiating update and replacement documents.
 178   *
 179   * @internal
 180   * @param array|object $document Update or replacement document
 181   * @throws InvalidArgumentException
 182   */
 183  function is_first_key_operator($document): bool
 184  {
 185      if ($document instanceof Serializable) {
 186          $document = $document->bsonSerialize();
 187      }
 188  
 189      if (is_object($document)) {
 190          $document = get_object_vars($document);
 191      }
 192  
 193      if (! is_array($document)) {
 194          throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
 195      }
 196  
 197      reset($document);
 198      $firstKey = (string) key($document);
 199  
 200      return isset($firstKey[0]) && $firstKey[0] === '$';
 201  }
 202  
 203  /**
 204   * Returns whether an update specification is a valid aggregation pipeline.
 205   *
 206   * @internal
 207   * @param mixed $pipeline
 208   */
 209  function is_pipeline($pipeline): bool
 210  {
 211      if (! is_array($pipeline)) {
 212          return false;
 213      }
 214  
 215      if ($pipeline === []) {
 216          return false;
 217      }
 218  
 219      $expectedKey = 0;
 220  
 221      foreach ($pipeline as $key => $stage) {
 222          if (! is_array($stage) && ! is_object($stage)) {
 223              return false;
 224          }
 225  
 226          if ($expectedKey !== $key) {
 227              return false;
 228          }
 229  
 230          $expectedKey++;
 231          $stage = (array) $stage;
 232          reset($stage);
 233          $key = key($stage);
 234  
 235          if (! is_string($key) || substr($key, 0, 1) !== '$') {
 236              return false;
 237          }
 238      }
 239  
 240      return true;
 241  }
 242  
 243  /**
 244   * Returns whether we are currently in a transaction.
 245   *
 246   * @internal
 247   * @param array $options Command options
 248   */
 249  function is_in_transaction(array $options): bool
 250  {
 251      if (isset($options['session']) && $options['session'] instanceof Session && $options['session']->isInTransaction()) {
 252          return true;
 253      }
 254  
 255      return false;
 256  }
 257  
 258  /**
 259   * Return whether the aggregation pipeline ends with an $out or $merge operator.
 260   *
 261   * This is used for determining whether the aggregation pipeline must be
 262   * executed against a primary server.
 263   *
 264   * @internal
 265   * @param array $pipeline List of pipeline operations
 266   */
 267  function is_last_pipeline_operator_write(array $pipeline): bool
 268  {
 269      $lastOp = end($pipeline);
 270  
 271      if ($lastOp === false) {
 272          return false;
 273      }
 274  
 275      $lastOp = (array) $lastOp;
 276  
 277      return in_array(key($lastOp), ['$out', '$merge'], true);
 278  }
 279  
 280  /**
 281   * Return whether the "out" option for a mapReduce operation is "inline".
 282   *
 283   * This is used to determine if a mapReduce command requires a primary.
 284   *
 285   * @internal
 286   * @see https://mongodb.com/docs/manual/reference/command/mapReduce/#output-inline
 287   * @param string|array|object $out Output specification
 288   * @throws InvalidArgumentException
 289   */
 290  function is_mapreduce_output_inline($out): bool
 291  {
 292      if (! is_array($out) && ! is_object($out)) {
 293          return false;
 294      }
 295  
 296      if ($out instanceof Serializable) {
 297          $out = $out->bsonSerialize();
 298      }
 299  
 300      if (is_object($out)) {
 301          $out = get_object_vars($out);
 302      }
 303  
 304      if (! is_array($out)) {
 305          throw InvalidArgumentException::invalidType('$out', $out, 'array or object');
 306      }
 307  
 308      reset($out);
 309  
 310      return key($out) === 'inline';
 311  }
 312  
 313  /**
 314   * Return whether the write concern is acknowledged.
 315   *
 316   * This function is similar to mongoc_write_concern_is_acknowledged but does not
 317   * check the fsync option since that was never supported in the PHP driver.
 318   *
 319   * @internal
 320   * @see https://mongodb.com/docs/manual/reference/write-concern/
 321   */
 322  function is_write_concern_acknowledged(WriteConcern $writeConcern): bool
 323  {
 324      /* Note: -1 corresponds to MONGOC_WRITE_CONCERN_W_ERRORS_IGNORED, which is
 325       * deprecated synonym of MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED and slated
 326       * for removal in libmongoc 2.0. */
 327      return ($writeConcern->getW() !== 0 && $writeConcern->getW() !== -1) || $writeConcern->getJournal() === true;
 328  }
 329  
 330  /**
 331   * Return whether the server supports a particular feature.
 332   *
 333   * @internal
 334   * @param Server  $server  Server to check
 335   * @param integer $feature Feature constant (i.e. wire protocol version)
 336   */
 337  function server_supports_feature(Server $server, int $feature): bool
 338  {
 339      $info = $server->getInfo();
 340      $maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0;
 341      $minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0;
 342  
 343      return $minWireVersion <= $feature && $maxWireVersion >= $feature;
 344  }
 345  
 346  /**
 347   * Return whether the input is an array of strings.
 348   *
 349   * @internal
 350   * @param mixed $input
 351   */
 352  function is_string_array($input): bool
 353  {
 354      if (! is_array($input)) {
 355          return false;
 356      }
 357  
 358      foreach ($input as $item) {
 359          if (! is_string($item)) {
 360              return false;
 361          }
 362      }
 363  
 364      return true;
 365  }
 366  
 367  /**
 368   * Performs a deep copy of a value.
 369   *
 370   * This function will clone objects and recursively copy values within arrays.
 371   *
 372   * @internal
 373   * @see https://bugs.php.net/bug.php?id=49664
 374   * @param mixed $element Value to be copied
 375   * @return mixed
 376   * @throws ReflectionException
 377   */
 378  function recursive_copy($element)
 379  {
 380      if (is_array($element)) {
 381          foreach ($element as $key => $value) {
 382              $element[$key] = recursive_copy($value);
 383          }
 384  
 385          return $element;
 386      }
 387  
 388      if (! is_object($element)) {
 389          return $element;
 390      }
 391  
 392      if (! (new ReflectionClass($element))->isCloneable()) {
 393          return $element;
 394      }
 395  
 396      return clone $element;
 397  }
 398  
 399  /**
 400   * Creates a type map to apply to a field type
 401   *
 402   * This is used in the Aggregate, Distinct, and FindAndModify operations to
 403   * apply the root-level type map to the document that will be returned. It also
 404   * replaces the root type with object for consistency within these operations
 405   *
 406   * An existing type map for the given field path will not be overwritten
 407   *
 408   * @internal
 409   * @param array  $typeMap   The existing typeMap
 410   * @param string $fieldPath The field path to apply the root type to
 411   */
 412  function create_field_path_type_map(array $typeMap, string $fieldPath): array
 413  {
 414      // If some field paths already exist, we prefix them with the field path we are assuming as the new root
 415      if (isset($typeMap['fieldPaths']) && is_array($typeMap['fieldPaths'])) {
 416          $fieldPaths = $typeMap['fieldPaths'];
 417  
 418          $typeMap['fieldPaths'] = [];
 419          foreach ($fieldPaths as $existingFieldPath => $type) {
 420              $typeMap['fieldPaths'][$fieldPath . '.' . $existingFieldPath] = $type;
 421          }
 422      }
 423  
 424      // If a root typemap was set, apply this to the field object
 425      if (isset($typeMap['root'])) {
 426          $typeMap['fieldPaths'][$fieldPath] = $typeMap['root'];
 427      }
 428  
 429      /* Special case if we want to convert an array, in which case we need to
 430       * ensure that the field containing the array is exposed as an array,
 431       * instead of the type given in the type map's array key. */
 432      if (substr($fieldPath, -2, 2) === '.$') {
 433          $typeMap['fieldPaths'][substr($fieldPath, 0, -2)] = 'array';
 434      }
 435  
 436      $typeMap['root'] = 'object';
 437  
 438      return $typeMap;
 439  }
 440  
 441  /**
 442   * Execute a callback within a transaction in the given session
 443   *
 444   * This helper takes care of retrying the commit operation or the entire
 445   * transaction if an error occurs.
 446   *
 447   * If the commit fails because of an UnknownTransactionCommitResult error, the
 448   * commit is retried without re-invoking the callback.
 449   * If the commit fails because of a TransientTransactionError, the entire
 450   * transaction will be retried. In this case, the callback will be invoked
 451   * again. It is important that the logic inside the callback is idempotent.
 452   *
 453   * In case of failures, the commit or transaction are retried until 120 seconds
 454   * from the initial call have elapsed. After that, no retries will happen and
 455   * the helper will throw the last exception received from the driver.
 456   *
 457   * @see Client::startSession
 458   * @see Session::startTransaction for supported transaction options
 459   *
 460   * @param Session  $session            A session object as retrieved by Client::startSession
 461   * @param callable $callback           A callback that will be invoked within the transaction
 462   * @param array    $transactionOptions Additional options that are passed to Session::startTransaction
 463   * @throws RuntimeException for driver errors while committing the transaction
 464   * @throws Exception for any other errors, including those thrown in the callback
 465   */
 466  function with_transaction(Session $session, callable $callback, array $transactionOptions = []): void
 467  {
 468      $operation = new WithTransaction($callback, $transactionOptions);
 469      $operation->execute($session);
 470  }
 471  
 472  /**
 473   * Returns the session option if it is set and valid.
 474   *
 475   * @internal
 476   */
 477  function extract_session_from_options(array $options): ?Session
 478  {
 479      if (! isset($options['session']) || ! $options['session'] instanceof Session) {
 480          return null;
 481      }
 482  
 483      return $options['session'];
 484  }
 485  
 486  /**
 487   * Returns the readPreference option if it is set and valid.
 488   *
 489   * @internal
 490   */
 491  function extract_read_preference_from_options(array $options): ?ReadPreference
 492  {
 493      if (! isset($options['readPreference']) || ! $options['readPreference'] instanceof ReadPreference) {
 494          return null;
 495      }
 496  
 497      return $options['readPreference'];
 498  }
 499  
 500  /**
 501   * Performs server selection, respecting the readPreference and session options
 502   * (if given)
 503   *
 504   * @internal
 505   */
 506  function select_server(Manager $manager, array $options): Server
 507  {
 508      $session = extract_session_from_options($options);
 509      $server = $session instanceof Session ? $session->getServer() : null;
 510      if ($server !== null) {
 511          return $server;
 512      }
 513  
 514      $readPreference = extract_read_preference_from_options($options);
 515      if (! $readPreference instanceof ReadPreference) {
 516          // TODO: PHPLIB-476: Read transaction read preference once PHPC-1439 is implemented
 517          $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY);
 518      }
 519  
 520      return $manager->selectServer($readPreference);
 521  }
 522  
 523  /**
 524   * Performs server selection for an aggregate operation with a write stage. The
 525   * $options parameter may be modified by reference if a primary read preference
 526   * must be forced due to the existence of pre-5.0 servers in the topology.
 527   *
 528   * @internal
 529   * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#aggregation-pipelines-with-write-stages
 530   */
 531  function select_server_for_aggregate_write_stage(Manager $manager, array &$options): Server
 532  {
 533      $readPreference = extract_read_preference_from_options($options);
 534  
 535      /* If there is either no read preference or a primary read preference, there
 536       * is no special server selection logic to apply. */
 537      if ($readPreference === null || $readPreference->getMode() === ReadPreference::RP_PRIMARY) {
 538          return select_server($manager, $options);
 539      }
 540  
 541      $server = null;
 542      $serverSelectionError = null;
 543  
 544      try {
 545          $server = select_server($manager, $options);
 546      } catch (DriverRuntimeException $serverSelectionError) {
 547      }
 548  
 549      /* If any pre-5.0 servers exist in the topology, force a primary read
 550       * preference and repeat server selection if it previously failed or
 551       * selected a secondary. */
 552      if (! all_servers_support_write_stage_on_secondary($manager->getServers())) {
 553          $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
 554  
 555          if ($server === null || $server->isSecondary()) {
 556              return select_server($manager, $options);
 557          }
 558      }
 559  
 560      /* If the topology only contains 5.0+ servers, we should either return the
 561       * previously selected server or propagate the server selection error. */
 562      if ($serverSelectionError !== null) {
 563          throw $serverSelectionError;
 564      }
 565  
 566      assert($server instanceof Server);
 567  
 568      return $server;
 569  }