Skip to content

Commit

Permalink
MDL-79802 core_h5p: Add a new setting for adding custom H5P styles.
Browse files Browse the repository at this point in the history
  • Loading branch information
gjb2048 committed Feb 20, 2024
1 parent e4bbd79 commit 39fcdac
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 60 deletions.
14 changes: 14 additions & 0 deletions admin/settings/h5p.php
Expand Up @@ -41,4 +41,18 @@

$settings->add(new admin_settings_h5plib_handler_select('h5plibraryhandler', new lang_string('h5plibraryhandler', 'core_h5p'),
new lang_string('h5plibraryhandler_help', 'core_h5p'), $defaulth5plib));

$setting = new admin_setting_configtextarea(
'core_h5p/h5pcustomcss',
new lang_string('h5pcustomcss', 'core_h5p'),
new lang_string('h5pcustomcss_help', 'core_h5p'),
'',
PARAM_NOTAGS
);
$setting->set_updatedcallback(function () {
// Enables use of file_storage constants.
\core_h5p\local\library\autoloader::register();
\core_h5p\file_storage::generate_custom_styles();
});
$settings->add($setting);
}
102 changes: 102 additions & 0 deletions h5p/classes/file_storage.php
Expand Up @@ -54,6 +54,8 @@ class file_storage implements H5PFileStorage {
public const CSS_FILEAREA = 'css';
/** The icon filename */
public const ICON_FILENAME = 'icon.svg';
/** The custom CSS filename */
private const CUSTOM_CSS_FILENAME = 'custom_h5p.css';

/**
* @var \context $context Currently we use the system context everywhere.
Expand Down Expand Up @@ -879,4 +881,104 @@ private function move_file(string $sourcefile, int $contentid): void {

$this->fs->create_file_from_pathname($record, $sourcefile);
}

/**
* Generate H5P custom styles if any.
*/
public static function generate_custom_styles(): void {
$record = self::get_custom_styles_file_record();
$cssfile = self::get_custom_styles_file($record);
if ($cssfile) {
// The CSS file needs to be updated, so delete and recreate it
// if there is CSS in the 'h5pcustomcss' setting.
$cssfile->delete();
}

$css = get_config('core_h5p', 'h5pcustomcss');
if (!empty($css)) {
$fs = get_file_storage();
$fs->create_file_from_string($record, $css);
}
}

/**
* Get H5P custom styles if any.
*
* @throws \moodle_exception If the CSS setting is empty but there is a file to serve
* or there is no file but the CSS setting is not empty.
* @return array|null If there is CSS then an array with the keys 'cssurl'
* and 'cssversion' is returned otherwise null. 'cssurl' is a link to the
* generated 'custom_h5p.css' file and 'cssversion' the md5 hash of its contents.
*/
public static function get_custom_styles(): ?array {
$record = self::get_custom_styles_file_record();

$css = get_config('core_h5p', 'h5pcustomcss');
if (self::get_custom_styles_file($record)) {
if (empty($css)) {
// The custom CSS file exists and yet the setting 'h5pcustomcss' is empty.
// This prevents an invalid content hash.
throw new \moodle_exception(
'The H5P \'h5pcustomcss\' setting is empty and yet the custom CSS file \''.
$record['filename'].
'\' exists.',
'core_h5p'
);
}
// File exists, so generate the url and version hash.
$cssurl = \moodle_url::make_pluginfile_url(
$record['contextid'],
$record['component'],
$record['filearea'],
null,
$record['filepath'],
$record['filename']
);
return ['cssurl' => $cssurl, 'cssversion' => md5($css)];
} else if (!empty($css)) {
// The custom CSS file does not exist and yet should do.
throw new \moodle_exception(
'The H5P custom CSS file \''.
$record['filename'].
'\' does not exist and yet there is CSS in the \'h5pcustomcss\' setting.',
'core_h5p'
);
}
return null;
}

/**
* Get H5P custom styles file record.
*
* @return array File record for the CSS custom styles.
*/
private static function get_custom_styles_file_record(): array {
return [
'contextid' => \context_system::instance()->id,
'component' => self::COMPONENT,
'filearea' => self::CSS_FILEAREA,
'itemid' => 0,
'filepath' => '/',
'filename' => self::CUSTOM_CSS_FILENAME,
];
}

/**
* Get H5P custom styles file.
*
* @param array $record The H5P custom styles file record.
*
* @return stored_file|bool stored_file instance if exists, false if not.
*/
private static function get_custom_styles_file($record): stored_file|bool {
$fs = get_file_storage();
return $fs->get_file(
$record['contextid'],
$record['component'],
$record['filearea'],
$record['itemid'],
$record['filepath'],
$record['filename']
);
}
}
67 changes: 7 additions & 60 deletions h5p/classes/output/renderer.php
Expand Up @@ -39,67 +39,14 @@ class renderer extends plugin_renderer_base {
* @param string $embedtype Possible values: div, iframe, external, editor
*/
public function h5p_alter_styles(&$styles, array $libraries, string $embedtype) {
global $CFG, $DB;

$record = [
'contextid' => \context_system::instance()->id,
'component' => \core_h5p\file_storage::COMPONENT,
'filearea' => \core_h5p\file_storage::CSS_FILEAREA,
'itemid' => 0,
'filepath' => '/',
'filename' => $CFG->theme . '_h5p.css',
];
$fs = get_file_storage();
// Check if the CSS file for the current theme needs to be updated (because the SCSS settings have changed recently).
if ($cssfile = $fs->get_file(
$record['contextid'],
$record['component'],
$record['filearea'],
$record['itemid'],
$record['filepath'],
$record['filename'])) {
// Get the last time when the SCSS and CSSPRE settings were updated for the current theme and compare it with the
// time modified of the H5P CSS file, to determine whether it needs to be updated.
$sql = "SELECT MAX(timemodified) as timemodified
FROM {config_log}
WHERE plugin = :theme AND (name = 'scss' OR name = 'scsspre')";
$params = ['theme' => 'theme_' . $CFG->theme];
$setting = $DB->get_record_sql($sql, $params);
if ($setting && $setting->timemodified > $cssfile->get_timemodified()) {
// The CSS file needs to be updated. First, delete it to recreate it later with the current CSS.
$cssfile->delete();
$cssfile = null;
}
}

$theme = \theme_config::load($CFG->theme);
// When 'Raw initial SCSS' and 'Raw SCSS' theme settings are empty, the file doesn't need to be created.
if (empty($theme->settings->scsspre) && empty($theme->settings->scss)) {
return;
$customcss = \core_h5p\file_storage::get_custom_styles();
if (!empty($customcss)) {
// Add the CSS file to the styles array, to load it from the H5P player.
$styles[] = (object) [
'path' => $customcss['cssurl']->out(),
'version' => '?ver='.$customcss['cssversion'],
];
}

// If the CSS file doesn't exist, create it with the styles defined in 'Raw initial SCSS' and 'Raw SCSS' theme settings.
// As these scss and scsspre settings might have dependencies on the theme, the whole CSS theme content will be used and
// passed to the H5P player.
if (!$cssfile) {
$css = $theme->get_css_content();
$cssfile = $fs->create_file_from_string($record, $css);
}

$cssurl = \moodle_url::make_pluginfile_url(
$record['contextid'],
$record['component'],
$record['filearea'],
null,
$record['filepath'],
$record['filename']
);

// Add the CSS file to the styles array, to load it from the H5P player.
$styles[] = (object) [
'path' => $cssurl->out(),
'version' => '?ver='.$cssfile->get_timemodified(),
];
}

/**
Expand Down
111 changes: 111 additions & 0 deletions h5p/tests/file_storage_test.php
Expand Up @@ -848,4 +848,115 @@ public function test_removeContentFile(): void {
$this->assertFalse($this->h5p_fs_fs->file_exists($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $h5pcontentid, $filepath, $filename));
}

/**
* Test H5P custom styles generation.
*
* @covers ::generate_custom_styles
*/
public function test_generate_custom_styles(): void {
\set_config('h5pcustomcss', '.debug { color: #fab; }', 'core_h5p');
$h5pfsrc = new \ReflectionClass(file_storage::class);
$customcssfilename = $h5pfsrc->getConstant('CUSTOM_CSS_FILENAME');

// Test 'h5pcustomcss' with data.
file_storage::generate_custom_styles();

$this->assertTrue($this->h5p_fs_fs->file_exists(
\context_system::instance()->id,
file_storage::COMPONENT,
file_storage::CSS_FILEAREA,
0,
'/',
$customcssfilename)
);

$cssfile = $this->h5p_fs_fs->get_file(
\context_system::instance()->id,
file_storage::COMPONENT,
file_storage::CSS_FILEAREA,
0,
'/',
$customcssfilename
);
$this->assertInstanceOf('stored_file', $cssfile);

$csscontents = $cssfile->get_content();
$this->assertEquals($csscontents, '.debug { color: #fab; }');

// Test 'h5pcustomcss' without data.
\set_config('h5pcustomcss', '', 'core_h5p');
file_storage::generate_custom_styles();
$this->assertFalse($this->h5p_fs_fs->file_exists(
\context_system::instance()->id,
file_storage::COMPONENT,
file_storage::CSS_FILEAREA,
0,
'/',
$customcssfilename)
);
}

/**
* Test H5P custom styles retrieval.
*
* @covers ::get_custom_styles
*/
public function test_get_custom_styles(): void {
global $CFG;
$css = '.debug { color: #fab; }';
$cssurl = $CFG->wwwroot . '/pluginfile.php/1/core_h5p/css/custom_h5p.css';
\set_config('h5pcustomcss', $css, 'core_h5p');
$h5pfsrc = new \ReflectionClass(file_storage::class);
$customcssfilename = $h5pfsrc->getConstant('CUSTOM_CSS_FILENAME');

// Normal operation without data.
\set_config('h5pcustomcss', '', 'core_h5p');
file_storage::generate_custom_styles();
$style = file_storage::get_custom_styles();
$this->assertNull($style);

// Normal operation with data.
\set_config('h5pcustomcss', $css, 'core_h5p');
file_storage::generate_custom_styles();
$style = file_storage::get_custom_styles();

$this->assertNotEmpty($style);
$this->assertEquals($style['cssurl']->out(), $cssurl);
$this->assertEquals($style['cssversion'], md5($css));

// No CSS set when there is a file.
\set_config('h5pcustomcss', '', 'core_h5p');
try {
$style = file_storage::get_custom_styles();
$this->fail('moodle_exception for when there is no CSS and yet there is a file, was not thrown');
} catch (\moodle_exception $me) {
$this->assertEquals(
'The H5P \'h5pcustomcss\' setting is empty and yet the custom CSS file \''.$customcssfilename.'\' exists.',
$me->errorcode
);
}
\set_config('h5pcustomcss', $css, 'core_h5p'); // Reset for next assertion.

// No CSS file when there is CSS.
$cssfile = $this->h5p_fs_fs->get_file(
\context_system::instance()->id,
file_storage::COMPONENT,
file_storage::CSS_FILEAREA,
0,
'/',
$customcssfilename
);
$cssfile->delete();
try {
$style = file_storage::get_custom_styles();
$this->fail('moodle_exception for when there is CSS and yet there is a file, was not thrown');
} catch (\moodle_exception $me) {
$this->assertEquals(
'The H5P custom CSS file \''.$customcssfilename.
'\' does not exist and yet there is CSS in the \'h5pcustomcss\' setting.',
$me->errorcode
);
}
}
}
4 changes: 4 additions & 0 deletions h5p/upgrade.txt
Expand Up @@ -3,6 +3,10 @@ information provided here is intended especially for developers.

=== 4.3 ===
* The `\core_h5p\file_storage::EDITOR_FILEAREA` constant has been removed, as all editor files are stored in user draft
* Added new methods, 'generate_custom_styles' and 'get_custom_styles' to 'core_h5p\file_storage' to generate then
provide a CSS file to be used by the 'core_h5p\output\renderer\h5p_alter_styles' method, reference:
(https://h5p.org/documentation/for-developers/visual-changes) when there is custom CSS in the 'core_h5p\h5pcustomcss'
setting. This will then apply to the H5P content.

=== 4.0 ===
* Added new methods to api: get_original_content_from_pluginfile_url and can_edit_content.
Expand Down
2 changes: 2 additions & 0 deletions lang/en/h5p.php
Expand Up @@ -131,6 +131,8 @@
$string['h5pinvalidurl'] = 'Invalid H5P content URL.';
$string['h5plibraryhandler'] = 'H5P framework handler';
$string['h5plibraryhandler_help'] = 'The H5P framework used to display H5P content. The latest version is recommended.';
$string['h5pcustomcss'] = 'Custom CSS';
$string['h5pcustomcss_help'] = 'Custom CSS to apply to your H5P modules.';
$string['h5pprivatefile'] = 'This H5P content can\'t be displayed because you don\'t have access to the .h5p file.';
$string['h5pmanage'] = 'Manage H5P content types';
$string['h5poverview'] = 'H5P overview';
Expand Down

0 comments on commit 39fcdac

Please sign in to comment.