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 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Core file system class definition.
  19   *
  20   * @package   core_files
  21   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * File system class used for low level access to real files in filedir.
  29   *
  30   * @package   core_files
  31   * @category  files
  32   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  abstract class file_system {
  36  
  37      /**
  38       * Output the content of the specified stored file.
  39       *
  40       * Note, this is different to get_content() as it uses the built-in php
  41       * readfile function which is more efficient.
  42       *
  43       * @param stored_file $file The file to serve.
  44       * @return void
  45       */
  46      public function readfile(stored_file $file) {
  47          if ($this->is_file_readable_locally_by_storedfile($file, false)) {
  48              $path = $this->get_local_path_from_storedfile($file, false);
  49          } else {
  50              $path = $this->get_remote_path_from_storedfile($file);
  51          }
  52          if (readfile_allow_large($path, $file->get_filesize()) === false) {
  53              throw new file_exception('storedfilecannotreadfile', $file->get_filename());
  54          }
  55      }
  56  
  57      /**
  58       * Get the full path on disk for the specified stored file.
  59       *
  60       * Note: This must return a consistent path for the file's contenthash
  61       * and the path _will_ be in a standard local format.
  62       * Streamable paths will not work.
  63       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  64       *
  65       * The $fetchifnotfound allows you to determine the expected path of the file.
  66       *
  67       * @param stored_file $file The file to serve.
  68       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
  69       * @return string full path to pool file with file content
  70       */
  71      public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) {
  72          return $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound);
  73      }
  74  
  75      /**
  76       * Get a remote filepath for the specified stored file.
  77       *
  78       * This is typically either the same as the local filepath, or it is a streamable resource.
  79       *
  80       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
  81       *
  82       * @param stored_file $file The file to serve.
  83       * @return string full path to pool file with file content
  84       */
  85      public function get_remote_path_from_storedfile(stored_file $file) {
  86          return $this->get_remote_path_from_hash($file->get_contenthash(), false);
  87      }
  88  
  89      /**
  90       * Get the full path for the specified hash, including the path to the filedir.
  91       *
  92       * Note: This must return a consistent path for the file's contenthash
  93       * and the path _will_ be in a standard local format.
  94       * Streamable paths will not work.
  95       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  96       *
  97       * The $fetchifnotfound allows you to determine the expected path of the file.
  98       *
  99       * @param string $contenthash The content hash
 100       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 101       * @return string The full path to the content file
 102       */
 103      abstract protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false);
 104  
 105      /**
 106       * Get the full path for the specified hash, including the path to the filedir.
 107       *
 108       * This is typically either the same as the local filepath, or it is a streamable resource.
 109       *
 110       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
 111       *
 112       * @param string $contenthash The content hash
 113       * @return string The full path to the content file
 114       */
 115      abstract protected function get_remote_path_from_hash($contenthash);
 116  
 117      /**
 118       * Determine whether the file is present on the file system somewhere.
 119       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
 120       *
 121       * The $fetchifnotfound allows you to determine the expected path of the file.
 122       *
 123       * @param stored_file $file The file to ensure is available.
 124       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 125       * @return bool
 126       */
 127      public function is_file_readable_locally_by_storedfile(stored_file $file, $fetchifnotfound = false) {
 128          if (!$file->get_filesize()) {
 129              // Files with empty size are either directories or empty.
 130              // We handle these virtually.
 131              return true;
 132          }
 133  
 134          // Check to see if the file is currently readable.
 135          $path = $this->get_local_path_from_storedfile($file, $fetchifnotfound);
 136          if (is_readable($path)) {
 137              return true;
 138          }
 139  
 140          return false;
 141      }
 142  
 143      /**
 144       * Determine whether the file is present on the local file system somewhere.
 145       *
 146       * @param stored_file $file The file to ensure is available.
 147       * @return bool
 148       */
 149      public function is_file_readable_remotely_by_storedfile(stored_file $file) {
 150          if (!$file->get_filesize()) {
 151              // Files with empty size are either directories or empty.
 152              // We handle these virtually.
 153              return true;
 154          }
 155  
 156          $path = $this->get_remote_path_from_storedfile($file, false);
 157          if (is_readable($path)) {
 158              return true;
 159          }
 160  
 161          return false;
 162      }
 163  
 164      /**
 165       * Determine whether the file is present on the file system somewhere given
 166       * the contenthash.
 167       *
 168       * @param string $contenthash The contenthash of the file to check.
 169       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 170       * @return bool
 171       */
 172      public function is_file_readable_locally_by_hash($contenthash, $fetchifnotfound = false) {
 173          if ($contenthash === file_storage::hash_from_string('')) {
 174              // Files with empty size are either directories or empty.
 175              // We handle these virtually.
 176              return true;
 177          }
 178  
 179          // This is called by file_storage::content_exists(), and in turn by the repository system.
 180          $path = $this->get_local_path_from_hash($contenthash, $fetchifnotfound);
 181  
 182          // Note - it is not possible to perform a content recovery safely from a hash alone.
 183          return is_readable($path);
 184      }
 185  
 186      /**
 187       * Determine whether the file is present locally on the file system somewhere given
 188       * the contenthash.
 189       *
 190       * @param string $contenthash The contenthash of the file to check.
 191       * @return bool
 192       */
 193      public function is_file_readable_remotely_by_hash($contenthash) {
 194          if ($contenthash === file_storage::hash_from_string('')) {
 195              // Files with empty size are either directories or empty.
 196              // We handle these virtually.
 197              return true;
 198          }
 199  
 200          $path = $this->get_remote_path_from_hash($contenthash, false);
 201  
 202          // Note - it is not possible to perform a content recovery safely from a hash alone.
 203          return is_readable($path);
 204      }
 205  
 206      /**
 207       * Copy content of file to given pathname.
 208       *
 209       * @param stored_file $file The file to be copied
 210       * @param string $target real path to the new file
 211       * @return bool success
 212       */
 213      abstract public function copy_content_from_storedfile(stored_file $file, $target);
 214  
 215      /**
 216       * Remove the file with the specified contenthash.
 217       *
 218       * Note, if overriding this function, you _must_ check that the file is
 219       * no longer in use - see {check_file_usage}.
 220       *
 221       * DO NOT call directly - reserved for core!!
 222       *
 223       * @param string $contenthash
 224       */
 225      abstract public function remove_file($contenthash);
 226  
 227      /**
 228       * Check whether a file is removable.
 229       *
 230       * This must be called prior to file removal.
 231       *
 232       * @param string $contenthash
 233       * @return bool
 234       */
 235      protected static function is_file_removable($contenthash) {
 236          global $DB;
 237  
 238          if ($contenthash === file_storage::hash_from_string('')) {
 239              // No need to delete files without content.
 240              return false;
 241          }
 242  
 243          // Note: This section is critical - in theory file could be reused at the same time, if this
 244          // happens we can still recover the file from trash.
 245          // Technically this is the responsibility of the file_storage API, but as this method is public, we go belt-and-braces.
 246          if ($DB->record_exists('files', array('contenthash' => $contenthash))) {
 247              // File content is still used.
 248              return false;
 249          }
 250  
 251          return true;
 252      }
 253  
 254      /**
 255       * Get the content of the specified stored file.
 256       *
 257       * Generally you will probably want to use readfile() to serve content,
 258       * and where possible you should see if you can use
 259       * get_content_file_handle and work with the file stream instead.
 260       *
 261       * @param stored_file $file The file to retrieve
 262       * @return string The full file content
 263       */
 264      public function get_content(stored_file $file) {
 265          if (!$file->get_filesize()) {
 266              // Directories are empty. Empty files are not worth fetching.
 267              return '';
 268          }
 269  
 270          $source = $this->get_remote_path_from_storedfile($file);
 271          return file_get_contents($source);
 272      }
 273  
 274      /**
 275       * List contents of archive.
 276       *
 277       * @param stored_file $file The archive to inspect
 278       * @param file_packer $packer file packer instance
 279       * @return array of file infos
 280       */
 281      public function list_files($file, file_packer $packer) {
 282          $archivefile = $this->get_local_path_from_storedfile($file, true);
 283          return $packer->list_files($archivefile);
 284      }
 285  
 286      /**
 287       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 288       *
 289       * @param stored_file $file The archive to inspect
 290       * @param file_packer $packer File packer instance
 291       * @param string $pathname Target directory
 292       * @param file_progress $progress progress indicator callback or null if not required
 293       * @return array|bool List of processed files; false if error
 294       */
 295      public function extract_to_pathname(stored_file $file, file_packer $packer, $pathname, file_progress $progress = null) {
 296          $archivefile = $this->get_local_path_from_storedfile($file, true);
 297          return $packer->extract_to_pathname($archivefile, $pathname, null, $progress);
 298      }
 299  
 300      /**
 301       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 302       *
 303       * @param stored_file $file The archive to inspect
 304       * @param file_packer $packer file packer instance
 305       * @param int $contextid context ID
 306       * @param string $component component
 307       * @param string $filearea file area
 308       * @param int $itemid item ID
 309       * @param string $pathbase path base
 310       * @param int $userid user ID
 311       * @param file_progress $progress Progress indicator callback or null if not required
 312       * @return array|bool list of processed files; false if error
 313       */
 314      public function extract_to_storage(stored_file $file, file_packer $packer, $contextid,
 315              $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) {
 316  
 317          // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 318          $archivefile = $this->get_local_path_from_storedfile($file, true);
 319          return $packer->extract_to_storage($archivefile, $contextid,
 320                  $component, $filearea, $itemid, $pathbase, $userid, $progress);
 321      }
 322  
 323      /**
 324       * Add file/directory into archive.
 325       *
 326       * @param stored_file $file The file to archive
 327       * @param file_archive $filearch file archive instance
 328       * @param string $archivepath pathname in archive
 329       * @return bool success
 330       */
 331      public function add_storedfile_to_archive(stored_file $file, file_archive $filearch, $archivepath) {
 332          if ($file->is_directory()) {
 333              return $filearch->add_directory($archivepath);
 334          } else {
 335              // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 336              return $filearch->add_file_from_pathname($archivepath, $this->get_local_path_from_storedfile($file, true));
 337          }
 338      }
 339  
 340      /**
 341       * Adds this file path to a curl request (POST only).
 342       *
 343       * @param stored_file $file The file to add to the curl request
 344       * @param curl $curlrequest The curl request object
 345       * @param string $key What key to use in the POST request
 346       * @return void
 347       * This needs the fullpath for the storedfile :/
 348       * Can this be achieved in some other fashion?
 349       */
 350      public function add_to_curl_request(stored_file $file, &$curlrequest, $key) {
 351          // Note: curl_file_create does not work with remote paths.
 352          $path = $this->get_local_path_from_storedfile($file, true);
 353          $curlrequest->_tmp_file_post_params[$key] = curl_file_create($path, null, $file->get_filename());
 354      }
 355  
 356      /**
 357       * Returns information about image.
 358       * Information is determined from the file content
 359       *
 360       * @param stored_file $file The file to inspect
 361       * @return mixed array with width, height and mimetype; false if not an image
 362       */
 363      public function get_imageinfo(stored_file $file) {
 364          if (!$this->is_image_from_storedfile($file)) {
 365              return false;
 366          }
 367  
 368          // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first.
 369          // It is more efficient to use a local file when possible.
 370          return $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true));
 371      }
 372  
 373      /**
 374       * Attempt to determine whether the specified file is likely to be an
 375       * image.
 376       * Since this relies upon the mimetype stored in the files table, there
 377       * may be times when this information is not 100% accurate.
 378       *
 379       * @param stored_file $file The file to check
 380       * @return bool
 381       */
 382      public function is_image_from_storedfile(stored_file $file) {
 383          if (!$file->get_filesize()) {
 384              // An empty file cannot be an image.
 385              return false;
 386          }
 387  
 388          $mimetype = $file->get_mimetype();
 389          if (!preg_match('|^image/|', $mimetype)) {
 390              // The mimetype does not include image.
 391              return false;
 392          }
 393  
 394          // If it looks like an image, and it smells like an image, perhaps it's an image!
 395          return true;
 396      }
 397  
 398      /**
 399       * Returns image information relating to the specified path or URL.
 400       *
 401       * @param string $path The full path of the image file.
 402       * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info.
 403       */
 404      protected function get_imageinfo_from_path($path) {
 405          $imagemimetype = file_storage::mimetype_from_file($path);
 406          $issvgimage = file_is_svg_image_from_mimetype($imagemimetype);
 407  
 408          if (!$issvgimage) {
 409              $imageinfo = getimagesize($path);
 410              if (!is_array($imageinfo)) {
 411                  return false; // Nothing to process, the file was not recognised as image by GD.
 412              }
 413              $image = [
 414                      'width' => $imageinfo[0],
 415                      'height' => $imageinfo[1],
 416                      'mimetype' => image_type_to_mime_type($imageinfo[2]),
 417              ];
 418          } else {
 419              // Since SVG file is actually an XML file, GD cannot handle.
 420              $svgcontent = @simplexml_load_file($path);
 421              if (!$svgcontent) {
 422                  // Cannot parse the file.
 423                  return false;
 424              }
 425              $svgattrs = $svgcontent->attributes();
 426  
 427              if (!empty($svgattrs->viewBox)) {
 428                  // We have viewBox.
 429                  $viewboxval = explode(' ', $svgattrs->viewBox);
 430                  $width = intval($viewboxval[2]);
 431                  $height = intval($viewboxval[3]);
 432              } else {
 433                  // Get the width.
 434                  if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) {
 435                      $width = intval($svgattrs->width);
 436                  } else {
 437                      // Default width.
 438                      $width = 800;
 439                  }
 440                  // Get the height.
 441                  if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) {
 442                      $height = intval($svgattrs->height);
 443                  } else {
 444                      // Default width.
 445                      $height = 600;
 446                  }
 447              }
 448  
 449              $image = [
 450                      'width' => $width,
 451                      'height' => $height,
 452                      'mimetype' => $imagemimetype,
 453              ];
 454          }
 455  
 456          if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) {
 457              // GD can not parse it, sorry.
 458              return false;
 459          }
 460          return $image;
 461      }
 462  
 463      /**
 464       * Serve file content using X-Sendfile header.
 465       * Please make sure that all headers are already sent and the all
 466       * access control checks passed.
 467       *
 468       * This alternate method to xsendfile() allows an alternate file system
 469       * to use the full file metadata and avoid extra lookups.
 470       *
 471       * @param stored_file $file The file to send
 472       * @return bool success
 473       */
 474      public function xsendfile_file(stored_file $file): bool {
 475          return $this->xsendfile($file->get_contenthash());
 476      }
 477  
 478      /**
 479       * Serve file content using X-Sendfile header.
 480       * Please make sure that all headers are already sent and the all
 481       * access control checks passed.
 482       *
 483       * @param string $contenthash The content hash of the file to be served
 484       * @return bool success
 485       */
 486      public function xsendfile($contenthash) {
 487          global $CFG;
 488          require_once($CFG->libdir . "/xsendfilelib.php");
 489  
 490          return xsendfile($this->get_remote_path_from_hash($contenthash));
 491      }
 492  
 493      /**
 494       * Returns true if filesystem is configured to support xsendfile.
 495       *
 496       * @return bool
 497       */
 498      public function supports_xsendfile() {
 499          global $CFG;
 500          return !empty($CFG->xsendfile);
 501      }
 502  
 503      /**
 504       * Validate that the content hash matches the content hash of the file on disk.
 505       *
 506       * @param string $contenthash The current content hash to validate
 507       * @param string $pathname The path to the file on disk
 508       * @return array The content hash (it might change) and file size
 509       */
 510      protected function validate_hash_and_file_size($contenthash, $pathname) {
 511          global $CFG;
 512  
 513          if (!is_readable($pathname)) {
 514              throw new file_exception('storedfilecannotread', '', $pathname);
 515          }
 516  
 517          $filesize = filesize($pathname);
 518          if ($filesize === false) {
 519              throw new file_exception('storedfilecannotread', '', $pathname);
 520          }
 521  
 522          if (is_null($contenthash)) {
 523              $contenthash = file_storage::hash_from_path($pathname);
 524          } else if ($CFG->debugdeveloper) {
 525              $filehash = file_storage::hash_from_path($pathname);
 526              if ($filehash === false) {
 527                  throw new file_exception('storedfilecannotread', '', $pathname);
 528              }
 529              if ($filehash !== $contenthash) {
 530                  // Hopefully this never happens, if yes we need to fix calling code.
 531                  debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
 532                  $contenthash = $filehash;
 533              }
 534          }
 535          if ($contenthash === false) {
 536              throw new file_exception('storedfilecannotread', '', $pathname);
 537          }
 538  
 539          if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 540              // Did the file change or is file_storage::hash_from_path() borked for this file?
 541              clearstatcache();
 542              $contenthash = file_storage::hash_from_path($pathname);
 543              $filesize    = filesize($pathname);
 544  
 545              if ($contenthash === false or $filesize === false) {
 546                  throw new file_exception('storedfilecannotread', '', $pathname);
 547              }
 548              if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 549                  // This is very weird...
 550                  throw new file_exception('storedfilecannotread', '', $pathname);
 551              }
 552          }
 553  
 554          return [$contenthash, $filesize];
 555      }
 556  
 557      /**
 558       * Add the supplied file to the file system.
 559       *
 560       * Note: If overriding this function, it is advisable to store the file
 561       * in the path returned by get_local_path_from_hash as there may be
 562       * subsequent uses of the file in the same request.
 563       *
 564       * @param string $pathname Path to file currently on disk
 565       * @param string $contenthash SHA1 hash of content if known (performance only)
 566       * @return array (contenthash, filesize, newfile)
 567       */
 568      abstract public function add_file_from_path($pathname, $contenthash = null);
 569  
 570      /**
 571       * Add a file with the supplied content to the file system.
 572       *
 573       * Note: If overriding this function, it is advisable to store the file
 574       * in the path returned by get_local_path_from_hash as there may be
 575       * subsequent uses of the file in the same request.
 576       *
 577       * @param string $content file content - binary string
 578       * @return array (contenthash, filesize, newfile)
 579       */
 580      abstract public function add_file_from_string($content);
 581  
 582      /**
 583       * Returns file handle - read only mode, no writing allowed into pool files!
 584       *
 585       * When you want to modify a file, create a new file and delete the old one.
 586       *
 587       * @param stored_file $file The file to retrieve a handle for
 588       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 589       * @return resource file handle
 590       */
 591      public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) {
 592          if ($type === stored_file::FILE_HANDLE_GZOPEN) {
 593              // Local file required for gzopen.
 594              $path = $this->get_local_path_from_storedfile($file, true);
 595          } else {
 596              $path = $this->get_remote_path_from_storedfile($file);
 597          }
 598  
 599          return self::get_file_handle_for_path($path, $type);
 600      }
 601  
 602      /**
 603       * Return a file handle for the specified path.
 604       *
 605       * This abstraction should be used when overriding get_content_file_handle in a new file system.
 606       *
 607       * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept.
 608       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 609       * @return resource
 610       * @throws coding_exception When an unexpected type of file handle is requested
 611       */
 612      protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) {
 613          switch ($type) {
 614              case stored_file::FILE_HANDLE_FOPEN:
 615                  // Binary reading.
 616                  return fopen($path, 'rb');
 617              case stored_file::FILE_HANDLE_GZOPEN:
 618                  // Binary reading of file in gz format.
 619                  return gzopen($path, 'rb');
 620              default:
 621                  throw new coding_exception('Unexpected file handle type');
 622          }
 623      }
 624  
 625      /**
 626       * Retrieve the mime information for the specified stored file.
 627       *
 628       * @param string $contenthash
 629       * @param string $filename
 630       * @return string The MIME type.
 631       */
 632      public function mimetype_from_hash($contenthash, $filename) {
 633          $pathname = $this->get_local_path_from_hash($contenthash);
 634          $mimetype = file_storage::mimetype($pathname, $filename);
 635  
 636          if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) {
 637              // The type is unknown, but the full checks weren't completed because the file isn't locally available.
 638              // Ensure we have a local copy and try again.
 639              $pathname = $this->get_local_path_from_hash($contenthash, true);
 640              $mimetype = file_storage::mimetype_from_file($pathname);
 641          }
 642  
 643          return $mimetype;
 644      }
 645  
 646      /**
 647       * Retrieve the mime information for the specified stored file.
 648       *
 649       * @param stored_file $file The stored file to retrieve mime information for
 650       * @return string The MIME type.
 651       */
 652      public function mimetype_from_storedfile($file) {
 653          if (!$file->get_filesize()) {
 654              // Files with an empty filesize are treated as directories and have no mimetype.
 655              return null;
 656          }
 657          return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename());
 658      }
 659  
 660      /**
 661       * Run any periodic tasks which must be performed.
 662       */
 663      public function cron() {
 664      }
 665  }