Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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   * Unit tests for /lib/filelib.php.
  19   *
  20   * @package   core
  21   * @category  test
  22   * @copyright 2009 Jerome Mouneyrac
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core;
  27  
  28  use core_filetypes;
  29  use curl;
  30  use repository;
  31  
  32  defined('MOODLE_INTERNAL') || die();
  33  
  34  global $CFG;
  35  require_once($CFG->libdir . '/filelib.php');
  36  require_once($CFG->dirroot . '/repository/lib.php');
  37  
  38  /**
  39   * Unit tests for /lib/filelib.php.
  40   *
  41   * @package   core
  42   * @category  test
  43   * @copyright 2009 Jerome Mouneyrac
  44   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class filelib_test extends \advanced_testcase {
  47      public function test_format_postdata_for_curlcall() {
  48  
  49          // POST params with just simple types.
  50          $postdatatoconvert = array( 'userid' => 1, 'roleid' => 22, 'name' => 'john');
  51          $expectedresult = "userid=1&roleid=22&name=john";
  52          $postdata = format_postdata_for_curlcall($postdatatoconvert);
  53          $this->assertEquals($expectedresult, $postdata);
  54  
  55          // POST params with a string containing & character.
  56          $postdatatoconvert = array( 'name' => 'john&emilie', 'roleid' => 22);
  57          $expectedresult = "name=john%26emilie&roleid=22"; // Urlencode: '%26' => '&'.
  58          $postdata = format_postdata_for_curlcall($postdatatoconvert);
  59          $this->assertEquals($expectedresult, $postdata);
  60  
  61          // POST params with an empty value.
  62          $postdatatoconvert = array( 'name' => null, 'roleid' => 22);
  63          $expectedresult = "name=&roleid=22";
  64          $postdata = format_postdata_for_curlcall($postdatatoconvert);
  65          $this->assertEquals($expectedresult, $postdata);
  66  
  67          // POST params with complex types.
  68          $postdatatoconvert = array( 'users' => array(
  69              array(
  70                  'id' => 2,
  71                  'customfields' => array(
  72                      array
  73                      (
  74                          'type' => 'Color',
  75                          'value' => 'violet'
  76                      )
  77                  )
  78              )
  79          )
  80          );
  81          $expectedresult = "users[0][id]=2&users[0][customfields][0][type]=Color&users[0][customfields][0][value]=violet";
  82          $postdata = format_postdata_for_curlcall($postdatatoconvert);
  83          $this->assertEquals($expectedresult, $postdata);
  84  
  85          // POST params with other complex types.
  86          $postdatatoconvert = array ('members' =>
  87          array(
  88              array('groupid' => 1, 'userid' => 1)
  89          , array('groupid' => 1, 'userid' => 2)
  90          )
  91          );
  92          $expectedresult = "members[0][groupid]=1&members[0][userid]=1&members[1][groupid]=1&members[1][userid]=2";
  93          $postdata = format_postdata_for_curlcall($postdatatoconvert);
  94          $this->assertEquals($expectedresult, $postdata);
  95      }
  96  
  97      public function test_download_file_content() {
  98          global $CFG;
  99  
 100          // Test http success first.
 101          $testhtml = $this->getExternalTestFileUrl('/test.html');
 102  
 103          $contents = download_file_content($testhtml);
 104          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 105  
 106          $tofile = "$CFG->tempdir/test.html";
 107          @unlink($tofile);
 108          $result = download_file_content($testhtml, null, null, false, 300, 20, false, $tofile);
 109          $this->assertTrue($result);
 110          $this->assertFileExists($tofile);
 111          $this->assertSame(file_get_contents($tofile), $contents);
 112          @unlink($tofile);
 113  
 114          $result = download_file_content($testhtml, null, null, false, 300, 20, false, null, true);
 115          $this->assertSame($contents, $result);
 116  
 117          $response = download_file_content($testhtml, null, null, true);
 118          $this->assertInstanceOf('stdClass', $response);
 119          $this->assertSame('200', $response->status);
 120          $this->assertTrue(is_array($response->headers));
 121          $this->assertMatchesRegularExpression('|^HTTP/1\.[01] 200 OK$|', rtrim($response->response_code));
 122          $this->assertSame($contents, $response->results);
 123          $this->assertSame('', $response->error);
 124  
 125          // Test https success.
 126          $testhtml = $this->getExternalTestFileUrl('/test.html', true);
 127  
 128          $contents = download_file_content($testhtml, null, null, false, 300, 20, true);
 129          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 130  
 131          $contents = download_file_content($testhtml);
 132          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 133  
 134          // Now 404.
 135          $testhtml = $this->getExternalTestFileUrl('/test.html_nonexistent');
 136  
 137          $contents = download_file_content($testhtml);
 138          $this->assertFalse($contents);
 139          $this->assertDebuggingCalled();
 140  
 141          $response = download_file_content($testhtml, null, null, true);
 142          $this->assertInstanceOf('stdClass', $response);
 143          $this->assertSame('404', $response->status);
 144          $this->assertTrue(is_array($response->headers));
 145          $this->assertMatchesRegularExpression('|^HTTP/1\.[01] 404 Not Found$|', rtrim($response->response_code));
 146          // Do not test the response starts with DOCTYPE here because some servers may return different headers.
 147          $this->assertSame('', $response->error);
 148  
 149          // Invalid url.
 150          $testhtml = $this->getExternalTestFileUrl('/test.html');
 151          $testhtml = str_replace('http://', 'ftp://', $testhtml);
 152  
 153          $contents = download_file_content($testhtml);
 154          $this->assertFalse($contents);
 155  
 156          // Test standard redirects.
 157          $testurl = $this->getExternalTestFileUrl('/test_redir.php');
 158  
 159          $contents = download_file_content("$testurl?redir=2");
 160          $this->assertSame('done', $contents);
 161  
 162          $contents = download_file_content("$testurl?redir=2&verbose=1");
 163          $this->assertSame('done', $contents);
 164  
 165          $response = download_file_content("$testurl?redir=2", null, null, true);
 166          $this->assertInstanceOf('stdClass', $response);
 167          $this->assertSame('200', $response->status);
 168          $this->assertTrue(is_array($response->headers));
 169          $this->assertMatchesRegularExpression('|^HTTP/1\.[01] 200 OK$|', rtrim($response->response_code));
 170          $this->assertSame('done', $response->results);
 171          $this->assertSame('', $response->error);
 172  
 173          $response = download_file_content("$testurl?redir=2&verbose=1", null, null, true);
 174          $this->assertInstanceOf('stdClass', $response);
 175          $this->assertSame('200', $response->status);
 176          $this->assertTrue(is_array($response->headers));
 177          $this->assertMatchesRegularExpression('|^HTTP/1\.[01] 200 OK$|', rtrim($response->response_code));
 178          $this->assertSame('done', $response->results);
 179          $this->assertSame('', $response->error);
 180  
 181          // Commented out this block if there are performance problems.
 182          /*
 183          $contents = download_file_content("$testurl?redir=6");
 184          $this->assertFalse(false, $contents);
 185          $this->assertDebuggingCalled();
 186          $response = download_file_content("$testurl?redir=6", null, null, true);
 187          $this->assertInstanceOf('stdClass', $response);
 188          $this->assertSame('0', $response->status);
 189          $this->assertTrue(is_array($response->headers));
 190          $this->assertFalse($response->results);
 191          $this->assertNotEmpty($response->error);
 192          */
 193  
 194          // Test relative redirects.
 195          $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 196  
 197          $contents = download_file_content("$testurl");
 198          $this->assertSame('done', $contents);
 199  
 200          $contents = download_file_content("$testurl?unused=xxx");
 201          $this->assertSame('done', $contents);
 202      }
 203  
 204      /**
 205       * Test curl basics.
 206       */
 207      public function test_curl_basics() {
 208          global $CFG;
 209  
 210          // Test HTTP success.
 211          $testhtml = $this->getExternalTestFileUrl('/test.html');
 212  
 213          $curl = new \curl();
 214          $contents = $curl->get($testhtml);
 215          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 216          $this->assertSame(0, $curl->get_errno());
 217  
 218          $curl = new \curl();
 219          $tofile = "$CFG->tempdir/test.html";
 220          @unlink($tofile);
 221          $fp = fopen($tofile, 'w');
 222          $result = $curl->get($testhtml, array(), array('CURLOPT_FILE'=>$fp));
 223          $this->assertTrue($result);
 224          fclose($fp);
 225          $this->assertFileExists($tofile);
 226          $this->assertSame($contents, file_get_contents($tofile));
 227          @unlink($tofile);
 228  
 229          $curl = new \curl();
 230          $tofile = "$CFG->tempdir/test.html";
 231          @unlink($tofile);
 232          $result = $curl->download_one($testhtml, array(), array('filepath'=>$tofile));
 233          $this->assertTrue($result);
 234          $this->assertFileExists($tofile);
 235          $this->assertSame($contents, file_get_contents($tofile));
 236          @unlink($tofile);
 237  
 238          // Test 404 request.
 239          $curl = new \curl();
 240          $contents = $curl->get($this->getExternalTestFileUrl('/i.do.not.exist'));
 241          $response = $curl->getResponse();
 242          $this->assertSame('404 Not Found', reset($response));
 243          $this->assertSame(0, $curl->get_errno());
 244      }
 245  
 246      /**
 247       * Test a curl basic request with security enabled.
 248       */
 249      public function test_curl_basics_with_security_helper() {
 250          $this->resetAfterTest();
 251  
 252          // Test a request with a basic hostname filter applied.
 253          $testhtml = $this->getExternalTestFileUrl('/test.html');
 254          $url = new \moodle_url($testhtml);
 255          $host = $url->get_host();
 256          set_config('curlsecurityblockedhosts', $host); // Blocks $host.
 257  
 258          // Create curl with the default security enabled. We expect this to be blocked.
 259          $curl = new \curl();
 260          $contents = $curl->get($testhtml);
 261          $expected = $curl->get_security()->get_blocked_url_string();
 262          $this->assertSame($expected, $contents);
 263          $this->assertSame(0, $curl->get_errno());
 264  
 265          // Now, create a curl using the 'ignoresecurity' override.
 266          // We expect this request to pass, despite the admin setting having been set earlier.
 267          $curl = new \curl(['ignoresecurity' => true]);
 268          $contents = $curl->get($testhtml);
 269          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 270          $this->assertSame(0, $curl->get_errno());
 271  
 272          // Now, try injecting a mock security helper into curl. This will override the default helper.
 273          $mockhelper = $this->getMockBuilder('\core\files\curl_security_helper')->getMock();
 274  
 275          // Make the mock return a different string.
 276          $mockhelper->expects($this->any())->method('get_blocked_url_string')->will($this->returnValue('You shall not pass'));
 277  
 278          // And make the mock security helper block all URLs. This helper instance doesn't care about config.
 279          $mockhelper->expects($this->any())->method('url_is_blocked')->will($this->returnValue(true));
 280  
 281          $curl = new \curl(['securityhelper' => $mockhelper]);
 282          $contents = $curl->get($testhtml);
 283          $this->assertSame('You shall not pass', $curl->get_security()->get_blocked_url_string());
 284          $this->assertSame($curl->get_security()->get_blocked_url_string(), $contents);
 285      }
 286  
 287      public function test_curl_redirects() {
 288          global $CFG;
 289  
 290          $testurl = $this->getExternalTestFileUrl('/test_redir.php');
 291  
 292          $curl = new \curl();
 293          $contents = $curl->get("$testurl?redir=2", array(), array('CURLOPT_MAXREDIRS'=>2));
 294          $response = $curl->getResponse();
 295          $this->assertSame('200 OK', reset($response));
 296          $this->assertSame(0, $curl->get_errno());
 297          $this->assertSame(2, $curl->info['redirect_count']);
 298          $this->assertSame('done', $contents);
 299  
 300          // All redirects are emulated now. Enabling "emulateredirects" explicitly does not have effect.
 301          $curl = new \curl();
 302          $curl->emulateredirects = true;
 303          $contents = $curl->get("$testurl?redir=2", array(), array('CURLOPT_MAXREDIRS'=>2));
 304          $response = $curl->getResponse();
 305          $this->assertSame('200 OK', reset($response));
 306          $this->assertSame(0, $curl->get_errno());
 307          $this->assertSame(2, $curl->info['redirect_count']);
 308          $this->assertSame('done', $contents);
 309  
 310          // All redirects are emulated now. Attempting to disable "emulateredirects" explicitly causes warning.
 311          $curl = new \curl();
 312          $curl->emulateredirects = false;
 313          $contents = $curl->get("$testurl?redir=2", array(), array('CURLOPT_MAXREDIRS' => 2));
 314          $response = $curl->getResponse();
 315          $this->assertDebuggingCalled('Attempting to disable emulated redirects has no effect any more!');
 316          $this->assertSame('200 OK', reset($response));
 317          $this->assertSame(0, $curl->get_errno());
 318          $this->assertSame(2, $curl->info['redirect_count']);
 319          $this->assertSame('done', $contents);
 320  
 321          // This test was failing for people behind Squid proxies. Squid does not
 322          // fully support HTTP 1.1, so converts things to HTTP 1.0, where the name
 323          // of the status code is different.
 324          reset($response);
 325          if (key($response) === 'HTTP/1.0') {
 326              $responsecode302 = '302 Moved Temporarily';
 327          } else {
 328              $responsecode302 = '302 Found';
 329          }
 330  
 331          $curl = new \curl();
 332          $contents = $curl->get("$testurl?redir=3", array(), array('CURLOPT_FOLLOWLOCATION'=>0));
 333          $response = $curl->getResponse();
 334          $this->assertSame($responsecode302, reset($response));
 335          $this->assertSame(0, $curl->get_errno());
 336          $this->assertSame(302, $curl->info['http_code']);
 337          $this->assertSame('', $contents);
 338  
 339          $curl = new \curl();
 340          $contents = $curl->get("$testurl?redir=2", array(), array('CURLOPT_MAXREDIRS'=>1));
 341          $this->assertSame(CURLE_TOO_MANY_REDIRECTS, $curl->get_errno());
 342          $this->assertNotEmpty($contents);
 343  
 344          $curl = new \curl();
 345          $tofile = "$CFG->tempdir/test.html";
 346          @unlink($tofile);
 347          $fp = fopen($tofile, 'w');
 348          $result = $curl->get("$testurl?redir=1", array(), array('CURLOPT_FILE'=>$fp));
 349          $this->assertTrue($result);
 350          fclose($fp);
 351          $this->assertFileExists($tofile);
 352          $this->assertSame('done', file_get_contents($tofile));
 353          @unlink($tofile);
 354  
 355          $curl = new \curl();
 356          $tofile = "$CFG->tempdir/test.html";
 357          @unlink($tofile);
 358          $fp = fopen($tofile, 'w');
 359          $result = $curl->get("$testurl?redir=1&verbose=1", array(), array('CURLOPT_FILE' => $fp));
 360          $this->assertTrue($result);
 361          fclose($fp);
 362          $this->assertFileExists($tofile);
 363          $this->assertSame('done', file_get_contents($tofile));
 364          @unlink($tofile);
 365  
 366          $curl = new \curl();
 367          $tofile = "$CFG->tempdir/test.html";
 368          @unlink($tofile);
 369          $result = $curl->download_one("$testurl?redir=1", array(), array('filepath'=>$tofile));
 370          $this->assertTrue($result);
 371          $this->assertFileExists($tofile);
 372          $this->assertSame('done', file_get_contents($tofile));
 373          @unlink($tofile);
 374  
 375          $curl = new \curl();
 376          $tofile = "$CFG->tempdir/test.html";
 377          @unlink($tofile);
 378          $result = $curl->download_one("$testurl?redir=1&verbose=1", array(), array('filepath' => $tofile));
 379          $this->assertTrue($result);
 380          $this->assertFileExists($tofile);
 381          $this->assertSame('done', file_get_contents($tofile));
 382          @unlink($tofile);
 383      }
 384  
 385      /**
 386       * Test that redirects to blocked hosts are blocked.
 387       */
 388      public function test_curl_blocked_redirect() {
 389          $this->resetAfterTest();
 390  
 391          $testurl = $this->getExternalTestFileUrl('/test_redir.php');
 392  
 393          // Block a host.
 394          // Note: moodle.com is the URL redirected to when test_redir.php has the param extdest=1 set.
 395          set_config('curlsecurityblockedhosts', 'moodle.com');
 396  
 397          // Redirecting to a non-blocked host should resolve.
 398          $curl = new \curl();
 399          $contents = $curl->get("{$testurl}?redir=2");
 400          $response = $curl->getResponse();
 401          $this->assertSame('200 OK', reset($response));
 402          $this->assertSame(0, $curl->get_errno());
 403  
 404          // Redirecting to the blocked host should fail.
 405          $curl = new \curl();
 406          $blockedstring = $curl->get_security()->get_blocked_url_string();
 407          $contents = $curl->get("{$testurl}?redir=1&extdest=1");
 408          $this->assertSame($blockedstring, $contents);
 409          $this->assertSame(0, $curl->get_errno());
 410  
 411          // Redirecting to the blocked host after multiple successful redirects should also fail.
 412          $curl = new \curl();
 413          $contents = $curl->get("{$testurl}?redir=3&extdest=1");
 414          $this->assertSame($blockedstring, $contents);
 415          $this->assertSame(0, $curl->get_errno());
 416      }
 417  
 418      public function test_curl_relative_redirects() {
 419          // Test relative location redirects.
 420          $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 421  
 422          $curl = new \curl();
 423          $contents = $curl->get($testurl);
 424          $response = $curl->getResponse();
 425          $this->assertSame('200 OK', reset($response));
 426          $this->assertSame(0, $curl->get_errno());
 427          $this->assertSame(1, $curl->info['redirect_count']);
 428          $this->assertSame('done', $contents);
 429  
 430          // Test different redirect types.
 431          $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 432  
 433          $curl = new \curl();
 434          $contents = $curl->get("$testurl?type=301");
 435          $response = $curl->getResponse();
 436          $this->assertSame('200 OK', reset($response));
 437          $this->assertSame(0, $curl->get_errno());
 438          $this->assertSame(1, $curl->info['redirect_count']);
 439          $this->assertSame('done', $contents);
 440  
 441          $curl = new \curl();
 442          $contents = $curl->get("$testurl?type=302");
 443          $response = $curl->getResponse();
 444          $this->assertSame('200 OK', reset($response));
 445          $this->assertSame(0, $curl->get_errno());
 446          $this->assertSame(1, $curl->info['redirect_count']);
 447          $this->assertSame('done', $contents);
 448  
 449          $curl = new \curl();
 450          $contents = $curl->get("$testurl?type=303");
 451          $response = $curl->getResponse();
 452          $this->assertSame('200 OK', reset($response));
 453          $this->assertSame(0, $curl->get_errno());
 454          $this->assertSame(1, $curl->info['redirect_count']);
 455          $this->assertSame('done', $contents);
 456  
 457          $curl = new \curl();
 458          $contents = $curl->get("$testurl?type=307");
 459          $response = $curl->getResponse();
 460          $this->assertSame('200 OK', reset($response));
 461          $this->assertSame(0, $curl->get_errno());
 462          $this->assertSame(1, $curl->info['redirect_count']);
 463          $this->assertSame('done', $contents);
 464  
 465          $curl = new \curl();
 466          $contents = $curl->get("$testurl?type=308");
 467          $response = $curl->getResponse();
 468          $this->assertSame('200 OK', reset($response));
 469          $this->assertSame(0, $curl->get_errno());
 470          $this->assertSame(1, $curl->info['redirect_count']);
 471          $this->assertSame('done', $contents);
 472      }
 473  
 474      public function test_curl_proxybypass() {
 475          global $CFG;
 476          $testurl = $this->getExternalTestFileUrl('/test.html');
 477  
 478          $oldproxy = $CFG->proxyhost;
 479          $oldproxybypass = $CFG->proxybypass;
 480  
 481          // Test without proxy bypass and inaccessible proxy.
 482          $CFG->proxyhost = 'i.do.not.exist';
 483          $CFG->proxybypass = '';
 484          $curl = new \curl();
 485          $contents = $curl->get($testurl);
 486          $this->assertNotEquals(0, $curl->get_errno());
 487          $this->assertNotEquals('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 488  
 489          // Test with proxy bypass.
 490          $testurlhost = parse_url($testurl, PHP_URL_HOST);
 491          $CFG->proxybypass = $testurlhost;
 492          $curl = new \curl();
 493          $contents = $curl->get($testurl);
 494          $this->assertSame(0, $curl->get_errno());
 495          $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 496  
 497          $CFG->proxyhost = $oldproxy;
 498          $CFG->proxybypass = $oldproxybypass;
 499      }
 500  
 501      /**
 502       * Test that duplicate lines in the curl header are removed.
 503       */
 504      public function test_duplicate_curl_header() {
 505          $testurl = $this->getExternalTestFileUrl('/test_post.php');
 506  
 507          $curl = new \curl();
 508          $headerdata = 'Accept: application/json';
 509          $header = [$headerdata, $headerdata];
 510          $this->assertCount(2, $header);
 511          $curl->setHeader($header);
 512          $this->assertCount(1, $curl->header);
 513          $this->assertEquals($headerdata, $curl->header[0]);
 514      }
 515  
 516      public function test_curl_post() {
 517          $testurl = $this->getExternalTestFileUrl('/test_post.php');
 518  
 519          // Test post request.
 520          $curl = new \curl();
 521          $contents = $curl->post($testurl, 'data=moodletest');
 522          $response = $curl->getResponse();
 523          $this->assertSame('200 OK', reset($response));
 524          $this->assertSame(0, $curl->get_errno());
 525          $this->assertSame('OK', $contents);
 526  
 527          // Test 100 requests.
 528          $curl = new \curl();
 529          $curl->setHeader('Expect: 100-continue');
 530          $contents = $curl->post($testurl, 'data=moodletest');
 531          $response = $curl->getResponse();
 532          $this->assertSame('200 OK', reset($response));
 533          $this->assertSame(0, $curl->get_errno());
 534          $this->assertSame('OK', $contents);
 535      }
 536  
 537      public function test_curl_file() {
 538          $this->resetAfterTest();
 539          $testurl = $this->getExternalTestFileUrl('/test_file.php');
 540  
 541          $fs = get_file_storage();
 542          $filerecord = array(
 543              'contextid' => \context_system::instance()->id,
 544              'component' => 'test',
 545              'filearea' => 'curl_post',
 546              'itemid' => 0,
 547              'filepath' => '/',
 548              'filename' => 'test.txt'
 549          );
 550          $teststring = 'moodletest';
 551          $testfile = $fs->create_file_from_string($filerecord, $teststring);
 552  
 553          // Test post with file.
 554          $data = array('testfile' => $testfile);
 555          $curl = new \curl();
 556          $contents = $curl->post($testurl, $data);
 557          $this->assertSame('OK', $contents);
 558      }
 559  
 560      public function test_curl_file_name() {
 561          $this->resetAfterTest();
 562          $testurl = $this->getExternalTestFileUrl('/test_file_name.php');
 563  
 564          $fs = get_file_storage();
 565          $filerecord = array(
 566              'contextid' => \context_system::instance()->id,
 567              'component' => 'test',
 568              'filearea' => 'curl_post',
 569              'itemid' => 0,
 570              'filepath' => '/',
 571              'filename' => 'test.txt'
 572          );
 573          $teststring = 'moodletest';
 574          $testfile = $fs->create_file_from_string($filerecord, $teststring);
 575  
 576          // Test post with file.
 577          $data = array('testfile' => $testfile);
 578          $curl = new \curl();
 579          $contents = $curl->post($testurl, $data);
 580          $this->assertSame('OK', $contents);
 581      }
 582  
 583      public function test_curl_protocols() {
 584  
 585          // HTTP and HTTPS requests were verified in previous requests. Now check
 586          // that we can selectively disable some protocols.
 587          $curl = new \curl();
 588  
 589          // Other protocols than HTTP(S) are disabled by default.
 590          $testurl = 'file:///';
 591          $curl->get($testurl);
 592          $this->assertNotEmpty($curl->error);
 593          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 594  
 595          $testurl = 'ftp://nowhere';
 596          $curl->get($testurl);
 597          $this->assertNotEmpty($curl->error);
 598          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 599  
 600          $testurl = 'telnet://somewhere';
 601          $curl->get($testurl);
 602          $this->assertNotEmpty($curl->error);
 603          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 604  
 605          // Protocols are also disabled during redirections.
 606          $testurl = $this->getExternalTestFileUrl('/test_redir_proto.php');
 607          $curl->get($testurl, array('proto' => 'file'));
 608          $this->assertNotEmpty($curl->error);
 609          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 610  
 611          $testurl = $this->getExternalTestFileUrl('/test_redir_proto.php');
 612          $curl->get($testurl, array('proto' => 'ftp'));
 613          $this->assertNotEmpty($curl->error);
 614          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 615  
 616          $testurl = $this->getExternalTestFileUrl('/test_redir_proto.php');
 617          $curl->get($testurl, array('proto' => 'telnet'));
 618          $this->assertNotEmpty($curl->error);
 619          $this->assertEquals(CURLE_UNSUPPORTED_PROTOCOL, $curl->errno);
 620      }
 621  
 622      /**
 623       * Testing prepare draft area
 624       *
 625       * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
 626       * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 627       */
 628      public function test_prepare_draft_area() {
 629          global $USER, $DB;
 630  
 631          $this->resetAfterTest(true);
 632  
 633          $generator = $this->getDataGenerator();
 634          $user = $generator->create_user();
 635          $usercontext = \context_user::instance($user->id);
 636          $USER = $DB->get_record('user', array('id'=>$user->id));
 637  
 638          $repositorypluginname = 'user';
 639  
 640          $args = array();
 641          $args['type'] = $repositorypluginname;
 642          $repos = repository::get_instances($args);
 643          $userrepository = reset($repos);
 644          $this->assertInstanceOf('repository', $userrepository);
 645  
 646          $fs = get_file_storage();
 647  
 648          $syscontext = \context_system::instance();
 649          $component = 'core';
 650          $filearea  = 'unittest';
 651          $itemid    = 0;
 652          $filepath  = '/';
 653          $filename  = 'test.txt';
 654          $sourcefield = 'Copyright stuff';
 655  
 656          $filerecord = array(
 657              'contextid' => $syscontext->id,
 658              'component' => $component,
 659              'filearea'  => $filearea,
 660              'itemid'    => $itemid,
 661              'filepath'  => $filepath,
 662              'filename'  => $filename,
 663              'source'    => $sourcefield,
 664          );
 665          $ref = $fs->pack_reference($filerecord);
 666          $originalfile = $fs->create_file_from_string($filerecord, 'Test content');
 667          $fileid = $originalfile->get_id();
 668          $this->assertInstanceOf('stored_file', $originalfile);
 669  
 670          // Create a user private file.
 671          $userfilerecord = new \stdClass;
 672          $userfilerecord->contextid = $usercontext->id;
 673          $userfilerecord->component = 'user';
 674          $userfilerecord->filearea  = 'private';
 675          $userfilerecord->itemid    = 0;
 676          $userfilerecord->filepath  = '/';
 677          $userfilerecord->filename  = 'userfile.txt';
 678          $userfilerecord->source    = 'test';
 679          $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
 680          $userfileref = $fs->pack_reference($userfilerecord);
 681  
 682          $filerefrecord = clone((object)$filerecord);
 683          $filerefrecord->filename = 'testref.txt';
 684  
 685          // Create a file reference.
 686          $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
 687          $this->assertInstanceOf('stored_file', $fileref);
 688          $this->assertEquals($userrepository->id, $fileref->get_repository_id());
 689          $this->assertSame($userfile->get_contenthash(), $fileref->get_contenthash());
 690          $this->assertEquals($userfile->get_filesize(), $fileref->get_filesize());
 691          $this->assertMatchesRegularExpression('#' . $userfile->get_filename(). '$#', $fileref->get_reference_details());
 692  
 693          $draftitemid = 0;
 694          file_prepare_draft_area($draftitemid, $syscontext->id, $component, $filearea, $itemid);
 695  
 696          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid);
 697          $this->assertCount(3, $draftfiles);
 698  
 699          $draftfile = $fs->get_file($usercontext->id, 'user', 'draft', $draftitemid, $filepath, $filename);
 700          $source = unserialize($draftfile->get_source());
 701          $this->assertSame($ref, $source->original);
 702          $this->assertSame($sourcefield, $source->source);
 703  
 704          $draftfileref = $fs->get_file($usercontext->id, 'user', 'draft', $draftitemid, $filepath, $filerefrecord->filename);
 705          $this->assertInstanceOf('stored_file', $draftfileref);
 706          $this->assertTrue($draftfileref->is_external_file());
 707  
 708          // Change some information.
 709          $author = 'Dongsheng Cai';
 710          $draftfile->set_author($author);
 711          $newsourcefield = 'Get from Flickr';
 712          $license = 'GPLv3';
 713          $draftfile->set_license($license);
 714          // If you want to really just change source field, do this.
 715          $source = unserialize($draftfile->get_source());
 716          $newsourcefield = 'From flickr';
 717          $source->source = $newsourcefield;
 718          $draftfile->set_source(serialize($source));
 719  
 720          // Save changed file.
 721          file_save_draft_area_files($draftitemid, $syscontext->id, $component, $filearea, $itemid);
 722  
 723          $file = $fs->get_file($syscontext->id, $component, $filearea, $itemid, $filepath, $filename);
 724  
 725          // Make sure it's the original file id.
 726          $this->assertEquals($fileid, $file->get_id());
 727          $this->assertInstanceOf('stored_file', $file);
 728          $this->assertSame($author, $file->get_author());
 729          $this->assertSame($license, $file->get_license());
 730          $this->assertEquals($newsourcefield, $file->get_source());
 731      }
 732  
 733      /**
 734       * Testing deleting original files.
 735       *
 736       * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
 737       * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 738       */
 739      public function test_delete_original_file_from_draft() {
 740          global $USER, $DB;
 741  
 742          $this->resetAfterTest(true);
 743  
 744          $generator = $this->getDataGenerator();
 745          $user = $generator->create_user();
 746          $usercontext = \context_user::instance($user->id);
 747          $USER = $DB->get_record('user', array('id'=>$user->id));
 748  
 749          $repositorypluginname = 'user';
 750  
 751          $args = array();
 752          $args['type'] = $repositorypluginname;
 753          $repos = repository::get_instances($args);
 754          $userrepository = reset($repos);
 755          $this->assertInstanceOf('repository', $userrepository);
 756  
 757          $fs = get_file_storage();
 758          $syscontext = \context_system::instance();
 759  
 760          $filecontent = 'User file content';
 761  
 762          // Create a user private file.
 763          $userfilerecord = new \stdClass;
 764          $userfilerecord->contextid = $usercontext->id;
 765          $userfilerecord->component = 'user';
 766          $userfilerecord->filearea  = 'private';
 767          $userfilerecord->itemid    = 0;
 768          $userfilerecord->filepath  = '/';
 769          $userfilerecord->filename  = 'userfile.txt';
 770          $userfilerecord->source    = 'test';
 771          $userfile = $fs->create_file_from_string($userfilerecord, $filecontent);
 772          $userfileref = $fs->pack_reference($userfilerecord);
 773          $contenthash = $userfile->get_contenthash();
 774  
 775          $filerecord = array(
 776              'contextid' => $syscontext->id,
 777              'component' => 'core',
 778              'filearea'  => 'phpunit',
 779              'itemid'    => 0,
 780              'filepath'  => '/',
 781              'filename'  => 'test.txt',
 782          );
 783          // Create a file reference.
 784          $fileref = $fs->create_file_from_reference($filerecord, $userrepository->id, $userfileref);
 785          $this->assertInstanceOf('stored_file', $fileref);
 786          $this->assertEquals($userrepository->id, $fileref->get_repository_id());
 787          $this->assertSame($userfile->get_contenthash(), $fileref->get_contenthash());
 788          $this->assertEquals($userfile->get_filesize(), $fileref->get_filesize());
 789          $this->assertMatchesRegularExpression('#' . $userfile->get_filename(). '$#', $fileref->get_reference_details());
 790  
 791          $draftitemid = 0;
 792          file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'private', 0);
 793          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid);
 794          $this->assertCount(2, $draftfiles);
 795          $draftfile = $fs->get_file($usercontext->id, 'user', 'draft', $draftitemid, $userfilerecord->filepath, $userfilerecord->filename);
 796          $draftfile->delete();
 797          // Save changed file.
 798          file_save_draft_area_files($draftitemid, $usercontext->id, 'user', 'private', 0);
 799  
 800          // The file reference should be a regular moodle file now.
 801          $fileref = $fs->get_file($syscontext->id, 'core', 'phpunit', 0, '/', 'test.txt');
 802          $this->assertFalse($fileref->is_external_file());
 803          $this->assertSame($contenthash, $fileref->get_contenthash());
 804          $this->assertEquals($filecontent, $fileref->get_content());
 805      }
 806  
 807      /**
 808       * Test avoid file merging when working with draft areas.
 809       */
 810      public function test_ignore_file_merging_in_draft_area() {
 811          global $USER, $DB;
 812  
 813          $this->resetAfterTest(true);
 814  
 815          $generator = $this->getDataGenerator();
 816          $user = $generator->create_user();
 817          $usercontext = \context_user::instance($user->id);
 818          $USER = $DB->get_record('user', array('id' => $user->id));
 819  
 820          $repositorypluginname = 'user';
 821  
 822          $args = array();
 823          $args['type'] = $repositorypluginname;
 824          $repos = repository::get_instances($args);
 825          $userrepository = reset($repos);
 826          $this->assertInstanceOf('repository', $userrepository);
 827  
 828          $fs = get_file_storage();
 829          $syscontext = \context_system::instance();
 830  
 831          $filecontent = 'User file content';
 832  
 833          // Create a user private file.
 834          $userfilerecord = new \stdClass;
 835          $userfilerecord->contextid = $usercontext->id;
 836          $userfilerecord->component = 'user';
 837          $userfilerecord->filearea  = 'private';
 838          $userfilerecord->itemid    = 0;
 839          $userfilerecord->filepath  = '/';
 840          $userfilerecord->filename  = 'userfile.txt';
 841          $userfilerecord->source    = 'test';
 842          $userfile = $fs->create_file_from_string($userfilerecord, $filecontent);
 843          $userfileref = $fs->pack_reference($userfilerecord);
 844          $contenthash = $userfile->get_contenthash();
 845  
 846          $filerecord = array(
 847              'contextid' => $syscontext->id,
 848              'component' => 'core',
 849              'filearea'  => 'phpunit',
 850              'itemid'    => 0,
 851              'filepath'  => '/',
 852              'filename'  => 'test.txt',
 853          );
 854          // Create a file reference.
 855          $fileref = $fs->create_file_from_reference($filerecord, $userrepository->id, $userfileref);
 856          $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));    // 2 because includes the '.' file.
 857  
 858          // Save using empty draft item id, all files will be deleted.
 859          file_save_draft_area_files(0, $usercontext->id, 'user', 'private', 0);
 860          $this->assertCount(0, $fs->get_area_files($usercontext->id, 'user', 'private'));
 861  
 862          // Create a file again.
 863          $userfile = $fs->create_file_from_string($userfilerecord, $filecontent);
 864          $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));
 865  
 866          // Save without merge.
 867          file_save_draft_area_files(IGNORE_FILE_MERGE, $usercontext->id, 'user', 'private', 0);
 868          $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));
 869          // Save again, this time including some inline text.
 870          $inlinetext = 'Some text <img src="@@PLUGINFILE@@/file.png">';
 871          $text = file_save_draft_area_files(IGNORE_FILE_MERGE, $usercontext->id, 'user', 'private', 0, null, $inlinetext);
 872          $this->assertCount(2, $fs->get_area_files($usercontext->id, 'user', 'private'));
 873          $this->assertEquals($inlinetext, $text);
 874      }
 875  
 876      /**
 877       * Testing deleting file_save_draft_area_files won't accidentally wipe unintended files.
 878       */
 879      public function test_file_save_draft_area_files_itemid_cannot_be_false() {
 880          global $USER, $DB;
 881          $this->resetAfterTest();
 882  
 883          $generator = $this->getDataGenerator();
 884          $user = $generator->create_user();
 885          $usercontext = \context_user::instance($user->id);
 886          $USER = $DB->get_record('user', ['id' => $user->id]);
 887  
 888          $draftitemid = 0;
 889          file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'private', 0);
 890  
 891          // Call file_save_draft_area_files with itemid false - which could only happen due to a bug.
 892          // This should throw an exception.
 893          $this->expectExceptionMessage('file_save_draft_area_files was called with $itemid false. ' .
 894                  'This suggests a bug, because it would wipe all (' . $usercontext->id . ', user, private) files.');
 895          file_save_draft_area_files($draftitemid, $usercontext->id, 'user', 'private', false);
 896      }
 897  
 898      /**
 899       * Tests the strip_double_headers function in the curl class.
 900       */
 901      public function test_curl_strip_double_headers() {
 902          // Example from issue tracker.
 903          $mdl30648example = <<<EOF
 904  HTTP/1.0 407 Proxy Authentication Required
 905  Server: squid/2.7.STABLE9
 906  Date: Thu, 08 Dec 2011 14:44:33 GMT
 907  Content-Type: text/html
 908  Content-Length: 1275
 909  X-Squid-Error: ERR_CACHE_ACCESS_DENIED 0
 910  Proxy-Authenticate: Basic realm="Squid proxy-caching web server"
 911  X-Cache: MISS from homer.lancs.ac.uk
 912  X-Cache-Lookup: NONE from homer.lancs.ac.uk:3128
 913  Via: 1.0 homer.lancs.ac.uk:3128 (squid/2.7.STABLE9)
 914  Connection: close
 915  
 916  HTTP/1.0 200 OK
 917  Server: Apache
 918  X-Lb-Nocache: true
 919  Cache-Control: private, max-age=15, no-transform
 920  ETag: "4d69af5d8ba873ea9192c489e151bd7b"
 921  Content-Type: text/html
 922  Date: Thu, 08 Dec 2011 14:44:53 GMT
 923  Set-Cookie: BBC-UID=c4de2e109c8df6a51de627cee11b214bd4fb6054a030222488317afb31b343360MoodleBot/1.0; expires=Mon, 07-Dec-15 14:44:53 GMT; path=/; domain=bbc.co.uk
 924  X-Cache-Action: MISS
 925  X-Cache-Age: 0
 926  Vary: Cookie,X-Country,X-Ip-is-uk-combined,X-Ip-is-advertise-combined,X-Ip_is_uk_combined,X-Ip_is_advertise_combined, X-GeoIP
 927  X-Cache: MISS from ww
 928  
 929  <html>...
 930  EOF;
 931          $mdl30648expected = <<<EOF
 932  HTTP/1.0 200 OK
 933  Server: Apache
 934  X-Lb-Nocache: true
 935  Cache-Control: private, max-age=15, no-transform
 936  ETag: "4d69af5d8ba873ea9192c489e151bd7b"
 937  Content-Type: text/html
 938  Date: Thu, 08 Dec 2011 14:44:53 GMT
 939  Set-Cookie: BBC-UID=c4de2e109c8df6a51de627cee11b214bd4fb6054a030222488317afb31b343360MoodleBot/1.0; expires=Mon, 07-Dec-15 14:44:53 GMT; path=/; domain=bbc.co.uk
 940  X-Cache-Action: MISS
 941  X-Cache-Age: 0
 942  Vary: Cookie,X-Country,X-Ip-is-uk-combined,X-Ip-is-advertise-combined,X-Ip_is_uk_combined,X-Ip_is_advertise_combined, X-GeoIP
 943  X-Cache: MISS from ww
 944  
 945  <html>...
 946  EOF;
 947          // For HTTP, replace the \n with \r\n.
 948          $mdl30648example = preg_replace("~(?!<\r)\n~", "\r\n", $mdl30648example);
 949          $mdl30648expected = preg_replace("~(?!<\r)\n~", "\r\n", $mdl30648expected);
 950  
 951          // Test stripping works OK.
 952          $this->assertSame($mdl30648expected, \curl::strip_double_headers($mdl30648example));
 953          // Test it does nothing to the 'plain' data.
 954          $this->assertSame($mdl30648expected, \curl::strip_double_headers($mdl30648expected));
 955  
 956          // Example from OU proxy.
 957          $httpsexample = <<<EOF
 958  HTTP/1.0 200 Connection established
 959  
 960  HTTP/1.1 200 OK
 961  Date: Fri, 22 Feb 2013 17:14:23 GMT
 962  Server: Apache/2
 963  X-Powered-By: PHP/5.3.3-7+squeeze14
 964  Content-Type: text/xml
 965  Connection: close
 966  Content-Encoding: gzip
 967  Transfer-Encoding: chunked
 968  
 969  <?xml version="1.0" encoding="ISO-8859-1" ?>
 970  <rss version="2.0">...
 971  EOF;
 972          $httpsexpected = <<<EOF
 973  HTTP/1.1 200 OK
 974  Date: Fri, 22 Feb 2013 17:14:23 GMT
 975  Server: Apache/2
 976  X-Powered-By: PHP/5.3.3-7+squeeze14
 977  Content-Type: text/xml
 978  Connection: close
 979  Content-Encoding: gzip
 980  Transfer-Encoding: chunked
 981  
 982  <?xml version="1.0" encoding="ISO-8859-1" ?>
 983  <rss version="2.0">...
 984  EOF;
 985          // For HTTP, replace the \n with \r\n.
 986          $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
 987          $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
 988  
 989          // Test stripping works OK.
 990          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexample));
 991          // Test it does nothing to the 'plain' data.
 992          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexpected));
 993  
 994          $httpsexample = <<<EOF
 995  HTTP/1.0 200 Connection established
 996  
 997  HTTP/2 200 OK
 998  Date: Fri, 22 Feb 2013 17:14:23 GMT
 999  Server: Apache/2
