Skip to content

Commit 8388be0

Browse files
MDL-88265 course: cache-bust course image URL with timemodified
1 parent 402b31e commit 8388be0

4 files changed

Lines changed: 56 additions & 9 deletions

File tree

public/course/classes/cache/course_image.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,16 @@ protected function get_image_url_from_overview_files(\stdClass $course): moodle_
7474
$courseinlist = new core_course_list_element($course);
7575
foreach ($courseinlist->get_course_overviewfiles() as $file) {
7676
if ($file->is_valid_image()) {
77-
return moodle_url::make_pluginfile_url(
77+
$url = moodle_url::make_pluginfile_url(
7878
$file->get_contextid(),
7979
$file->get_component(),
8080
$file->get_filearea(),
8181
null,
8282
$file->get_filepath(),
8383
$file->get_filename()
8484
);
85+
$url->param('oid', $file->get_timemodified());
86+
return $url;
8587
}
8688
}
8789

public/course/renderer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,8 @@ protected function course_overview_files(core_course_list_element $course): stri
519519
$url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php",
520520
'/' . $file->get_contextid() . '/' . $file->get_component() . '/' .
521521
$file->get_filearea() . $file->get_filepath() . $file->get_filename(), !$isimage);
522+
$url->param('oid', $file->get_timemodified());
523+
522524
if ($isimage) {
523525
$contentimages .= html_writer::tag('div',
524526
html_writer::empty_tag('img', ['src' => $url, 'alt' => '']),

public/course/tests/course_image_cache_test.php

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
3131
* @copyright 2021 Catalyst IT
3232
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33+
* @covers \core_course\cache\course_image
3334
*/
3435
final class course_image_cache_test extends \advanced_testcase {
3536

@@ -76,11 +77,13 @@ protected function fill_draft_area(array $files): int {
7677
*
7778
* @param \stdClass $course Course object.
7879
* @param string $filename File name.
80+
* @param int $timemodified File timemodified timestamp for cache-busting.
7981
* @return string
8082
*/
81-
protected function build_expected_course_image_url(\stdClass $course, string $filename): string {
83+
protected function build_expected_course_image_url(\stdClass $course, string $filename, int $timemodified): string {
8284
$contextid = context_course::instance($course->id)->id;
83-
return 'https://www.example.com/moodle/pluginfile.php/' . $contextid. '/course/overviewfiles/' . $filename;
85+
return 'https://www.example.com/moodle/pluginfile.php/' . $contextid .
86+
'/course/overviewfiles/' . $filename . '?oid=' . $timemodified;
8487
}
8588

8689
/**
@@ -129,7 +132,9 @@ public function test_get_image_url_from_overview_files_returns_url_if_there_is_a
129132
'filename1.jpg' => file_get_contents(self::get_fixture_path(__NAMESPACE__, 'image.jpg')),
130133
]);
131134
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);
132-
$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg');
135+
$contextid = context_course::instance($course1->id)->id;
136+
$file = get_file_storage()->get_file($contextid, 'course', 'overviewfiles', 0, '/', 'filename1.jpg');
137+
$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg', $file->get_timemodified());
133138
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
134139
}
135140

@@ -146,8 +151,9 @@ public function test_get_image_url_from_overview_files_returns_url_of_the_first_
146151
'filename2.jpg' => file_get_contents(self::get_fixture_path(__NAMESPACE__, 'image.jpg')),
147152
]);
148153
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);
149-
150-
$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg');
154+
$contextid = context_course::instance($course1->id)->id;
155+
$file = get_file_storage()->get_file($contextid, 'course', 'overviewfiles', 0, '/', 'filename1.jpg');
156+
$expected = $this->build_expected_course_image_url($course1, 'filename1.jpg', $file->get_timemodified());
151157
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
152158
}
153159

@@ -165,9 +171,44 @@ public function test_get_image_url_from_overview_files_returns_url_of_the_first_
165171
'filename3.jpg' => file_get_contents(self::get_fixture_path(__NAMESPACE__, 'image.jpg')),
166172
]);
167173
$course1 = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid1]);
168-
169-
$expected = $this->build_expected_course_image_url($course1, 'filename2.jpg');
174+
$contextid = context_course::instance($course1->id)->id;
175+
$file = get_file_storage()->get_file($contextid, 'course', 'overviewfiles', 0, '/', 'filename2.jpg');
176+
$expected = $this->build_expected_course_image_url($course1, 'filename2.jpg', $file->get_timemodified());
170177
$this->assertEquals($expected, $method->invokeArgs($cache, [$course1]));
171178
}
172179

180+
/**
181+
* Test that replacing a course image with a file of the same name produces a different URL.
182+
*/
183+
public function test_get_image_url_changes_when_file_replaced_with_same_name(): void {
184+
$method = new ReflectionMethod(course_image::class, 'get_image_url_from_overview_files');
185+
$cache = course_image::get_instance_for_cache(new definition());
186+
$imagecontent = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'image.jpg'));
187+
188+
// Create course with initial image.
189+
$draftid = $this->fill_draft_area(['image.jpg' => $imagecontent]);
190+
$course = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid]);
191+
192+
$contextid = context_course::instance($course->id)->id;
193+
$fs = get_file_storage();
194+
$originalfile = $fs->get_file($contextid, 'course', 'overviewfiles', 0, '/', 'image.jpg');
195+
$url1 = $method->invokeArgs($cache, [$course])->out_as_local_url();
196+
197+
// Simulate file replacement with same name: delete original, re-create with a later timemodified.
198+
$newrecord = [
199+
'contextid' => $contextid,
200+
'component' => 'course',
201+
'filearea' => 'overviewfiles',
202+
'itemid' => 0,
203+
'filepath' => '/',
204+
'filename' => 'image.jpg',
205+
'timemodified' => $originalfile->get_timemodified() + 1,
206+
];
207+
$originalfile->delete();
208+
$fs->create_file_from_string($newrecord, $imagecontent);
209+
210+
$url2 = $method->invokeArgs($cache, [$course])->out_as_local_url();
211+
212+
$this->assertNotEquals($url1, $url2, 'URL must change when course image is replaced with a file of the same name.');
213+
}
173214
}

public/course/tests/course_summary_exporter_test.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ public function test_get_course_image_when_overview_images_are_uploaded(): void
6464
$fs->create_file_from_string($filerecord, file_get_contents(self::get_fixture_path(__NAMESPACE__, 'image.jpg')));
6565
$course = $this->getDataGenerator()->create_course(['overviewfiles_filemanager' => $draftid]);
6666
$coursecontext = context_course::instance($course->id);
67+
$storedfile = get_file_storage()->get_file($coursecontext->id, 'course', 'overviewfiles', 0, '/', 'image.jpg');
6768

68-
$expected = 'https://www.example.com/moodle/pluginfile.php/' . $coursecontext->id . '/course/overviewfiles/image.jpg';
69+
$expected = 'https://www.example.com/moodle/pluginfile.php/' . $coursecontext->id .
70+
'/course/overviewfiles/image.jpg?oid=' . $storedfile->get_timemodified();
6971
$actual = course_summary_exporter::get_course_image($course);
7072
$this->assertSame($expected, $actual);
7173
}

0 commit comments

Comments
 (0)