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 2016-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\GridFS;
  19  
  20  use MongoDB\BSON\UTCDateTime;
  21  
  22  use function assert;
  23  use function explode;
  24  use function in_array;
  25  use function is_integer;
  26  use function is_resource;
  27  use function stream_context_get_options;
  28  use function stream_get_wrappers;
  29  use function stream_wrapper_register;
  30  use function stream_wrapper_unregister;
  31  
  32  use const SEEK_CUR;
  33  use const SEEK_END;
  34  use const SEEK_SET;
  35  use const STREAM_IS_URL;
  36  
  37  /**
  38   * Stream wrapper for reading and writing a GridFS file.
  39   *
  40   * @internal
  41   * @see Bucket::openUploadStream()
  42   * @see Bucket::openDownloadStream()
  43   */
  44  class StreamWrapper
  45  {
  46      /** @var resource|null Stream context (set by PHP) */
  47      public $context;
  48  
  49      /** @var string|null */
  50      private $mode;
  51  
  52      /** @var string|null */
  53      private $protocol;
  54  
  55      /** @var ReadableStream|WritableStream|null */
  56      private $stream;
  57  
  58      public function __destruct()
  59      {
  60          /* This destructor is a workaround for PHP trying to use the stream well
  61           * after all objects have been destructed. This can cause autoloading
  62           * issues and possibly segmentation faults during PHP shutdown. */
  63          $this->stream = null;
  64      }
  65  
  66      /**
  67       * Return the stream's file document.
  68       */
  69      public function getFile(): object
  70      {
  71          assert($this->stream !== null);
  72  
  73          return $this->stream->getFile();
  74      }
  75  
  76      /**
  77       * Register the GridFS stream wrapper.
  78       *
  79       * @param string $protocol Protocol to use for stream_wrapper_register()
  80       */
  81      public static function register(string $protocol = 'gridfs'): void
  82      {
  83          if (in_array($protocol, stream_get_wrappers())) {
  84              stream_wrapper_unregister($protocol);
  85          }
  86  
  87          stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
  88      }
  89  
  90      /**
  91       * Closes the stream.
  92       *
  93       * @see https://php.net/manual/en/streamwrapper.stream-close.php
  94       */
  95      public function stream_close(): void
  96      {
  97          if (! $this->stream) {
  98              return;
  99          }
 100  
 101          $this->stream->close();
 102      }
 103  
 104      /**
 105       * Returns whether the file pointer is at the end of the stream.
 106       *
 107       * @see https://php.net/manual/en/streamwrapper.stream-eof.php
 108       */
 109      public function stream_eof(): bool
 110      {
 111          if (! $this->stream instanceof ReadableStream) {
 112              return false;
 113          }
 114  
 115          return $this->stream->isEOF();
 116      }
 117  
 118      /**
 119       * Opens the stream.
 120       *
 121       * @see https://php.net/manual/en/streamwrapper.stream-open.php
 122       * @param string      $path       Path to the file resource
 123       * @param string      $mode       Mode used to open the file (only "r" and "w" are supported)
 124       * @param integer     $options    Additional flags set by the streams API
 125       * @param string|null $openedPath Not used
 126       */
 127      public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
 128      {
 129          $this->initProtocol($path);
 130          $this->mode = $mode;
 131  
 132          if ($mode === 'r') {
 133              return $this->initReadableStream();
 134          }
 135  
 136          if ($mode === 'w') {
 137              return $this->initWritableStream();
 138          }
 139  
 140          return false;
 141      }
 142  
 143      /**
 144       * Read bytes from the stream.
 145       *
 146       * Note: this method may return a string smaller than the requested length
 147       * if data is not available to be read.
 148       *
 149       * @see https://php.net/manual/en/streamwrapper.stream-read.php
 150       * @param integer $length Number of bytes to read
 151       */
 152      public function stream_read(int $length): string
 153      {
 154          if (! $this->stream instanceof ReadableStream) {
 155              return '';
 156          }
 157  
 158          return $this->stream->readBytes($length);
 159      }
 160  
 161      /**
 162       * Return the current position of the stream.
 163       *
 164       * @see https://php.net/manual/en/streamwrapper.stream-seek.php
 165       * @param integer $offset Stream offset to seek to
 166       * @param integer $whence One of SEEK_SET, SEEK_CUR, or SEEK_END
 167       * @return boolean True if the position was updated and false otherwise
 168       */
 169      public function stream_seek(int $offset, int $whence = SEEK_SET): bool
 170      {
 171          assert($this->stream !== null);
 172  
 173          $size = $this->stream->getSize();
 174  
 175          if ($whence === SEEK_CUR) {
 176              $offset += $this->stream->tell();
 177          }
 178  
 179          if ($whence === SEEK_END) {
 180              $offset += $size;
 181          }
 182  
 183          // WritableStreams are always positioned at the end of the stream
 184          if ($this->stream instanceof WritableStream) {
 185              return $offset === $size;
 186          }
 187  
 188          if ($offset < 0 || $offset > $size) {
 189              return false;
 190          }
 191  
 192          $this->stream->seek($offset);
 193  
 194          return true;
 195      }
 196  
 197      /**
 198       * Return information about the stream.
 199       *
 200       * @see https://php.net/manual/en/streamwrapper.stream-stat.php
 201       */
 202      public function stream_stat(): array
 203      {
 204          assert($this->stream !== null);
 205  
 206          $stat = $this->getStatTemplate();
 207  
 208          $stat[2] = $stat['mode'] = $this->stream instanceof ReadableStream
 209              ? 0100444  // S_IFREG & S_IRUSR & S_IRGRP & S_IROTH
 210              : 0100222; // S_IFREG & S_IWUSR & S_IWGRP & S_IWOTH
 211          $stat[7] = $stat['size'] = $this->stream->getSize();
 212  
 213          $file = $this->stream->getFile();
 214  
 215          if (isset($file->uploadDate) && $file->uploadDate instanceof UTCDateTime) {
 216              $timestamp = $file->uploadDate->toDateTime()->getTimestamp();
 217              $stat[9] = $stat['mtime'] = $timestamp;
 218              $stat[10] = $stat['ctime'] = $timestamp;
 219          }
 220  
 221          if (isset($file->chunkSize) && is_integer($file->chunkSize)) {
 222              $stat[11] = $stat['blksize'] = $file->chunkSize;
 223          }
 224  
 225          return $stat;
 226      }
 227  
 228      /**
 229       * Return the current position of the stream.
 230       *
 231       * @see https://php.net/manual/en/streamwrapper.stream-tell.php
 232       * @return integer The current position of the stream
 233       */
 234      public function stream_tell(): int
 235      {
 236          assert($this->stream !== null);
 237  
 238          return $this->stream->tell();
 239      }
 240  
 241      /**
 242       * Write bytes to the stream.
 243       *
 244       * @see https://php.net/manual/en/streamwrapper.stream-write.php
 245       * @param string $data Data to write
 246       * @return integer The number of bytes written
 247       */
 248      public function stream_write(string $data): int
 249      {
 250          if (! $this->stream instanceof WritableStream) {
 251              return 0;
 252          }
 253  
 254          return $this->stream->writeBytes($data);
 255      }
 256  
 257      /**
 258       * Returns a stat template with default values.
 259       */
 260      private function getStatTemplate(): array
 261      {
 262          return [
 263              // phpcs:disable Squiz.Arrays.ArrayDeclaration.IndexNoNewline
 264              0  => 0,  'dev'     => 0,
 265              1  => 0,  'ino'     => 0,
 266              2  => 0,  'mode'    => 0,
 267              3  => 0,  'nlink'   => 0,
 268              4  => 0,  'uid'     => 0,
 269              5  => 0,  'gid'     => 0,
 270              6  => -1, 'rdev'    => -1,
 271              7  => 0,  'size'    => 0,
 272              8  => 0,  'atime'   => 0,
 273              9  => 0,  'mtime'   => 0,
 274              10 => 0,  'ctime'   => 0,
 275              11 => -1, 'blksize' => -1,
 276              12 => -1, 'blocks'  => -1,
 277              // phpcs:enable
 278          ];
 279      }
 280  
 281      /**
 282       * Initialize the protocol from the given path.
 283       *
 284       * @see StreamWrapper::stream_open()
 285       */
 286      private function initProtocol(string $path): void
 287      {
 288          $parts = explode('://', $path, 2);
 289          $this->protocol = $parts[0] ?: 'gridfs';
 290      }
 291  
 292      /**
 293       * Initialize the internal stream for reading.
 294       *
 295       * @see StreamWrapper::stream_open()
 296       */
 297      private function initReadableStream(): bool
 298      {
 299          assert(is_resource($this->context));
 300          $context = stream_context_get_options($this->context);
 301  
 302          assert($this->protocol !== null);
 303          $this->stream = new ReadableStream(
 304              $context[$this->protocol]['collectionWrapper'],
 305              $context[$this->protocol]['file']
 306          );
 307  
 308          return true;
 309      }
 310  
 311      /**
 312       * Initialize the internal stream for writing.
 313       *
 314       * @see StreamWrapper::stream_open()
 315       */
 316      private function initWritableStream(): bool
 317      {
 318          assert(is_resource($this->context));
 319          $context = stream_context_get_options($this->context);
 320  
 321          assert($this->protocol !== null);
 322          $this->stream = new WritableStream(
 323              $context[$this->protocol]['collectionWrapper'],
 324              $context[$this->protocol]['filename'],
 325              $context[$this->protocol]['options']
 326          );
 327  
 328          return true;
 329      }
 330  }