Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * This plugin is used to access local files
  19   *
  20   * @since Moodle 2.0
  21   * @package    repository_local
  22   * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  require_once($CFG->dirroot . '/repository/lib.php');
  26  
  27  /**
  28   * repository_local class is used to browse moodle files
  29   *
  30   * @since Moodle 2.0
  31   * @package    repository_local
  32   * @copyright  2012 Marina Glancy
  33   * @copyright  2009 Dongsheng Cai {@link http://dongsheng.org}
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class repository_local extends repository {
  37      /**
  38       * Get file listing
  39       *
  40       * @param string $encodedpath
  41       * @param string $page no paging is used in repository_local
  42       * @return mixed
  43       */
  44      public function get_listing($encodedpath = '', $page = '') {
  45          global $CFG, $USER, $OUTPUT;
  46          $ret = array();
  47          $ret['dynload'] = true;
  48          $ret['nosearch'] = false;
  49          $ret['nologin'] = true;
  50          $ret['list'] = array();
  51  
  52          $itemid   = null;
  53          $filename = null;
  54          $filearea = null;
  55          $filepath = null;
  56          $component = null;
  57  
  58          if (!empty($encodedpath)) {
  59              $params = json_decode(base64_decode($encodedpath), true);
  60              if (is_array($params) && isset($params['contextid'])) {
  61                  $component = is_null($params['component']) ? NULL : clean_param($params['component'], PARAM_COMPONENT);
  62                  $filearea  = is_null($params['filearea']) ? NULL : clean_param($params['filearea'], PARAM_AREA);
  63                  $itemid    = is_null($params['itemid']) ? NULL : clean_param($params['itemid'], PARAM_INT);
  64                  $filepath  = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH);
  65                  $filename  = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE);
  66                  $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT));
  67              }
  68          }
  69          if (empty($context) && !empty($this->context)) {
  70              $context = $this->context->get_course_context(false);
  71          }
  72          if (empty($context)) {
  73              $context = context_system::instance();
  74          }
  75  
  76          // prepare list of allowed extensions: $extensions is either string '*'
  77          // or array of lowercase extensions, i.e. array('.gif','.jpg')
  78          $extensions = optional_param_array('accepted_types', '', PARAM_RAW);
  79          if (empty($extensions) || $extensions === '*' || (is_array($extensions) && in_array('*', $extensions))) {
  80              $extensions = '*';
  81          } else {
  82              if (!is_array($extensions)) {
  83                  $extensions = array($extensions);
  84              }
  85              $extensions = array_map('core_text::strtolower', $extensions);
  86          }
  87  
  88          // build file tree
  89          $browser = get_file_browser();
  90          if (!($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename))) {
  91              // if file doesn't exist, build path nodes root of current context
  92              $fileinfo = $browser->get_file_info($context, null, null, null, null, null);
  93          }
  94          $ret['list'] = $this->get_non_empty_children($fileinfo, $extensions);
  95  
  96          // build path navigation
  97          $path = array();
  98          for ($level = $fileinfo; $level; $level = $level->get_parent()) {
  99              array_unshift($path, $level);
 100          }
 101          array_unshift($path, null);
 102          $ret['path'] = array();
 103          for ($i=1; $i<count($path); $i++) {
 104              if ($path[$i] == $fileinfo || !$this->can_skip($path[$i], $extensions, $path[$i-1])) {
 105                  $ret['path'][] = $this->get_node_path($path[$i]);
 106              }
 107          }
 108          return $ret;
 109      }
 110  
 111      /**
 112       * Tells how the file can be picked from this repository
 113       *
 114       * @return int
 115       */
 116      public function supported_returntypes() {
 117          return FILE_INTERNAL | FILE_REFERENCE;
 118      }
 119  
 120      /**
 121       * Does this repository used to browse moodle files?
 122       *
 123       * @return boolean
 124       */
 125      public function has_moodle_files() {
 126          return true;
 127      }
 128  
 129      /**
 130       * Returns all children elements that have one of the specified extensions
 131       *
 132       * This function may skip subfolders and recursively add their children
 133       * {@link repository_local::can_skip()}
 134       *
 135       * @param file_info $fileinfo
 136       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 137       * @return array array of file_info elements
 138       */
 139      private function get_non_empty_children(file_info $fileinfo, $extensions) {
 140          $nonemptychildren = $fileinfo->get_non_empty_children($extensions);
 141          $list = array();
 142          foreach ($nonemptychildren as $child) {
 143              if ($this->can_skip($child, $extensions, $fileinfo)) {
 144                  $list = array_merge($list, $this->get_non_empty_children($child, $extensions));
 145              } else {
 146                  $list[] = $this->get_node($child);
 147              }
 148          }
 149          return $list;
 150      }
 151  
 152      /**
 153       * Whether this folder may be skipped in folder hierarchy
 154       *
 155       * 1. Skip the name of a single filearea in a module
 156       * 2. Skip course categories for non-admins who do not have navshowmycoursecategories setting
 157       *
 158       * @param file_info $fileinfo
 159       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 160       * @param file_info|int $parent specify parent here if we know it to avoid creating extra objects
 161       * @return bool
 162       */
 163      private function can_skip(file_info $fileinfo, $extensions, $parent = -1) {
 164          global $CFG;
 165          if (!$fileinfo->is_directory()) {
 166              // do not skip files
 167              return false;
 168          }
 169          if ($fileinfo instanceof file_info_context_course ||
 170              $fileinfo instanceof file_info_context_user ||
 171              $fileinfo instanceof file_info_area_course_legacy ||
 172              $fileinfo instanceof file_info_context_module ||
 173              $fileinfo instanceof file_info_context_system) {
 174              // These instances can never be filearea inside an activity, they will never be skipped.
 175              return false;
 176          } else if ($fileinfo instanceof file_info_context_coursecat) {
 177              // This is a course category. For non-admins we do not display categories
 178              return empty($CFG->navshowmycoursecategories) &&
 179                              !has_capability('moodle/course:update', context_system::instance());
 180          } else {
 181              $params = $fileinfo->get_params();
 182              if (strlen($params['filearea']) &&
 183                      ($params['filepath'] === '/' || empty($params['filepath'])) &&
 184                      ($params['filename'] === '.' || empty($params['filename'])) &&
 185                      context::instance_by_id($params['contextid'])->contextlevel == CONTEXT_MODULE) {
 186                  if ($parent === -1) {
 187                      $parent = $fileinfo->get_parent();
 188                  }
 189                  // This is a filearea inside an activity, it can be skipped if it has no non-empty siblings
 190                  if ($parent && ($parent instanceof file_info_context_module)) {
 191                      if ($parent->count_non_empty_children($extensions, 2) <= 1) {
 192                          return true;
 193                      }
 194                  }
 195              }
 196          }
 197          return false;
 198      }
 199  
 200      /**
 201       * Converts file_info object to element of repository return list
 202       *
 203       * @param file_info $fileinfo
 204       * @return array
 205       */
 206      private function get_node(file_info $fileinfo) {
 207          global $OUTPUT;
 208          $encodedpath = base64_encode(json_encode($fileinfo->get_params()));
 209          $node = array(
 210              'title' => $fileinfo->get_visible_name(),
 211              'datemodified' => $fileinfo->get_timemodified(),
 212              'datecreated' => $fileinfo->get_timecreated()
 213          );
 214          if ($fileinfo->is_directory()) {
 215              $node['path'] = $encodedpath;
 216              $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false);
 217              $node['children'] = array();
 218          } else {
 219              $node['size'] = $fileinfo->get_filesize();
 220              $node['author'] = $fileinfo->get_author();
 221              $node['license'] = $fileinfo->get_license();
 222              $node['isref'] = $fileinfo->is_external_file();
 223              if ($fileinfo->get_status() == 666) {
 224                  $node['originalmissing'] = true;
 225              }
 226              $node['source'] = $encodedpath;
 227              $node['thumbnail'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false);
 228              $node['icon'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false);
 229              if ($imageinfo = $fileinfo->get_imageinfo()) {
 230                  // what a beautiful picture, isn't it
 231                  $fileurl = new moodle_url($fileinfo->get_url());
 232                  $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $fileinfo->get_timemodified()));
 233                  $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $fileinfo->get_timemodified()));
 234                  $node['image_width'] = $imageinfo['width'];
 235                  $node['image_height'] = $imageinfo['height'];
 236              }
 237          }
 238          return $node;
 239      }
 240  
 241      /**
 242       * Converts file_info object to element of repository return path
 243       *
 244       * @param file_info $fileinfo
 245       * @return array
 246       */
 247      private function get_node_path(file_info $fileinfo) {
 248          $encodedpath = base64_encode(json_encode($fileinfo->get_params()));
 249          return array(
 250              'path' => $encodedpath,
 251              'name' => $fileinfo->get_visible_name()
 252          );
 253      }
 254  
 255      /**
 256       * Search through all the files.
 257       *
 258       * This method will do a raw search through the database, then will try
 259       * to match with files that a user can access. A maximum of 50 files will be
 260       * returned at a time, excluding possible duplicates found along the way.
 261       *
 262       * Queries are done in chunk of 100 files to prevent too many records to be fetched
 263       * at once. When too many files are not included, or a maximum of 10 queries are
 264       * performed we consider that this was the last page.
 265       *
 266       * @param  String  $q    The query string.
 267       * @param  integer $page The page number.
 268       * @return array of results.
 269       */
 270      public function search($q, $page = 1) {
 271          global $DB, $SESSION;
 272  
 273          // Because the repository API is weird, the first page is 0, but it should be 1.
 274          if (!$page) {
 275              $page = 1;
 276          }
 277  
 278          if (!isset($SESSION->repository_local_search)) {
 279              $SESSION->repository_local_search = array();
 280          }
 281  
 282          $fs = get_file_storage();
 283          $fb = get_file_browser();
 284  
 285          $max = 50;
 286          $limit = 100;
 287          if ($page <= 1) {
 288              $SESSION->repository_local_search['query'] = $q;
 289              $SESSION->repository_local_search['from'] = 0;
 290              $from = 0;
 291          } else {
 292              // Yes, the repository does not send the query again...
 293              $q = $SESSION->repository_local_search['query'];
 294              $from = (int) $SESSION->repository_local_search['from'];
 295          }
 296  
 297          $count = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', null, null, true);
 298          $remaining = $count - $from;
 299          $maxloops = 3000;
 300          $loops = 0;
 301  
 302          $results = array();
 303          while (count($results) < $max && $maxloops > 0 && $remaining > 0) {
 304              if (empty($files)) {
 305                  $files = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', $from, $limit);
 306                  $from += $limit;
 307              };
 308  
 309              $remaining--;
 310              $maxloops--;
 311              $loops++;
 312  
 313              $file = array_shift($files);
 314              if (!$file) {
 315                  // This should not happen.
 316                  throw new coding_exception('Unexpected end of files list.');
 317              }
 318  
 319              $key = $file->get_contenthash() . ':' . $file->get_filename();
 320              if (isset($results[$key])) {
 321                  // We found the file with same content and same name, let's skip it.
 322                  continue;
 323              }
 324  
 325              $ctx = context::instance_by_id($file->get_contextid());
 326              $fileinfo = $fb->get_file_info($ctx, $file->get_component(), $file->get_filearea(), $file->get_itemid(),
 327                  $file->get_filepath(), $file->get_filename());
 328              if ($fileinfo) {
 329                  $results[$key] = $this->get_node($fileinfo);
 330              }
 331  
 332          }
 333  
 334          // Save the position for the paging to work.
 335          if ($maxloops > 0 && $remaining > 0) {
 336              $SESSION->repository_local_search['from'] += $loops;
 337              $pages = -1;
 338          } else {
 339              $SESSION->repository_local_search['from'] = 0;
 340              $pages = 0;
 341          }
 342  
 343          $return = array(
 344              'list' => array_values($results),
 345              'dynload' => true,
 346              'pages' => $pages,
 347              'page' => $page
 348          );
 349  
 350          return $return;
 351      }
 352  
 353      /**
 354       * Is this repository accessing private data?
 355       *
 356       * @return bool
 357       */
 358      public function contains_private_data() {
 359          return false;
 360      }
 361  }