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.

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

   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   * Provides the {@link core_form\filetypes_util} class.
  19   *
  20   * @package     core_form
  21   * @copyright   2017 David Mudrák <david@moodle.com>
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_form;
  26  
  27  use core_collator;
  28  use core_filetypes;
  29  use core_text;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  /**
  34   * Utility class for handling with file types in the forms.
  35   *
  36   * This class is supposed to serve as a helper class for {@link MoodleQuickForm_filetypes}
  37   * and {@link admin_setting_filetypes} classes.
  38   *
  39   * The file types can be specified in a syntax compatible with what filepicker
  40   * and filemanager support via the "accepted_types" option: a list of extensions
  41   * (e.g. ".doc"), mimetypes ("image/png") or groups ("audio").
  42   *
  43   * @copyright 2017 David Mudrak <david@moodle.com>
  44   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class filetypes_util {
  47  
  48      /** @var array Cache of all file type groups for the {@link self::get_groups_info()}. */
  49      protected $cachegroups = null;
  50  
  51      /**
  52       * Converts the argument into an array (list) of file types.
  53       *
  54       * The list can be separated by whitespace, end of lines, commas, colons and semicolons.
  55       * Empty values are not returned. Values are converted to lowercase.
  56       * Duplicates are removed. Glob evaluation is not supported.
  57       *
  58       * The return value can be used as the accepted_types option for the filepicker.
  59       *
  60       * @param string|array $types List of file extensions, groups or mimetypes
  61       * @return array of strings
  62       */
  63      public function normalize_file_types($types) {
  64  
  65          if ($types === '') {
  66              return [];
  67          }
  68  
  69          // Turn string into a list.
  70          if (!is_array($types)) {
  71              $types = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY);
  72          }
  73  
  74          // Fix whitespace and normalize the syntax a bit.
  75          foreach ($types as $i => $type) {
  76              $type = str_replace('*.', '.', $type);
  77              $type = core_text::strtolower($type);
  78              $type = trim($type);
  79  
  80              if ($type === '*') {
  81                  return ['*'];
  82              }
  83  
  84              $types[$i] = $type;
  85          }
  86  
  87          // Do not make the user think that globs (like ".doc?") would work.
  88          foreach ($types as $i => $type) {
  89              if (strpos($type, '*') !== false or strpos($type, '?') !== false) {
  90                  unset($types[$i]);
  91              }
  92          }
  93  
  94          foreach ($types as $i => $type) {
  95              if (substr($type, 0, 1) === '.') {
  96                  // It looks like an extension.
  97                  $type = '.'.ltrim($type, '.');
  98                  $types[$i] = clean_param($type, PARAM_FILE);
  99              } else if ($this->looks_like_mimetype($type)) {
 100                  // All good, it looks like a mimetype.
 101                  continue;
 102              } else if ($this->is_filetype_group($type)) {
 103                  // All good, it is a known type group.
 104                  continue;
 105              } else {
 106                  // We assume the user typed something like "png" so we consider
 107                  // it an extension.
 108                  $types[$i] = '.'.$type;
 109              }
 110          }
 111  
 112          $types = array_filter($types, 'strlen');
 113          $types = array_keys(array_flip($types));
 114  
 115          return $types;
 116      }
 117  
 118      /**
 119       * Does the given file type looks like a valid MIME type?
 120       *
 121       * This does not check of the MIME type is actually registered here/known.
 122       *
 123       * @param string $type
 124       * @return bool
 125       */
 126      public function looks_like_mimetype($type) {
 127          return (bool)preg_match('~^[-\.a-z0-9]+/[a-z0-9]+([-\.\+][a-z0-9]+)*$~', $type);
 128      }
 129  
 130      /**
 131       * Is the given string a known filetype group?
 132       *
 133       * @param string $type
 134       * @return bool|object false or the group info
 135       */
 136      public function is_filetype_group($type) {
 137  
 138          $info = $this->get_groups_info();
 139  
 140          if (isset($info[$type])) {
 141              return $info[$type];
 142  
 143          } else {
 144              return false;
 145          }
 146      }
 147  
 148      /**
 149       * Provides a list of all known file type groups and their properties.
 150       *
 151       * @return array
 152       */
 153      public function get_groups_info() {
 154  
 155          if ($this->cachegroups !== null) {
 156              return $this->cachegroups;
 157          }
 158  
 159          $groups = [];
 160  
 161          foreach (core_filetypes::get_types() as $ext => $info) {
 162              if (isset($info['groups']) && is_array($info['groups'])) {
 163                  foreach ($info['groups'] as $group) {
 164                      if (!isset($groups[$group])) {
 165                          $groups[$group] = (object) [
 166                              'extensions' => [],
 167                              'mimetypes' => [],
 168                          ];
 169                      }
 170                      $groups[$group]->extensions['.'.$ext] = true;
 171                      if (isset($info['type'])) {
 172                          $groups[$group]->mimetypes[$info['type']] = true;
 173                      }
 174                  }
 175              }
 176          }
 177  
 178          foreach ($groups as $group => $info) {
 179              $info->extensions = array_keys($info->extensions);
 180              $info->mimetypes = array_keys($info->mimetypes);
 181          }
 182  
 183          $this->cachegroups = $groups;
 184          return $this->cachegroups;
 185      }
 186  
 187      /**
 188       * Return a human readable name of the filetype group.
 189       *
 190       * @param string $group
 191       * @return string
 192       */
 193      public function get_group_description($group) {
 194  
 195          if (get_string_manager()->string_exists('group:'.$group, 'core_mimetypes')) {
 196              return get_string('group:'.$group, 'core_mimetypes');
 197          } else {
 198              return s($group);
 199          }
 200      }
 201  
 202      /**
 203       * Describe the list of file types for human user.
 204       *
 205       * Given the list of file types, return a list of human readable
 206       * descriptive names of relevant groups, types or file formats.
 207       *
 208       * @param string|array $types
 209       * @return object
 210       */
 211      public function describe_file_types($types) {
 212  
 213          $descriptions = [];
 214          $types = $this->normalize_file_types($types);
 215  
 216          foreach ($types as $type) {
 217              if ($type === '*') {
 218                  $desc = get_string('filetypesany', 'core_form');
 219                  $descriptions[$desc] = [];
 220              } else if ($group = $this->is_filetype_group($type)) {
 221                  $desc = $this->get_group_description($type);
 222                  $descriptions[$desc] = $group->extensions;
 223  
 224              } else if ($this->looks_like_mimetype($type)) {
 225                  $desc = get_mimetype_description($type);
 226                  $descriptions[$desc] = file_get_typegroup('extension', [$type]);
 227  
 228              } else {
 229                  $desc = get_mimetype_description(['filename' => 'fakefile'.$type]);
 230                  if (isset($descriptions[$desc])) {
 231                      $descriptions[$desc][] = $type;
 232                  } else {
 233                      $descriptions[$desc] = [$type];
 234                  }
 235              }
 236          }
 237  
 238          $data = [];
 239  
 240          foreach ($descriptions as $desc => $exts) {
 241              sort($exts);
 242              $data[] = (object)[
 243                  'description' => $desc,
 244                  'extensions' => join(' ', $exts),
 245              ];
 246          }
 247  
 248          core_collator::asort_objects_by_property($data, 'description', core_collator::SORT_NATURAL);
 249  
 250          return (object)[
 251              'hasdescriptions' => !empty($data),
 252              'descriptions' => array_values($data),
 253          ];
 254      }
 255  
 256      /**
 257       * Prepares data for the filetypes-browser.mustache
 258       *
 259       * @param string|array $onlytypes Allow selection from these file types only; for example 'web_image'.
 260       * @param bool $allowall Allow to select 'All file types'. Does not apply with onlytypes are set.
 261       * @param string|array $current Current values that should be selected.
 262       * @return object
 263       */
 264      public function data_for_browser($onlytypes=null, $allowall=true, $current=null) {
 265  
 266          $groups = [];
 267          $current = $this->normalize_file_types($current);
 268  
 269          // Firstly populate the tree of extensions categorized into groups.
 270  
 271          foreach ($this->get_groups_info() as $groupkey => $groupinfo) {
 272              if (empty($groupinfo->extensions)) {
 273                  continue;
 274              }
 275  
 276              $group = (object) [
 277                  'key' => $groupkey,
 278                  'name' => $this->get_group_description($groupkey),
 279                  'selectable' => true,
 280                  'selected' => in_array($groupkey, $current),
 281                  'ext' => implode(' ', $groupinfo->extensions),
 282                  'expanded' => false,
 283              ];
 284  
 285              $types = [];
 286  
 287              foreach ($groupinfo->extensions as $extension) {
 288                  if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
 289                      $group->selectable = false;
 290                      $group->expanded = true;
 291                      $group->ext = '';
 292                      continue;
 293                  }
 294  
 295                  $desc = get_mimetype_description(['filename' => 'fakefile'.$extension]);
 296  
 297                  if ($selected = in_array($extension, $current)) {
 298                      $group->expanded = true;
 299                  }
 300  
 301                  $types[] = (object) [
 302                      'key' => $extension,
 303                      'name' => get_mimetype_description(['filename' => 'fakefile'.$extension]),
 304                      'selected' => $selected,
 305                      'ext' => $extension,
 306                  ];
 307              }
 308  
 309              if (empty($types)) {
 310                  continue;
 311              }
 312  
 313              core_collator::asort_objects_by_property($types, 'name', core_collator::SORT_NATURAL);
 314  
 315              $group->types = array_values($types);
 316              $groups[] = $group;
 317          }
 318  
 319          core_collator::asort_objects_by_property($groups, 'name', core_collator::SORT_NATURAL);
 320  
 321          // Append all other uncategorized extensions.
 322  
 323          $others = [];
 324  
 325          foreach (core_filetypes::get_types() as $extension => $info) {
 326              // Reserved for unknown file types. Not available here.
 327              if ($extension === 'xxx') {
 328                  continue;
 329              }
 330              $extension = '.'.$extension;
 331              if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
 332                  continue;
 333              }
 334              if (!isset($info['groups']) || empty($info['groups'])) {
 335                  $others[] = (object) [
 336                      'key' => $extension,
 337                      'name' => get_mimetype_description(['filename' => 'fakefile'.$extension]),
 338                      'selected' => in_array($extension, $current),
 339                      'ext' => $extension,
 340                  ];
 341              }
 342          }
 343  
 344          core_collator::asort_objects_by_property($others, 'name', core_collator::SORT_NATURAL);
 345  
 346          if (!empty($others)) {
 347              $groups[] = (object) [
 348                  'key' => '',
 349                  'name' => get_string('filetypesothers', 'core_form'),
 350                  'selectable' => false,
 351                  'selected' => false,
 352                  'ext' => '',
 353                  'types' => array_values($others),
 354                  'expanded' => true,
 355              ];
 356          }
 357  
 358          if (empty($onlytypes) and $allowall) {
 359              array_unshift($groups, (object) [
 360                  'key' => '*',
 361                  'name' => get_string('filetypesany', 'core_form'),
 362                  'selectable' => true,
 363                  'selected' => in_array('*', $current),
 364                  'ext' => null,
 365                  'types' => [],
 366                  'expanded' => false,
 367              ]);
 368          }
 369  
 370          $groups = array_values($groups);
 371  
 372          return $groups;
 373      }
 374  
 375      /**
 376       * Expands the file types into the list of file extensions.
 377       *
 378       * The groups and mimetypes are expanded into the list of their associated file
 379       * extensions. Depending on the $keepgroups and $keepmimetypes, the groups
 380       * and mimetypes themselves are either kept in the list or removed.
 381       *
 382       * @param string|array $types
 383       * @param bool $keepgroups Keep the group item in the list after expansion
 384       * @param bool $keepmimetypes Keep the mimetype item in the list after expansion
 385       * @return array list of extensions and eventually groups and types
 386       */
 387      public function expand($types, $keepgroups=false, $keepmimetypes=false) {
 388  
 389          $expanded = [];
 390  
 391          foreach ($this->normalize_file_types($types) as $type) {
 392              if ($group = $this->is_filetype_group($type)) {
 393                  foreach ($group->extensions as $ext) {
 394                      $expanded[$ext] = true;
 395                  }
 396                  if ($keepgroups) {
 397                      $expanded[$type] = true;
 398                  }
 399  
 400              } else if ($this->looks_like_mimetype($type)) {
 401                  // A mime type expands to the associated extensions.
 402                  foreach (file_get_typegroup('extension', [$type]) as $ext) {
 403                      $expanded[$ext] = true;
 404                  }
 405                  if ($keepmimetypes) {
 406                      $expanded[$type] = true;
 407                  }
 408  
 409              } else {
 410                  // Single extension expands to itself.
 411                  $expanded[$type] = true;
 412              }
 413          }
 414  
 415          return array_keys($expanded);
 416      }
 417  
 418      /**
 419       * Should the given file type be considered as a part of the given whitelist.
 420       *
 421       * If multiple types are provided, all of them must be part of the
 422       * whitelist. Empty type is part of any whitelist. Any type is part of an
 423       * empty whitelist.
 424       *
 425       * @param string|array $types File types to be checked
 426       * @param string|array $whitelist An array or string of whitelisted types
 427       * @return boolean
 428       */
 429      public function is_whitelisted($types, $whitelist) {
 430          return empty($this->get_not_whitelisted($types, $whitelist));
 431      }
 432  
 433      /**
 434       * Returns all types that are not part of the give whitelist.
 435       *
 436       * This is similar check to the {@link self::is_whitelisted()} but this one
 437       * actually returns the extra types.
 438       *
 439       * @param string|array $types File types to be checked
 440       * @param string|array $whitelist An array or string of whitelisted types
 441       * @return array Types not present in the whitelist
 442       */
 443      public function get_not_whitelisted($types, $whitelist) {
 444  
 445          $whitelistedtypes = $this->expand($whitelist, true, true);
 446  
 447          if (empty($whitelistedtypes) || $whitelistedtypes == ['*']) {
 448              return [];
 449          }
 450  
 451          $giventypes = $this->normalize_file_types($types);
 452  
 453          if (empty($giventypes)) {
 454              return [];
 455          }
 456  
 457          return array_diff($giventypes, $whitelistedtypes);
 458      }
 459  
 460      /**
 461       * Is the given filename of an allowed file type?
 462       *
 463       * Empty whitelist is interpretted as "any file type is allowed" rather
 464       * than "no file can be uploaded".
 465       *
 466       * @param string $filename the file name
 467       * @param string|array $whitelist list of allowed file extensions
 468       * @return boolean True if the file type is allowed, false if not
 469       */
 470      public function is_allowed_file_type($filename, $whitelist) {
 471  
 472          $allowedextensions = $this->expand($whitelist);
 473  
 474          if (empty($allowedextensions) || $allowedextensions == ['*']) {
 475              return true;
 476          }
 477  
 478          $haystack = strrev(trim(core_text::strtolower($filename)));
 479  
 480          foreach ($allowedextensions as $extension) {
 481              if (strpos($haystack, strrev($extension)) === 0) {
 482                  // The file name ends with the extension.
 483                  return true;
 484              }
 485          }
 486  
 487          return false;
 488      }
 489  
 490      /**
 491       * Returns file types from the list that are not recognized
 492       *
 493       * @param string|array $types list of user-defined file types
 494       * @return array A list of unknown file types.
 495       */
 496      public function get_unknown_file_types($types) {
 497          $unknown = [];
 498  
 499          foreach ($this->normalize_file_types($types) as $type) {
 500              if ($type === '*') {
 501                  // Any file is considered as a known type.
 502                  continue;
 503              } else if ($type === '.xxx') {
 504                  $unknown[$type] = true;
 505              } else if ($this->is_filetype_group($type)) {
 506                  // The type is a group that exists.
 507                  continue;
 508              } else if ($this->looks_like_mimetype($type)) {
 509                  // If there's no extension associated with that mimetype, we consider it unknown.
 510                  if (empty(file_get_typegroup('extension', [$type]))) {
 511                      $unknown[$type] = true;
 512                  }
 513              } else {
 514                  $coretypes = core_filetypes::get_types();
 515                  $typecleaned = str_replace(".", "", $type);
 516                  if (empty($coretypes[$typecleaned])) {
 517                      // If there's no extension, it doesn't exist.
 518                      $unknown[$type] = true;
 519                  }
 520              }
 521          }
 522  
 523          return array_keys($unknown);
 524      }
 525  }