Merge branch 'MDL-72582-MOODLE_500_STABLE' of https://github.com/tsmilan/moodle into MOODLE_500_STABLE

This commit is contained in:
Huong Nguyen
2025-07-15 10:07:50 +07:00
3 changed files with 178 additions and 9 deletions
+16 -9
View File
@@ -2226,10 +2226,11 @@ function readfile_accel($file, $mimetype, $accelerate) {
header('Content-Type: '.$mimetype);
}
$lastmodified = is_object($file) ? $file->get_timemodified() : filemtime($file);
$isfileobj = is_object($file);
$lastmodified = $isfileobj ? $file->get_timemodified() : filemtime($file);
header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
if (is_object($file)) {
if ($isfileobj) {
header('Etag: "' . $file->get_contenthash() . '"');
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) and trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') === $file->get_contenthash()) {
header('HTTP/1.1 304 Not Modified');
@@ -2238,7 +2239,7 @@ function readfile_accel($file, $mimetype, $accelerate) {
}
// if etag present for stored file rely on it exclusively
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) and (empty($_SERVER['HTTP_IF_NONE_MATCH']) or !is_object($file))) {
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (empty($_SERVER['HTTP_IF_NONE_MATCH']) || !$isfileobj)) {
// get unixtime of request header; clip extra junk off first
$since = strtotime(preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]));
if ($since && $since >= $lastmodified) {
@@ -2254,7 +2255,7 @@ function readfile_accel($file, $mimetype, $accelerate) {
}
if ($accelerate) {
if (is_object($file)) {
if ($isfileobj) {
$fs = get_file_storage();
if ($fs->supports_xsendfile()) {
if ($fs->xsendfile_file($file)) {
@@ -2271,7 +2272,8 @@ function readfile_accel($file, $mimetype, $accelerate) {
}
}
$filesize = is_object($file) ? $file->get_filesize() : filesize($file);
$filesize = $isfileobj ? $file->get_filesize() : filesize($file);
$filename = $isfileobj ? $file->get_filename() : $file;
header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
@@ -2305,10 +2307,10 @@ function readfile_accel($file, $mimetype, $accelerate) {
$ranges = false;
}
if ($ranges) {
if (is_object($file)) {
if ($isfileobj) {
$handle = $file->get_content_file_handle();
if ($handle === false) {
throw new file_exception('storedfilecannotreadfile', $file->get_filename());
throw new file_exception('storedfilecannotreadfile', $filename);
}
} else {
$handle = fopen($file, 'rb');
@@ -2334,7 +2336,12 @@ function readfile_accel($file, $mimetype, $accelerate) {
// We do not expect any content in the buffer when we are serving files.
$buffercontents = ob_get_clean();
if ($buffercontents !== '') {
error_log('Non-empty default output handler buffer detected while serving the file ' . $file);
// Include a preview of the first 20 characters of the output buffer to help identify
// what's causing it to be non-empty. This is useful for diagnosing unexpected output
// without exposing full content.
$buffercontentspreview = substr($buffercontents, 0, 20);
debugging("Non-empty default output handler buffer detected while serving the file {$filename}. " .
"Buffer contents (first 20 characters): {$buffercontentspreview}", DEBUG_DEVELOPER);
}
} else {
// Some handlers such as zlib output compression may have file signature buffered - flush it.
@@ -2343,7 +2350,7 @@ function readfile_accel($file, $mimetype, $accelerate) {
}
// send the whole file content
if (is_object($file)) {
if ($isfileobj) {
$file->readfile();
} else {
if (readfile_allow_large($file, $filesize) === false) {
+50
View File
@@ -2076,6 +2076,56 @@ EOF;
],
];
}
/**
* Tests that readfile_accel() triggers the expected debugging message when a non-empty
* output buffer is detected, using both a file path and a stored_file input.
*
* This test runs a CLI script in a separate process to isolate buffer manipulation.
* This is necessary because readfile_accel() uses ob_get_clean() and ob_end_flush(),
* which interfere with PHPUnit's internal output buffer enforcement and cause risky
* test errors.
*
* The CLI script simulates a non-empty output buffer, calls the readfile_accel(), and
* prints any debugging output. The test then captures that output and asserts that the
* correct debugging message was generated.
*
* @covers ::readfile_accel
*/
public function test_readfile_accel_with_path_and_stored_file(): void {
$this->resetAfterTest();
// Construct the command to run the CLI script with a custom constant defined.
$scriptpath = __DIR__ . '/fixtures/readfile_accel_debug_cli.php';
$cmd = 'php -r ' . escapeshellarg("define('PHPUNIT_READFILE_ACCEL_TEST', true); require '$scriptpath';");
$pipes = [];
$process = proc_open($cmd, [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitcode = proc_close($process);
$output = $stdout . $stderr;
// Debug just in case the subprocess fails.
$this->assertSame(0, $exitcode);
// Validate that both path-based and stored_file debugging messages are present.
$filename = "readfile_accel.txt";
$filepath = '/tmp/' . $filename;
$this->assertStringContainsString('Non-empty default output handler buffer detected while serving the file ' .
$filepath . '. Buffer contents (first 20 characters): test text', $output);
$this->assertStringContainsString('Non-empty default output handler buffer detected while serving the file ' .
$filename . '. Buffer contents (first 20 characters): test text', $output);
}
}
/**
+112
View File
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cli script for testing readfile_accel function.
*
* @package core
* @subpackage fixtures
* @copyright 2025 Catalyst IT
* @author Trisha Milan <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('CLI_SCRIPT', true);
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/filelib.php');
if (!defined('PHPUNIT_READFILE_ACCEL_TEST')) {
echo 'This script is only intended to be run via PHPUnit.';
exit(1);
}
$testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
$testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->phpunit_prefix);
$DB = $testdb;
set_debugging(DEBUG_DEVELOPER, true);
$CFG->tempdir = '/tmp';
/**
* Runs readfile_accel() with a file path or a stored_file to trigger the buffer check.
*
* @param string|stored_file $input
* @param string $mimetype
* @param bool $accelerate
*/
function run_readfile_accel_test(string|stored_file $input, string $mimetype, bool $accelerate): void {
try {
ob_start();
echo "test text";
$_SERVER['REQUEST_METHOD'] = 'GET';
readfile_accel($input, $mimetype, $accelerate);
} catch (Exception $e) {
echo $e->getMessage() . "\n";
}
}
try {
// Prepare test file.
$filename = "readfile_accel.txt";
// Generate temporary local file for testing.
$path = "$CFG->tempdir/$filename";
file_put_contents($path, "\nMoodle test data\n");
// Populate {files} table.
$fs = get_file_storage();
$filerecord = [
'contextid' => context_system::instance()->id,
'component' => 'test',
'filearea' => 'readfile',
'itemid' => 0,
'filepath' => '/',
'filename' => $filename,
];
$storedfile = null;
$filerecord['filename'] = $fs->get_unused_filename(
$filerecord['contextid'],
$filerecord['component'],
$filerecord['filearea'],
$filerecord['itemid'],
$filerecord['filepath'],
$filerecord['filename']
);
$storedfile = $fs->create_file_from_pathname($filerecord, $path);
$mimetype = get_mimetype_for_sending($storedfile->get_filename());
$accelerate = true;
// Run the test with direct path.
run_readfile_accel_test($path, $mimetype, $accelerate);
// Run the test with direct stored_file.
run_readfile_accel_test($storedfile, $mimetype, $accelerate);
} finally {
// Clean up {files} table.
if (!is_null($fs)) {
@$fs->delete_area_files(
$filerecord['contextid'],
$filerecord['component'],
$filerecord['filearea'],
$filerecord['itemid']
);
}
// Clean up testing file.
if ($path !== "") {
@unlink($path);
}
}