1000  X-Powered-By: PHP/5.3.3-7+squeeze14
1001  Content-Type: text/xml
1002  Connection: close
1003  Content-Encoding: gzip
1004  Transfer-Encoding: chunked
1005  
1006  <?xml version="1.0" encoding="ISO-8859-1" ?>
1007  <rss version="2.0">...
1008  EOF;
1009          $httpsexpected = <<<EOF
1010  HTTP/2 200 OK
1011  Date: Fri, 22 Feb 2013 17:14:23 GMT
1012  Server: Apache/2
1013  X-Powered-By: PHP/5.3.3-7+squeeze14
1014  Content-Type: text/xml
1015  Connection: close
1016  Content-Encoding: gzip
1017  Transfer-Encoding: chunked
1018  
1019  <?xml version="1.0" encoding="ISO-8859-1" ?>
1020  <rss version="2.0">...
1021  EOF;
1022          // For HTTP, replace the \n with \r\n.
1023          $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
1024          $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
1025  
1026          // Test stripping works OK.
1027          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexample));
1028          // Test it does nothing to the 'plain' data.
1029          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexpected));
1030  
1031          $httpsexample = <<<EOF
1032  HTTP/1.0 200 Connection established
1033  
1034  HTTP/2.1 200 OK
1035  Date: Fri, 22 Feb 2013 17:14:23 GMT
1036  Server: Apache/2
1037  X-Powered-By: PHP/5.3.3-7+squeeze14
1038  Content-Type: text/xml
1039  Connection: close
1040  Content-Encoding: gzip
1041  Transfer-Encoding: chunked
1042  
1043  <?xml version="1.0" encoding="ISO-8859-1" ?>
1044  <rss version="2.0">...
1045  EOF;
1046          $httpsexpected = <<<EOF
1047  HTTP/2.1 200 OK
1048  Date: Fri, 22 Feb 2013 17:14:23 GMT
1049  Server: Apache/2
1050  X-Powered-By: PHP/5.3.3-7+squeeze14
1051  Content-Type: text/xml
1052  Connection: close
1053  Content-Encoding: gzip
1054  Transfer-Encoding: chunked
1055  
1056  <?xml version="1.0" encoding="ISO-8859-1" ?>
1057  <rss version="2.0">...
1058  EOF;
1059          // For HTTP, replace the \n with \r\n.
1060          $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
1061          $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
1062  
1063          // Test stripping works OK.
1064          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexample));
1065          // Test it does nothing to the 'plain' data.
1066          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexpected));
1067  
1068          $httpsexample = <<<EOF
1069  HTTP/1.1 200 Connection established
1070  
1071  HTTP/3 200 OK
1072  Date: Fri, 22 Feb 2013 17:14:23 GMT
1073  Server: Apache/2
1074  X-Powered-By: PHP/5.3.3-7+squeeze14
1075  Content-Type: text/xml
1076  Connection: close
1077  Content-Encoding: gzip
1078  Transfer-Encoding: chunked
1079  
1080  <?xml version="1.0" encoding="ISO-8859-1" ?>
1081  <rss version="2.0">...
1082  EOF;
1083          $httpsexpected = <<<EOF
1084  HTTP/3 200 OK
1085  Date: Fri, 22 Feb 2013 17:14:23 GMT
1086  Server: Apache/2
1087  X-Powered-By: PHP/5.3.3-7+squeeze14
1088  Content-Type: text/xml
1089  Connection: close
1090  Content-Encoding: gzip
1091  Transfer-Encoding: chunked
1092  
1093  <?xml version="1.0" encoding="ISO-8859-1" ?>
1094  <rss version="2.0">...
1095  EOF;
1096          // For HTTP, replace the \n with \r\n.
1097          $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
1098          $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
1099  
1100          // Test stripping works OK.
1101          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexample));
1102          // Test it does nothing to the 'plain' data.
1103          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexpected));
1104  
1105          $httpsexample = <<<EOF
1106  HTTP/2 200 Connection established
1107  
1108  HTTP/4 200 OK
1109  Date: Fri, 22 Feb 2013 17:14:23 GMT
1110  Server: Apache/2
1111  X-Powered-By: PHP/5.3.3-7+squeeze14
1112  Content-Type: text/xml
1113  Connection: close
1114  Content-Encoding: gzip
1115  Transfer-Encoding: chunked
1116  
1117  <?xml version="1.0" encoding="ISO-8859-1" ?>
1118  <rss version="2.0">...
1119  EOF;
1120          $httpsexpected = <<<EOF
1121  HTTP/4 200 OK
1122  Date: Fri, 22 Feb 2013 17:14:23 GMT
1123  Server: Apache/2
1124  X-Powered-By: PHP/5.3.3-7+squeeze14
1125  Content-Type: text/xml
1126  Connection: close
1127  Content-Encoding: gzip
1128  Transfer-Encoding: chunked
1129  
1130  <?xml version="1.0" encoding="ISO-8859-1" ?>
1131  <rss version="2.0">...
1132  EOF;
1133          // For HTTP, replace the \n with \r\n.
1134          $httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
1135          $httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
1136  
1137          // Test stripping works OK.
1138          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexample));
1139          // Test it does nothing to the 'plain' data.
1140          $this->assertSame($httpsexpected, \curl::strip_double_headers($httpsexpected));
1141      }
1142  
1143      /**
1144       * Tests the get_mimetype_description function.
1145       */
1146      public function test_get_mimetype_description() {
1147          $this->resetAfterTest();
1148  
1149          // Test example type (.doc).
1150          $this->assertEquals(get_string('application/msword', 'mimetypes'),
1151                  get_mimetype_description(array('filename' => 'test.doc')));
1152  
1153          // Test an unknown file type.
1154          $this->assertEquals(get_string('document/unknown', 'mimetypes'),
1155                  get_mimetype_description(array('filename' => 'test.frog')));
1156  
1157          // Test a custom filetype with no lang string specified.
1158          core_filetypes::add_type('frog', 'application/x-frog', 'document');
1159          $this->assertEquals('application/x-frog',
1160                  get_mimetype_description(array('filename' => 'test.frog')));
1161  
1162          // Test custom description.
1163          core_filetypes::update_type('frog', 'frog', 'application/x-frog', 'document',
1164                  array(), '', 'Froggy file');
1165          $this->assertEquals('Froggy file',
1166                  get_mimetype_description(array('filename' => 'test.frog')));
1167  
1168          // Test custom description using multilang filter.
1169          \filter_manager::reset_caches();
1170          filter_set_global_state('multilang', TEXTFILTER_ON);
1171          filter_set_applies_to_strings('multilang', true);
1172          core_filetypes::update_type('frog', 'frog', 'application/x-frog', 'document',
1173                  array(), '', '<span lang="en" class="multilang">Green amphibian</span>' .
1174                  '<span lang="fr" class="multilang">Amphibian vert</span>');
1175          $this->assertEquals('Green amphibian',
1176                  get_mimetype_description(array('filename' => 'test.frog')));
1177      }
1178  
1179      /**
1180       * Tests the get_mimetypes_array function.
1181       */
1182      public function test_get_mimetypes_array() {
1183          $mimeinfo = get_mimetypes_array();
1184  
1185          // Test example MIME type (doc).
1186          $this->assertEquals('application/msword', $mimeinfo['doc']['type']);
1187          $this->assertEquals('document', $mimeinfo['doc']['icon']);
1188          $this->assertEquals(array('document'), $mimeinfo['doc']['groups']);
1189          $this->assertFalse(isset($mimeinfo['doc']['string']));
1190          $this->assertFalse(isset($mimeinfo['doc']['defaulticon']));
1191          $this->assertFalse(isset($mimeinfo['doc']['customdescription']));
1192  
1193          // Check the less common fields using other examples.
1194          $this->assertEquals('image', $mimeinfo['png']['string']);
1195          $this->assertEquals(true, $mimeinfo['txt']['defaulticon']);
1196      }
1197  
1198      /**
1199       * Tests for get_mimetype_for_sending function.
1200       */
1201      public function test_get_mimetype_for_sending() {
1202          // Without argument.
1203          $this->assertEquals('application/octet-stream', get_mimetype_for_sending());
1204  
1205          // Argument is null.
1206          $this->assertEquals('application/octet-stream', get_mimetype_for_sending(null));
1207  
1208          // Filename having no extension.
1209          $this->assertEquals('application/octet-stream', get_mimetype_for_sending('filenamewithoutextension'));
1210  
1211          // Test using the extensions listed from the get_mimetypes_array function.
1212          $mimetypes = get_mimetypes_array();
1213          foreach ($mimetypes as $ext => $info) {
1214              if ($ext === 'xxx') {
1215                  $this->assertEquals('application/octet-stream', get_mimetype_for_sending('SampleFile.' . $ext));
1216              } else {
1217                  $this->assertEquals($info['type'], get_mimetype_for_sending('SampleFile.' . $ext));
1218              }
1219          }
1220      }
1221  
1222      /**
1223       * Test curl agent settings.
1224       */
1225      public function test_curl_useragent() {
1226          $curl = new testable_curl();
1227          $options = $curl->get_options();
1228          $this->assertNotEmpty($options);
1229  
1230          $moodlebot = \core_useragent::get_moodlebot_useragent();
1231  
1232          $curl->call_apply_opt($options);
1233          $this->assertTrue(in_array("User-Agent: $moodlebot", $curl->header));
1234          $this->assertFalse(in_array('User-Agent: Test/1.0', $curl->header));
1235  
1236          $options['CURLOPT_USERAGENT'] = 'Test/1.0';
1237          $curl->call_apply_opt($options);
1238          $this->assertTrue(in_array('User-Agent: Test/1.0', $curl->header));
1239          $this->assertFalse(in_array("User-Agent: $moodlebot", $curl->header));
1240  
1241          $curl->set_option('CURLOPT_USERAGENT', 'AnotherUserAgent/1.0');
1242          $curl->call_apply_opt();
1243          $this->assertTrue(in_array('User-Agent: AnotherUserAgent/1.0', $curl->header));
1244          $this->assertFalse(in_array('User-Agent: Test/1.0', $curl->header));
1245  
1246          $curl->set_option('CURLOPT_USERAGENT', 'AnotherUserAgent/1.1');
1247          $options = $curl->get_options();
1248          $curl->call_apply_opt($options);
1249          $this->assertTrue(in_array('User-Agent: AnotherUserAgent/1.1', $curl->header));
1250          $this->assertFalse(in_array('User-Agent: AnotherUserAgent/1.0', $curl->header));
1251  
1252          $curl->unset_option('CURLOPT_USERAGENT');
1253          $curl->call_apply_opt();
1254          $this->assertTrue(in_array("User-Agent: $moodlebot", $curl->header));
1255  
1256          // Finally, test it via exttests, to ensure the agent is sent properly.
1257          $testurl = $this->getExternalTestFileUrl('/test_agent.php');
1258          $extcurl = new \curl();
1259  
1260          // Matching (assert we don't receive an error, and get back the content "OK").
1261          $contents = $extcurl->get($testurl, array(), array('CURLOPT_USERAGENT' => 'AnotherUserAgent/1.2'));
1262          $this->assertSame(0, $extcurl->get_errno());
1263          $this->assertSame('OK', $contents);
1264  
1265          // Not matching (assert we don't receive an error, and get back empty content - not "OK").
1266          $contents = $extcurl->get($testurl, array(), array('CURLOPT_USERAGENT' => 'NonMatchingUserAgent/1.2'));
1267          $this->assertSame(0, $extcurl->get_errno());
1268          $this->assertSame('', $contents);
1269      }
1270  
1271      /**
1272       * Test file_rewrite_pluginfile_urls.
1273       */
1274      public function test_file_rewrite_pluginfile_urls() {
1275  
1276          $syscontext = \context_system::instance();
1277          $originaltext = 'Fake test with an image <img src="@@PLUGINFILE@@/image.png">';
1278  
1279          // Do the rewrite.
1280          $finaltext = file_rewrite_pluginfile_urls($originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0);
1281          $this->assertStringContainsString("pluginfile.php", $finaltext);
1282  
1283          // Now undo.
1284          $options = array('reverse' => true);
1285          $finaltext = file_rewrite_pluginfile_urls($finaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1286  
1287          // Compare the final text is the same that the original.
1288          $this->assertEquals($originaltext, $finaltext);
1289      }
1290  
1291      /**
1292       * Test file_rewrite_pluginfile_urls with includetoken.
1293       */
1294      public function test_file_rewrite_pluginfile_urls_includetoken() {
1295          global $USER, $CFG;
1296  
1297          $CFG->slasharguments = true;
1298  
1299          $this->resetAfterTest();
1300  
1301          $syscontext = \context_system::instance();
1302          $originaltext = 'Fake test with an image <img src="@@PLUGINFILE@@/image.png">';
1303          $options = ['includetoken' => true];
1304  
1305          // Rewrite the content. This will generate a new token.
1306          $finaltext = file_rewrite_pluginfile_urls(
1307                  $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1308  
1309          $token = get_user_key('core_files', $USER->id);
1310          $expectedurl = new \moodle_url("/tokenpluginfile.php/{$token}/{$syscontext->id}/user/private/0/image.png");
1311          $expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
1312          $this->assertEquals($expectedtext, $finaltext);
1313  
1314          // Do it again - the second time will use an existing token.
1315          $finaltext = file_rewrite_pluginfile_urls(
1316                  $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1317          $this->assertEquals($expectedtext, $finaltext);
1318  
1319          // Now undo.
1320          $options['reverse'] = true;
1321          $finaltext = file_rewrite_pluginfile_urls($finaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1322  
1323          // Compare the final text is the same that the original.
1324          $this->assertEquals($originaltext, $finaltext);
1325  
1326          // Now indicates a user different than $USER.
1327          $user = $this->getDataGenerator()->create_user();
1328          $options = ['includetoken' => $user->id];
1329  
1330          // Rewrite the content. This will generate a new token.
1331          $finaltext = file_rewrite_pluginfile_urls(
1332                  $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1333  
1334          $token = get_user_key('core_files', $user->id);
1335          $expectedurl = new \moodle_url("/tokenpluginfile.php/{$token}/{$syscontext->id}/user/private/0/image.png");
1336          $expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
1337          $this->assertEquals($expectedtext, $finaltext);
1338      }
1339  
1340      /**
1341       * Test file_rewrite_pluginfile_urls with includetoken with slasharguments disabled..
1342       */
1343      public function test_file_rewrite_pluginfile_urls_includetoken_no_slashargs() {
1344          global $USER, $CFG;
1345  
1346          $CFG->slasharguments = false;
1347  
1348          $this->resetAfterTest();
1349  
1350          $syscontext = \context_system::instance();
1351          $originaltext = 'Fake test with an image <img src="@@PLUGINFILE@@/image.png">';
1352          $options = ['includetoken' => true];
1353  
1354          // Rewrite the content. This will generate a new token.
1355          $finaltext = file_rewrite_pluginfile_urls(
1356                  $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1357  
1358          $token = get_user_key('core_files', $USER->id);
1359          $expectedurl = new \moodle_url("/tokenpluginfile.php");
1360          $expectedurl .= "?token={$token}&file=/{$syscontext->id}/user/private/0/image.png";
1361          $expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
1362          $this->assertEquals($expectedtext, $finaltext);
1363  
1364          // Do it again - the second time will use an existing token.
1365          $finaltext = file_rewrite_pluginfile_urls(
1366                  $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1367          $this->assertEquals($expectedtext, $finaltext);
1368  
1369          // Now undo.
1370          $options['reverse'] = true;
1371          $finaltext = file_rewrite_pluginfile_urls($finaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
1372  
1373          // Compare the final text is the same that the original.
1374          $this->assertEquals($originaltext, $finaltext);
1375      }
1376  
1377      /**
1378       * Helpter function to create draft files
1379       *
1380       * @param  array  $filedata data for the file record (to not use defaults)
1381       * @return stored_file the stored file instance
1382       */
1383      public static function create_draft_file($filedata = array()) {
1384          global $USER;
1385  
1386          $fs = get_file_storage();
1387  
1388          $filerecord = array(
1389              'component' => 'user',
1390              'filearea'  => 'draft',
1391              'itemid'    => isset($filedata['itemid']) ? $filedata['itemid'] : file_get_unused_draft_itemid(),
1392              'author'    => isset($filedata['author']) ? $filedata['author'] : fullname($USER),
1393              'filepath'  => isset($filedata['filepath']) ? $filedata['filepath'] : '/',
1394              'filename'  => isset($filedata['filename']) ? $filedata['filename'] : 'file.txt',
1395          );
1396  
1397          if (isset($filedata['contextid'])) {
1398              $filerecord['contextid'] = $filedata['contextid'];
1399          } else {
1400              $usercontext = \context_user::instance($USER->id);
1401              $filerecord['contextid'] = $usercontext->id;
1402          }
1403          $source = isset($filedata['source']) ? $filedata['source'] : serialize((object)array('source' => 'From string'));
1404          $content = isset($filedata['content']) ? $filedata['content'] : 'some content here';
1405  
1406          $file = $fs->create_file_from_string($filerecord, $content);
1407          $file->set_source($source);
1408  
1409          return $file;
1410      }
1411  
1412      /**
1413       * Test file_merge_files_from_draft_area_into_filearea
1414       */
1415      public function test_file_merge_files_from_draft_area_into_filearea() {
1416          global $USER, $CFG;
1417  
1418          $this->resetAfterTest(true);
1419          $this->setAdminUser();
1420          $fs = get_file_storage();
1421          $usercontext = \context_user::instance($USER->id);
1422  
1423          // Create a draft file.
1424          $filename = 'data.txt';
1425          $filerecord = array(
1426              'filename'  => $filename,
1427          );
1428          $file = self::create_draft_file($filerecord);
1429          $draftitemid = $file->get_itemid();
1430  
1431          $maxbytes = $CFG->userquota;
1432          $maxareabytes = $CFG->userquota;
1433          $options = array('subdirs' => 1,
1434                           'maxbytes' => $maxbytes,
1435                           'maxfiles' => -1,
1436                           'areamaxbytes' => $maxareabytes);
1437  
1438          // Add new file.
1439          file_merge_files_from_draft_area_into_filearea($draftitemid, $usercontext->id, 'user', 'private', 0, $options);
1440  
1441          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1442          // Directory and file.
1443          $this->assertCount(2, $files);
1444          $found = false;
1445          foreach ($files as $file) {
1446              if (!$file->is_directory()) {
1447                  $found = true;
1448                  $this->assertEquals($filename, $file->get_filename());
1449                  $this->assertEquals('some content here', $file->get_content());
1450              }
1451          }
1452          $this->assertTrue($found);
1453  
1454          // Add two more files.
1455          $filerecord = array(
1456              'itemid'  => $draftitemid,
1457              'filename'  => 'second.txt',
1458          );
1459          self::create_draft_file($filerecord);
1460          $filerecord = array(
1461              'itemid'  => $draftitemid,
1462              'filename'  => 'third.txt',
1463          );
1464          $file = self::create_draft_file($filerecord);
1465  
1466          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $usercontext->id, 'user', 'private', 0, $options);
1467  
1468          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1469          $this->assertCount(4, $files);
1470  
1471          // Update contents of one file.
1472          $filerecord = array(
1473              'filename'  => 'second.txt',
1474              'content'  => 'new content',
1475          );
1476          $file = self::create_draft_file($filerecord);
1477          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $usercontext->id, 'user', 'private', 0, $options);
1478  
1479          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1480          $this->assertCount(4, $files);
1481          $found = false;
1482          foreach ($files as $file) {
1483              if ($file->get_filename() == 'second.txt') {
1484                  $found = true;
1485                  $this->assertEquals('new content', $file->get_content());
1486              }
1487          }
1488          $this->assertTrue($found);
1489  
1490          // Update author.
1491          // Set different author in the current file.
1492          foreach ($files as $file) {
1493              if ($file->get_filename() == 'second.txt') {
1494                  $file->set_author('Nobody');
1495              }
1496          }
1497          $filerecord = array(
1498              'filename'  => 'second.txt',
1499          );
1500          $file = self::create_draft_file($filerecord);
1501  
1502          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $usercontext->id, 'user', 'private', 0, $options);
1503  
1504          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1505          $this->assertCount(4, $files);
1506          $found = false;
1507          foreach ($files as $file) {
1508              if ($file->get_filename() == 'second.txt') {
1509                  $found = true;
1510                  $this->assertEquals(fullname($USER), $file->get_author());
1511              }
1512          }
1513          $this->assertTrue($found);
1514  
1515      }
1516  
1517      /**
1518       * Test max area bytes for file_merge_files_from_draft_area_into_filearea
1519       */
1520      public function test_file_merge_files_from_draft_area_into_filearea_max_area_bytes() {
1521          global $USER;
1522  
1523          $this->resetAfterTest(true);
1524          $this->setAdminUser();
1525          $fs = get_file_storage();
1526  
1527          $file = self::create_draft_file();
1528          $options = array('subdirs' => 1,
1529                           'maxbytes' => 5,
1530                           'maxfiles' => -1,
1531                           'areamaxbytes' => 10);
1532  
1533          // Add new file.
1534          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $file->get_contextid(), 'user', 'private', 0, $options);
1535          $usercontext = \context_user::instance($USER->id);
1536          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1537          $this->assertCount(0, $files);
1538      }
1539  
1540      /**
1541       * Test max file bytes for file_merge_files_from_draft_area_into_filearea
1542       */
1543      public function test_file_merge_files_from_draft_area_into_filearea_max_file_bytes() {
1544          global $USER;
1545  
1546          $this->resetAfterTest(true);
1547          // The admin has no restriction for max file uploads, so use a normal user.
1548          $user = $this->getDataGenerator()->create_user();
1549          $this->setUser($user);
1550          $fs = get_file_storage();
1551  
1552          $file = self::create_draft_file();
1553          $options = array('subdirs' => 1,
1554                           'maxbytes' => 1,
1555                           'maxfiles' => -1,
1556                           'areamaxbytes' => 100);
1557  
1558          // Add new file.
1559          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $file->get_contextid(), 'user', 'private', 0, $options);
1560          $usercontext = \context_user::instance($USER->id);
1561          // Check we only get the base directory, not a new file.
1562          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1563          $this->assertCount(1, $files);
1564          $file = array_shift($files);
1565          $this->assertTrue($file->is_directory());
1566      }
1567  
1568      /**
1569       * Test max file number for file_merge_files_from_draft_area_into_filearea
1570       */
1571      public function test_file_merge_files_from_draft_area_into_filearea_max_files() {
1572          global $USER;
1573  
1574          $this->resetAfterTest(true);
1575          $this->setAdminUser();
1576          $fs = get_file_storage();
1577  
1578          $file = self::create_draft_file();
1579          $options = array('subdirs' => 1,
1580                           'maxbytes' => 1000,
1581                           'maxfiles' => 0,
1582                           'areamaxbytes' => 1000);
1583  
1584          // Add new file.
1585          file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $file->get_contextid(), 'user', 'private', 0, $options);
1586          $usercontext = \context_user::instance($USER->id);
1587          // Check we only get the base directory, not a new file.
1588          $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
1589          $this->assertCount(1, $files);
1590          $file = array_shift($files);
1591          $this->assertTrue($file->is_directory());
1592      }
1593  
1594      /**
1595       * Test file_get_draft_area_info.
1596       */
1597      public function test_file_get_draft_area_info() {
1598          global $USER;
1599  
1600          $this->resetAfterTest(true);
1601          $this->setAdminUser();
1602          $fs = get_file_storage();
1603  
1604          $filerecord = array(
1605              'filename'  => 'one.txt',
1606          );
1607          $file = self::create_draft_file($filerecord);
1608          $size = $file->get_filesize();
1609          $draftitemid = $file->get_itemid();
1610          // Add another file.
1611          $filerecord = array(
1612              'itemid'  => $draftitemid,
1613              'filename'  => 'second.txt',
1614          );
1615          $file = self::create_draft_file($filerecord);
1616          $size += $file->get_filesize();
1617  
1618          // Create directory.
1619          $usercontext = \context_user::instance($USER->id);
1620          $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
1621          // Add file to directory.
1622          $filerecord = array(
1623              'itemid'  => $draftitemid,
1624              'filename' => 'third.txt',
1625              'filepath' => '/testsubdir/',
1626          );
1627          $file = self::create_draft_file($filerecord);
1628          $size += $file->get_filesize();
1629  
1630          $fileinfo = file_get_draft_area_info($draftitemid);
1631          $this->assertEquals(3, $fileinfo['filecount']);
1632          $this->assertEquals($size, $fileinfo['filesize']);
1633          $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
1634          $this->assertEquals($size, $fileinfo['filesize_without_references']);
1635  
1636          // Now get files from just one folder.
1637          $fileinfo = file_get_draft_area_info($draftitemid, '/testsubdir/');
1638          $this->assertEquals(1, $fileinfo['filecount']);
1639          $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
1640          $this->assertEquals(0, $fileinfo['foldercount']);   // No subdirectories inside the directory.
1641          $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
1642  
1643          // Check we get the same results if we call file_get_file_area_info.
1644          $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid);
1645          $this->assertEquals(3, $fileinfo['filecount']);
1646          $this->assertEquals($size, $fileinfo['filesize']);
1647          $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
1648          $this->assertEquals($size, $fileinfo['filesize_without_references']);
1649      }
1650  
1651      /**
1652       * Test file_get_file_area_info.
1653       */
1654      public function test_file_get_file_area_info() {
1655          global $USER;
1656  
1657          $this->resetAfterTest(true);
1658          $this->setAdminUser();
1659          $fs = get_file_storage();
1660  
1661          $filerecord = array(
1662              'filename'  => 'one.txt',
1663          );
1664          $file = self::create_draft_file($filerecord);
1665          $size = $file->get_filesize();
1666          $draftitemid = $file->get_itemid();
1667          // Add another file.
1668          $filerecord = array(
1669              'itemid'  => $draftitemid,
1670              'filename'  => 'second.txt',
1671          );
1672          $file = self::create_draft_file($filerecord);
1673          $size += $file->get_filesize();
1674  
1675          // Create directory.
1676          $usercontext = \context_user::instance($USER->id);
1677          $dir = $fs->create_directory($usercontext->id, 'user', 'draft', $draftitemid, '/testsubdir/');
1678          // Add file to directory.
1679          $filerecord = array(
1680              'itemid'  => $draftitemid,
1681              'filename' => 'third.txt',
1682              'filepath' => '/testsubdir/',
1683          );
1684          $file = self::create_draft_file($filerecord);
1685          $size += $file->get_filesize();
1686  
1687          // Add files to user private file area.
1688          $options = array('subdirs' => 1, 'maxfiles' => 3);
1689          file_merge_files_from_draft_area_into_filearea($draftitemid, $file->get_contextid(), 'user', 'private', 0, $options);
1690  
1691          $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private');
1692          $this->assertEquals(3, $fileinfo['filecount']);
1693          $this->assertEquals($size, $fileinfo['filesize']);
1694          $this->assertEquals(1, $fileinfo['foldercount']);   // Directory created.
1695          $this->assertEquals($size, $fileinfo['filesize_without_references']);
1696  
1697          // Now get files from just one folder.
1698          $fileinfo = file_get_file_area_info($usercontext->id, 'user', 'private', 0, '/testsubdir/');
1699          $this->assertEquals(1, $fileinfo['filecount']);
1700          $this->assertEquals($file->get_filesize(), $fileinfo['filesize']);
1701          $this->assertEquals(0, $fileinfo['foldercount']);   // No subdirectories inside the directory.
1702          $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']);
1703      }
1704  
1705      /**
1706       * Test confirming that draft files not referenced in the editor text are removed.
1707       */
1708      public function test_file_remove_editor_orphaned_files() {
1709          global $USER, $CFG;
1710          $this->resetAfterTest(true);
1711          $this->setAdminUser();
1712  
1713          // Create three draft files.
1714          $filerecord = ['filename'  => 'file1.png'];
1715          $file = self::create_draft_file($filerecord);
1716          $draftitemid = $file->get_itemid();
1717  
1718          $filerecord['itemid'] = $draftitemid;
1719  
1720          $filerecord['filename'] = 'file2.png';
1721          self::create_draft_file($filerecord);
1722  
1723          $filerecord['filename'] = 'file 3.png';
1724          self::create_draft_file($filerecord);
1725  
1726          $filerecord['filename'] = 'file4.png';
1727          self::create_draft_file($filerecord);
1728  
1729          // Confirm the user drafts area lists 3 files.
1730          $fs = get_file_storage();
1731          $usercontext = \context_user::instance($USER->id);
1732          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'itemid', 0);
1733          $this->assertCount(4, $draftfiles);
1734  
1735          // Now, spoof some editor text content, referencing 2 of the files; one requiring name encoding, one not.
1736          $editor = [
1737              'itemid' => $draftitemid,
1738              'text' => "
1739                  <img src=\"{$CFG->wwwroot}/draftfile.php/{$usercontext->id}/user/draft/{$draftitemid}/file%203.png\" alt=\"\">
1740                  <img src=\"{$CFG->wwwroot}/draftfile.php/{$usercontext->id}/user/draft/{$draftitemid}/file1.png\" alt=\"\">
1741                  <span>{$CFG->wwwroot}/draftfile.php/{$usercontext->id}/user/draft/{$draftitemid}/file4.png</span>"
1742          ];
1743  
1744          // Run the remove orphaned drafts function and confirm that only the referenced files remain in the user drafts.
1745          // The drafts we expect will not be removed (are referenced in the online text).
1746          $expected = ['file1.png', 'file 3.png', 'file4.png'];
1747          file_remove_editor_orphaned_files($editor);
1748          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'itemid', 0);
1749          $this->assertCount(3, $draftfiles);
1750          foreach ($draftfiles as $file) {
1751              $this->assertContains($file->get_filename(), $expected);
1752          }
1753      }
1754  
1755      /**
1756       * Test that all files in the draftarea are returned.
1757       */
1758      public function test_file_get_all_files_in_draftarea() {
1759          $this->resetAfterTest();
1760          $this->setAdminUser();
1761  
1762          $filerecord = ['filename' => 'basepic.jpg'];
1763          $file = self::create_draft_file($filerecord);
1764  
1765          $secondrecord = [
1766              'filename' => 'infolder.jpg',
1767              'filepath' => '/assignment/',
1768              'itemid' => $file->get_itemid()
1769          ];
1770          $file = self::create_draft_file($secondrecord);
1771  
1772          $thirdrecord = [
1773              'filename' => 'deeperfolder.jpg',
1774              'filepath' => '/assignment/pics/',
1775              'itemid' => $file->get_itemid()
1776          ];
1777          $file = self::create_draft_file($thirdrecord);
1778  
1779          $fourthrecord = [
1780              'filename' => 'differentimage.jpg',
1781              'filepath' => '/secondfolder/',
1782              'itemid' => $file->get_itemid()
1783          ];
1784          $file = self::create_draft_file($fourthrecord);
1785  
1786          // This record has the same name as the last record, but it's in a different folder.
1787          // Just checking this is also returned.
1788          $fifthrecord = [
1789              'filename' => 'differentimage.jpg',
1790              'filepath' => '/assignment/pics/',
1791              'itemid' => $file->get_itemid()
1792          ];
1793          $file = self::create_draft_file($fifthrecord);
1794  
1795          $allfiles = file_get_all_files_in_draftarea($file->get_itemid());
1796          $this->assertCount(5, $allfiles);
1797          $this->assertEquals($filerecord['filename'], $allfiles[0]->filename);
1798          $this->assertEquals($secondrecord['filename'], $allfiles[1]->filename);
1799          $this->assertEquals($thirdrecord['filename'], $allfiles[2]->filename);
1800          $this->assertEquals($fourthrecord['filename'], $allfiles[3]->filename);
1801          $this->assertEquals($fifthrecord['filename'], $allfiles[4]->filename);
1802      }
1803  
1804      public function test_file_copy_file_to_file_area() {
1805          // Create two files in different draft areas but owned by the same user.
1806          global $USER;
1807          $this->resetAfterTest(true);
1808          $this->setAdminUser();
1809  
1810          $filerecord = ['filename'  => 'file1.png', 'itemid' => file_get_unused_draft_itemid()];
1811          $file1 = self::create_draft_file($filerecord);
1812          $filerecord = ['filename'  => 'file2.png', 'itemid' => file_get_unused_draft_itemid()];
1813          $file2 = self::create_draft_file($filerecord);
1814  
1815          // Confirm one file in each draft area.
1816          $fs = get_file_storage();
1817          $usercontext = \context_user::instance($USER->id);
1818          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file1->get_itemid(), 'itemid', 0);
1819          $this->assertCount(1, $draftfiles);
1820          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file2->get_itemid(), 'itemid', 0);
1821          $this->assertCount(1, $draftfiles);
1822  
1823          // Create file record.
1824          $filerecord = [
1825              'component' => $file2->get_component(),
1826              'filearea' => $file2->get_filearea(),
1827              'itemid' => $file2->get_itemid(),
1828              'contextid' => $file2->get_contextid(),
1829              'filepath' => '/',
1830              'filename' => $file2->get_filename()
1831          ];
1832  
1833          // Copy file2 into file1's draft area.
1834          file_copy_file_to_file_area($filerecord, $file2->get_filename(), $file1->get_itemid());
1835          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file1->get_itemid(), 'itemid', 0);
1836          $this->assertCount(2, $draftfiles);
1837          $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $file2->get_itemid(), 'itemid', 0);
1838          $this->assertCount(1, $draftfiles);
1839      }
1840  
1841      /**
1842       * Test file_is_draft_areas_limit_reached
1843       */
1844      public function test_file_is_draft_areas_limit_reached() {
1845          global $CFG;
1846          $this->resetAfterTest(true);
1847  
1848          $capacity = $CFG->draft_area_bucket_capacity = 5;
1849          $leak = $CFG->draft_area_bucket_leak = 0.2; // Leaks every 5 seconds.
1850  
1851          $generator = $this->getDataGenerator();
1852          $user = $generator->create_user();
1853  
1854          $this->setUser($user);
1855  
1856          $itemids = [];
1857          for ($i = 0; $i < $capacity; $i++) {
1858              $itemids[$i] = file_get_unused_draft_itemid();
1859          }
1860  
1861          // This test highly depends on time. We try to make sure that the test starts at the early moments on the second.
1862          // This was not needed if MDL-37327 was implemented.
1863          $after = time();
1864          while (time() === $after) {
1865              usleep(100000);
1866          }
1867  
1868          // Burst up to the capacity and make sure that the bucket allows it.
1869          $burststart = microtime();
1870          for ($i = 0; $i < $capacity; $i++) {
1871              if ($i) {
1872                  sleep(1); // A little delay so we have different timemodified value for files.
1873              }
1874              $this->assertFalse(file_is_draft_areas_limit_reached($user->id));
1875              self::create_draft_file([
1876                  'filename' => 'file1.png',
1877                  'itemid' => $itemids[$i],
1878              ]);
1879          }
1880  
1881          // The bucket should be full after bursting.
1882          $this->assertTrue(file_is_draft_areas_limit_reached($user->id));
1883  
1884          // Calculate the time taken to burst up the bucket capacity.
1885          $timetaken = microtime_diff($burststart, microtime());
1886  
1887          // The bucket leaks so it shouldn't be full after a certain time.
1888          // Items are added into the bucket at the rate of 1 item per second.
1889          // One item leaks from the bucket every 1/$leak seconds.
1890          // So it takes 1/$leak - ($capacity-1) seconds for the bucket to leak one item and not be full anymore.
1891          $milliseconds = ceil(1000000 * ((1 / $leak) - ($capacity - 1)) - ($timetaken  * 1000));
1892          usleep($milliseconds);
1893  
1894          $this->assertFalse(file_is_draft_areas_limit_reached($user->id));
1895  
1896          // Only one item was leaked from the bucket. So the bucket should become full again if we add a single item to it.
1897          self::create_draft_file([
1898              'filename' => 'file2.png',
1899              'itemid' => $itemids[0],
1900          ]);
1901          $this->assertTrue(file_is_draft_areas_limit_reached($user->id));
1902  
1903          // The bucket leaks at a constant rate. It doesn't matter if it is filled as the result of bursting or not.
1904          sleep(ceil(1 / $leak));
1905          $this->assertFalse(file_is_draft_areas_limit_reached($user->id));
1906      }
1907  
1908      /**
1909       * Test text cleaning when preparing text editor data.
1910       *
1911       * @covers ::file_prepare_standard_editor
1912       */
1913      public function test_file_prepare_standard_editor_clean_text() {
1914          $text = "lala <object>xx</object>";
1915  
1916          $syscontext = \context_system::instance();
1917  
1918          $object = new \stdClass();
1919          $object->some = $text;
1920          $object->someformat = FORMAT_PLAIN;
1921  
1922          $result = file_prepare_standard_editor(clone($object), 'some',
1923              ['noclean' => false]);
1924          $this->assertSame($text, $result->some);
1925          $result = file_prepare_standard_editor(clone($object), 'some',
1926              ['noclean' => true]);
1927          $this->assertSame($text, $result->some);
1928          $result = file_prepare_standard_editor(clone($object), 'some',
1929              ['noclean' => false, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1930          $this->assertSame($text, $result->some);
1931          $result = file_prepare_standard_editor(clone($object), 'some',
1932              ['noclean' => true, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1933          $this->assertSame($text, $result->some);
1934  
1935          $object = new \stdClass();
1936          $object->some = $text;
1937          $object->someformat = FORMAT_MARKDOWN;
1938  
1939          $result = file_prepare_standard_editor(clone($object), 'some',
1940              ['noclean' => false]);
1941          $this->assertSame($text, $result->some);
1942          $result = file_prepare_standard_editor(clone($object), 'some',
1943              ['noclean' => true]);
1944          $this->assertSame($text, $result->some);
1945          $result = file_prepare_standard_editor(clone($object), 'some',
1946              ['noclean' => false, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1947          $this->assertSame($text, $result->some);
1948          $result = file_prepare_standard_editor(clone($object), 'some',
1949              ['noclean' => true, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1950          $this->assertSame($text, $result->some);
1951  
1952          $object = new \stdClass();
1953          $object->some = $text;
1954          $object->someformat = FORMAT_MOODLE;
1955  
1956          $result = file_prepare_standard_editor(clone($object), 'some',
1957              ['noclean' => false]);
1958          $this->assertSame('lala xx', $result->some);
1959          $result = file_prepare_standard_editor(clone($object), 'some',
1960              ['noclean' => true]);
1961          $this->assertSame($text, $result->some);
1962          $result = file_prepare_standard_editor(clone($object), 'some',
1963              ['noclean' => false, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1964          $this->assertSame('lala xx', $result->some);
1965          $result = file_prepare_standard_editor(clone($object), 'some',
1966              ['noclean' => true, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1967          $this->assertSame($text, $result->some);
1968  
1969          $object = new \stdClass();
1970          $object->some = $text;
1971          $object->someformat = FORMAT_HTML;
1972  
1973          $result = file_prepare_standard_editor(clone($object), 'some',
1974              ['noclean' => false]);
1975          $this->assertSame('lala xx', $result->some);
1976          $result = file_prepare_standard_editor(clone($object), 'some',
1977              ['noclean' => true]);
1978          $this->assertSame($text, $result->some);
1979          $result = file_prepare_standard_editor(clone($object), 'some',
1980              ['noclean' => false, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1981          $this->assertSame('lala xx', $result->some);
1982          $result = file_prepare_standard_editor(clone($object), 'some',
1983              ['noclean' => true, 'context' => $syscontext], $syscontext, 'core', 'some', 1);
1984          $this->assertSame($text, $result->some);
1985      }
1986  
1987      /**
1988       * Tests for file_get_typegroup to check that both arrays, and string values are accepted.
1989       *
1990       * @dataProvider file_get_typegroup_provider
1991       * @param string|array $group
1992       * @param string $expected
1993       */
1994      public function test_file_get_typegroup(
1995          $group,
1996          string $expected
1997      ): void {
1998          $result = file_get_typegroup('type', $group);
1999          $this->assertContains($expected, $result);
2000      }
2001  
2002      public static function file_get_typegroup_provider(): array {
2003          return [
2004              'Array of values' => [
2005                  ['.html', '.htm'],
2006                  'text/html',
2007              ],
2008              'String of comma-separated values' => [
2009                  '.html, .htm',
2010                  'text/html',
2011              ],
2012              'String of colon-separated values' => [
2013                  '.html : .htm',
2014                  'text/html',
2015              ],
2016              'String of semi-colon-separated values' => [
2017                  '.html ; .htm',
2018                  'text/html',
2019              ],
2020          ];
2021      }
2022  }
2023  
2024  /**
2025   * Test-specific class to allow easier testing of curl functions.
2026   *
2027   * @copyright 2015 Dave Cooper
2028   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2029   */
2030  class testable_curl extends curl {
2031      /**
2032       * Accessor for private options array using reflection.
2033       *
2034       * @return array
2035       */
2036      public function get_options() {
2037          // Access to private property.
2038          $rp = new \ReflectionProperty('curl', 'options');
2039          $rp->setAccessible(true);
2040          return $rp->getValue($this);
2041      }
2042  
2043      /**
2044       * Setter for private options array using reflection.
2045       *
2046       * @param array $options
2047       */
2048      public function set_options($options) {
2049          // Access to private property.
2050          $rp = new \ReflectionProperty('curl', 'options');
2051          $rp->setAccessible(true);
2052          $rp->setValue($this, $options);
2053      }
2054  
2055      /**
2056       * Setter for individual option.
2057       * @param string $option
2058       * @param string $value
2059       */
2060      public function set_option($option, $value) {
2061          $options = $this->get_options();
2062          $options[$option] = $value;
2063          $this->set_options($options);
2064      }
2065  
2066      /**
2067       * Unsets an option on the curl object
2068       * @param string $option
2069       */
2070      public function unset_option($option) {
2071          $options = $this->get_options();
2072          unset($options[$option]);
2073          $this->set_options($options);
2074      }
2075  
2076      /**
2077       * Wrapper to access the private \curl::apply_opt() method using reflection.
2078       *
2079       * @param array $options
2080       * @return resource The curl handle
2081       */
2082      public function call_apply_opt($options = null) {
2083          // Access to private method.
2084          $rm = new \ReflectionMethod('curl', 'apply_opt');
2085          $rm->setAccessible(true);
2086          $ch = curl_init();
2087          return $rm->invoke($this, $ch, $options);
2088      }
2089  }