Search moodle.org's
Developer Documentation

See Release Notes

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

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

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