Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 === '' || $types === null) { 66 return []; 67 } 68 69 // Turn string into a list. 70 if (!is_array($types)) { 71 $types = preg_split('/[\s,;:"\']+/', $types, -1, 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 array 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_listed($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_listed($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 file type be considered as a part of the given list. 420 * 421 * If multiple types are provided, all of them must be part of the list. Empty type is part of any list. 422 * Any type is part of an empty list. 423 * 424 * @param string|array $types File type or list of types to be checked. 425 * @param string|array $list An array or string listing the types to check against. 426 * @return boolean 427 */ 428 public function is_listed($types, $list) { 429 return empty($this->get_not_listed($types, $list)); 430 } 431 432 /** 433 * @deprecated since Moodle 3.10 MDL-69050 - please use {@see is_listed} instead. 434 */ 435 public function is_whitelisted() { 436 throw new \coding_exception('\core_form\filetypes_util::is_whitelisted() has been removed.'); 437 } 438 439 /** 440 * Returns all types that are not part of the given list. 441 * 442 * This is similar check to the {@see self::is_listed()} but this one actually returns the extra types. 443 * 444 * @param string|array $types File type or list of types to be checked. 445 * @param string|array $list An array or string listing the types to check against. 446 * @return array Types not present in the list. 447 */ 448 public function get_not_listed($types, $list) { 449 450 $listedtypes = $this->expand($list, true, true); 451 452 if (empty($listedtypes) || $listedtypes == ['*']) { 453 return []; 454 } 455 456 $giventypes = $this->normalize_file_types($types); 457 458 if (empty($giventypes)) { 459 return []; 460 } 461 462 return array_diff($giventypes, $listedtypes); 463 } 464 465 /** 466 * @deprecated since Moodle 3.10 MDL-69050 - please use {@see get_not_listed} instead. 467 */ 468 public function get_not_whitelisted() { 469 throw new \coding_exception('\core_form\filetypes_util::get_not_whitelisted() has been removed.'); 470 } 471 472 /** 473 * Is the given filename of an allowed file type? 474 * 475 * Empty allowlist is interpreted as "any file type is allowed" rather 476 * than "no file can be uploaded". 477 * 478 * @param string $filename the file name 479 * @param string|array $allowlist list of allowed file extensions 480 * @return boolean True if the file type is allowed, false if not 481 */ 482 public function is_allowed_file_type($filename, $allowlist) { 483 484 $allowedextensions = $this->expand($allowlist); 485 486 if (empty($allowedextensions) || $allowedextensions == ['*']) { 487 return true; 488 } 489 490 $haystack = strrev(trim(core_text::strtolower($filename))); 491 492 foreach ($allowedextensions as $extension) { 493 if (strpos($haystack, strrev($extension)) === 0) { 494 // The file name ends with the extension. 495 return true; 496 } 497 } 498 499 return false; 500 } 501 502 /** 503 * Returns file types from the list that are not recognized 504 * 505 * @param string|array $types list of user-defined file types 506 * @return array A list of unknown file types. 507 */ 508 public function get_unknown_file_types($types) { 509 $unknown = []; 510 511 foreach ($this->normalize_file_types($types) as $type) { 512 if ($type === '*') { 513 // Any file is considered as a known type. 514 continue; 515 } else if ($type === '.xxx') { 516 $unknown[$type] = true; 517 } else if ($this->is_filetype_group($type)) { 518 // The type is a group that exists. 519 continue; 520 } else if ($this->looks_like_mimetype($type)) { 521 // If there's no extension associated with that mimetype, we consider it unknown. 522 if (empty(file_get_typegroup('extension', [$type]))) { 523 $unknown[$type] = true; 524 } 525 } else { 526 $coretypes = core_filetypes::get_types(); 527 $typecleaned = str_replace(".", "", $type); 528 if (empty($coretypes[$typecleaned])) { 529 // If there's no extension, it doesn't exist. 530 $unknown[$type] = true; 531 } 532 } 533 } 534 535 return array_keys($unknown); 536 } 537 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body