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.
<?php

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This plugin is used to access files by providing an url
 *
 * @since Moodle 2.0
 * @package    repository_url
 * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
require_once($CFG->dirroot . '/repository/lib.php');
require_once(__DIR__.'/locallib.php');

/**
 * repository_url class
 * A subclass of repository, which is used to download a file from a specific url
 *
 * @since Moodle 2.0
 * @package    repository_url
 * @copyright  2009 Dongsheng Cai {@link http://dongsheng.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class repository_url extends repository {
    /** @var int Maximum time of recursion. */
    const MAX_RECURSION_TIME = 5;
> /** @var int Maximum number of CSS imports. */ var $processedfiles = array(); > protected const MAX_CSS_IMPORTS = 10; /** @var int Recursion counter. */ > /** @var int CSS import counter. */ var $recursioncounter = 0; > protected int $cssimportcounter = 0;
> /** @var string file URL. */ /** > public $file_url;
* @param int $repositoryid * @param object $context * @param array $options */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()){ global $CFG; parent::__construct($repositoryid, $context, $options); $this->file_url = optional_param('file', '', PARAM_RAW); $this->file_url = $this->escape_url($this->file_url); } public function check_login() { if (!empty($this->file_url)) { return true; } else { return false; } } /** * @return mixed */ public function print_login() { $strdownload = get_string('download', 'repository'); $strname = get_string('rename', 'repository_url'); $strurl = get_string('url', 'repository_url'); if ($this->options['ajax']) { $url = new stdClass(); $url->label = $strurl.': '; $url->id = 'fileurl'; $url->type = 'text'; $url->name = 'file'; $ret['login'] = array($url); $ret['login_btn_label'] = get_string('download', 'repository_url'); $ret['allowcaching'] = true; // indicates that login form can be cached in filepicker.js return $ret; } else { echo <<<EOD <table> <tr> <td>{$strurl}: </td><td><input name="file" type="text" /></td> </tr> </table> <input type="submit" value="{$strdownload}" /> EOD; } } /** * @param mixed $path * @param string $search * @return array */ public function get_listing($path='', $page='') { $ret = array(); $ret['list'] = array(); $ret['nosearch'] = true; $ret['norefresh'] = true; $ret['nologin'] = true; $this->file_url = clean_param($this->file_url, PARAM_URL); if (empty($this->file_url)) { throw new repository_exception('validfiletype', 'repository_url'); } $this->parse_file(null, $this->file_url, $ret, true); return $ret; } /** * Parses one file (either html or css) * * @param string $baseurl (optional) URL of the file where link to this file was found * @param string $relativeurl relative or absolute link to the file * @param array $list * @param bool $mainfile true only for main HTML false and false for all embedded/linked files */ protected function parse_file($baseurl, $relativeurl, &$list, $mainfile = false) { if (preg_match('/([\'"])(.*)\1/', $relativeurl, $matches)) { $relativeurl = $matches[2]; } if (empty($baseurl)) { $url = $relativeurl; } else {
< $url = htmlspecialchars_decode(url_to_absolute($baseurl, $relativeurl));
> $url = htmlspecialchars_decode(url_to_absolute($baseurl, $relativeurl), ENT_COMPAT);
} if (in_array($url, $this->processedfiles)) { // Avoid endless recursion for the same URL with same parameters. return; }
< // Remove the query string before check. < $recursioncheckurl = preg_replace('/\?.*/', '', $url);
> // Remove the query string and anchors before check. > $recursioncheckurl = (new moodle_url($url))->out_omit_querystring();
if (in_array($recursioncheckurl, $this->processedfiles)) { $this->recursioncounter++; } if ($this->recursioncounter >= self::MAX_RECURSION_TIME) { // Avoid endless recursion for the same URL with different parameters. return; } $this->processedfiles[] = $url; $curl = new curl; $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 3)); $msg = $curl->head($url); $info = $curl->get_info(); if ($info['http_code'] != 200) { if ($mainfile) { $list['error'] = $msg; } } else { $csstoanalyze = ''; if ($mainfile && (strstr($info['content_type'], 'text/html') || empty($info['content_type']))) { // parse as html $htmlcontent = $curl->get($info['url']); $ddoc = new DOMDocument(); @$ddoc->loadHTML($htmlcontent); // extract <img> $tags = $ddoc->getElementsByTagName('img'); foreach ($tags as $tag) { $url = $tag->getAttribute('src'); $this->add_image_to_list($info['url'], $url, $list); } // analyse embedded css (<style>) $tags = $ddoc->getElementsByTagName('style'); foreach ($tags as $tag) { if ($tag->getAttribute('type') == 'text/css') { $csstoanalyze .= $tag->textContent."\n"; } } // analyse links to css (<link type='text/css' href='...'>) $tags = $ddoc->getElementsByTagName('link'); foreach ($tags as $tag) { if ($tag->getAttribute('type') == 'text/css' && strlen($tag->getAttribute('href'))) { $this->parse_file($info['url'], $tag->getAttribute('href'), $list); } } } else if (strstr($info['content_type'], 'css')) { // parse as css $csscontent = $curl->get($info['url']); $csstoanalyze .= $csscontent."\n"; } else if (strstr($info['content_type'], 'image/')) { // download this file $this->add_image_to_list($info['url'], $info['url'], $list); } else { $list['error'] = get_string('validfiletype', 'repository_url'); } // parse all found css styles if (strlen($csstoanalyze)) { $urls = extract_css_urls($csstoanalyze); if (!empty($urls['property'])) { foreach ($urls['property'] as $url) { $this->add_image_to_list($info['url'], $url, $list); } } if (!empty($urls['import'])) { foreach ($urls['import'] as $cssurl) {
> // Limit the number of CSS imports to avoid infinite imports. $this->parse_file($info['url'], $cssurl, $list); > if ($this->cssimportcounter >= self::MAX_CSS_IMPORTS) { } > return; } > } } > $this->cssimportcounter++;
} } protected function add_image_to_list($baseurl, $url, &$list) { if (empty($list['list'])) { $list['list'] = array(); }
< $src = url_to_absolute($baseurl, htmlspecialchars_decode($url));
> $src = url_to_absolute($baseurl, htmlspecialchars_decode($url, ENT_COMPAT));
foreach ($list['list'] as $image) { if ($image['source'] == $src) { return; } } $list['list'][] = array( 'title'=>$this->guess_filename($url, ''), 'source'=>$src, 'thumbnail'=>$src, 'thumbnail_height'=>84, 'thumbnail_width'=>84 ); } public function guess_filename($url, $type) { $pattern = '#\/([\w_\?\-.]+)$#'; $matches = null; preg_match($pattern, $url, $matches); if (empty($matches[1])) { return $url; } else { return $matches[1]; } } /** * Escapes a url by replacing spaces with %20. * * Note: In general moodle does not automatically escape urls, but for the purposes of making this plugin more user friendly * and make it consistent with some other areas in moodle (such as mod_url), urls will automatically be escaped. * * If moodle_url or PARAM_URL is changed to clean characters that need to be escaped, then this function can be removed * * @param string $url An unescaped url. * @return string The escaped url */ protected function escape_url($url) { $url = str_replace('"', '%22', $url); $url = str_replace('\'', '%27', $url); $url = str_replace(' ', '%20', $url); $url = str_replace('<', '%3C', $url); $url = str_replace('>', '%3E', $url); return $url; } public function supported_returntypes() { return (FILE_INTERNAL | FILE_EXTERNAL); } /** * Return the source information * * @param stdClass $url * @return string|null */ public function get_file_source_info($url) { return $url; } /** * file types supported by url downloader plugin * * @return array */ public function supported_filetypes() { return array('web_image'); } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } }