mirror of
https://github.com/moodle/moodle.git
synced 2025-07-26 08:50:30 +02:00
357 lines
12 KiB
PHP
357 lines
12 KiB
PHP
<?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/>.
|
|
|
|
/**
|
|
* Tiny text editor integration - Language Producer.
|
|
*
|
|
* @package editor_tiny
|
|
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace editor_tiny;
|
|
|
|
// Disable moodle specific debug messages and any errors in output,
|
|
// comment out when debugging or better look into error log!
|
|
define('NO_DEBUG_DISPLAY', true);
|
|
|
|
// We need just the values from config.php and minlib.php.
|
|
define('ABORT_AFTER_CONFIG', true);
|
|
|
|
// This stops immediately at the beginning of lib/setup.php.
|
|
require('../../../config.php');
|
|
|
|
/**
|
|
* An anonymous class to handle loading and serving lang files for TinyMCE.
|
|
*
|
|
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class lang {
|
|
/** @var string The language code to load */
|
|
protected $lang;
|
|
|
|
/** @var int The revision requested */
|
|
protected $rev;
|
|
|
|
/** @var bool Whether Moodle is fully loaded or not */
|
|
protected $fullyloaded = false;
|
|
|
|
/** @var string The complete path to the candidate file */
|
|
protected $candidatefile;
|
|
|
|
/**
|
|
* Constructor to load and serve the langfile.
|
|
*/
|
|
public function __construct() {
|
|
$this->parse_file_information_from_url();
|
|
$this->serve_file();
|
|
}
|
|
|
|
/**
|
|
* Parse the file information from the URL.
|
|
*/
|
|
protected function parse_file_information_from_url(): void {
|
|
global $CFG;
|
|
|
|
// The URL format is /[revision]/[lang].
|
|
// The revision is an integer with negative values meaning the file is not cached.
|
|
// The lang is a simple word with no directory separators or special characters.
|
|
if ($slashargument = min_get_slash_argument()) {
|
|
$slashargument = ltrim($slashargument, '/');
|
|
if (substr_count($slashargument, '/') < 1) {
|
|
css_send_css_not_found();
|
|
}
|
|
|
|
[$rev, $lang] = explode('/', $slashargument, 2);
|
|
$rev = min_clean_param($rev, 'INT');
|
|
$lang = min_clean_param($lang, 'SAFEDIR');
|
|
} else {
|
|
$rev = min_optional_param('rev', 0, 'INT');
|
|
$lang = min_optional_param('lang', 'standard', 'SAFEDIR');
|
|
}
|
|
|
|
// Retrieve the correct language by converting to Moodle's language code format.
|
|
$this->lang = str_replace('-', '_', $lang);
|
|
$this->rev = $rev;
|
|
$this->candidatefile = "{$CFG->localcachedir}/editor_tiny/{$this->rev}/lang/{$this->lang}/lang.json";
|
|
}
|
|
|
|
/**
|
|
* Serve the language pack content.
|
|
*/
|
|
protected function serve_file(): void {
|
|
// Attempt to send the cached langpack.
|
|
if ($this->rev > 0) {
|
|
if ($this->is_candidate_file_available()) {
|
|
// The send_cached_file_if_available function will exit if successful.
|
|
// In theory the file could become unavailable after checking that the file exists.
|
|
// Whilst this is unlikely, fall back to caching the content below.
|
|
$this->send_cached_pack();
|
|
}
|
|
|
|
// The file isn't cached yet.
|
|
// Load the content. store it in the cache, and serve it.
|
|
$strings = $this->load_language_pack();
|
|
$this->store_lang_file($strings);
|
|
$this->send_cached();
|
|
} else {
|
|
// If the revision is less than 0, then do not cache anything.
|
|
$strings = $this->load_language_pack();
|
|
$this->send_uncached($strings);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the full Moodle Framework.
|
|
*/
|
|
protected function load_full_moodle(): void {
|
|
global $CFG, $DB, $SESSION, $OUTPUT, $PAGE;
|
|
|
|
if ($this->is_full_moodle_loaded()) {
|
|
return;
|
|
}
|
|
|
|
// Ok, now we need to start normal moodle script, we need to load all libs and $DB.
|
|
define('ABORT_AFTER_CONFIG_CANCEL', true);
|
|
|
|
// Session not used here.
|
|
define('NO_MOODLE_COOKIES', true);
|
|
|
|
// Ignore upgrade check.
|
|
define('NO_UPGRADE_CHECK', true);
|
|
|
|
require("{$CFG->dirroot}/lib/setup.php");
|
|
$this->fullyloaded = true;
|
|
}
|
|
|
|
/**
|
|
* Check whether Moodle is fully loaded.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_full_moodle_loaded(): bool {
|
|
return $this->fullyloaded;
|
|
}
|
|
|
|
/**
|
|
* Load the language pack strings.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
protected function load_language_pack(): array {
|
|
// We need to load the full moodle API to use the string manager.
|
|
$this->load_full_moodle();
|
|
|
|
// We maintain a list of string identifier to original TinyMCE string.
|
|
// TinyMCE uses English language strings to perform translations.
|
|
$stringlist = file_get_contents(__DIR__ . "/tinystrings.json");
|
|
if (empty($stringlist)) {
|
|
$this->send_not_found("Failed to load strings from tinystrings.json");
|
|
}
|
|
|
|
$stringlist = json_decode($stringlist, true);
|
|
if (empty($stringlist)) {
|
|
$this->send_not_found("Failed to load strings from tinystrings.json");
|
|
}
|
|
|
|
// Load all strings for the TinyMCE Editor which have a prefix of `tiny:` from the Moodle String Manager.
|
|
$stringmanager = get_string_manager();
|
|
$translatedvalues = array_filter(
|
|
$stringmanager->load_component_strings('editor_tiny', $this->lang),
|
|
function(string $value, string $key): bool {
|
|
return strpos($key, 'tiny:') === 0;
|
|
},
|
|
ARRAY_FILTER_USE_BOTH
|
|
);
|
|
|
|
// We will associate the _original_ TinyMCE string to its translation, but only where it is different.
|
|
// Where the original TinyMCE string matches the Moodle translation of it, we do not supply the string.
|
|
$strings = [];
|
|
foreach ($stringlist as $key => $value) {
|
|
if (array_key_exists($key, $translatedvalues)) {
|
|
if ($translatedvalues[$key] !== $value) {
|
|
$strings[$value] = $translatedvalues[$key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// TinyMCE uses a secret string only present in some languages to set a language direction.
|
|
// Rather than applying to only some languages, we just apply to all from our own langconfig.
|
|
// Note: Do not rely on right_to_left() as the current language is unset.
|
|
$strings['_dir'] = $stringmanager->get_string('thisdirection', 'langconfig', null, $this->lang);
|
|
|
|
return $strings;
|
|
}
|
|
|
|
/**
|
|
* Send a cached language pack.
|
|
*/
|
|
protected function send_cached_pack(): void {
|
|
global $CFG;
|
|
|
|
if (file_exists($this->candidatefile)) {
|
|
if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
|
// We do not actually need to verify the etag value because our files
|
|
// never change in cache because we increment the rev counter.
|
|
$this->send_unmodified_headers(filemtime($this->candidatefile));
|
|
}
|
|
$this->send_cached($this->candidatefile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store a langauge cache file containing all of the processed strings.
|
|
*
|
|
* @param string[] $strings The strings to store
|
|
*/
|
|
protected function store_lang_file(array $strings): void {
|
|
global $CFG;
|
|
|
|
clearstatcache();
|
|
if (!file_exists(dirname($this->candidatefile))) {
|
|
@mkdir(dirname($this->candidatefile), $CFG->directorypermissions, true);
|
|
}
|
|
|
|
// Prevent serving of incomplete file from concurrent request,
|
|
// the rename() should be more atomic than fwrite().
|
|
ignore_user_abort(true);
|
|
|
|
// First up write out the single file for all those using decent browsers.
|
|
$content = json_encode($strings, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES);
|
|
|
|
$filename = $this->candidatefile;
|
|
if ($fp = fopen($filename . '.tmp', 'xb')) {
|
|
fwrite($fp, $content);
|
|
fclose($fp);
|
|
rename($filename . '.tmp', $filename);
|
|
@chmod($filename, $CFG->filepermissions);
|
|
@unlink($filename . '.tmp'); // Just in case anything fails.
|
|
}
|
|
|
|
ignore_user_abort(false);
|
|
if (connection_aborted()) {
|
|
die;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the candidate file exists.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function is_candidate_file_available(): bool {
|
|
return file_exists($this->candidatefile);
|
|
}
|
|
|
|
/**
|
|
* Get the eTag for the candidate file.
|
|
*
|
|
* This is a unique hash based on the file arguments.
|
|
* It does not need to consider the file content because we use a cache busting URL.
|
|
*
|
|
* @return string The eTag content
|
|
*/
|
|
protected function get_etag(): string {
|
|
$etag = [
|
|
$this->lang,
|
|
$this->rev,
|
|
];
|
|
|
|
return sha1(implode('/', $etag));
|
|
}
|
|
|
|
/**
|
|
* Send the candidate file, with aggressive cachign headers.
|
|
*
|
|
* This includdes eTags, a last-modified, and expiry approximately 90 days in the future.
|
|
*/
|
|
protected function send_cached(): void {
|
|
$path = $this->candidatefile;
|
|
|
|
// 90 days only - based on Moodle point release cadence being every 3 months.
|
|
$lifetime = 60 * 60 * 24 * 90;
|
|
|
|
header('Etag: "' . $this->get_etag() . '"');
|
|
header('Content-Disposition: inline; filename="lang.php"');
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($path)) . ' GMT');
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');
|
|
header('Pragma: ');
|
|
header('Cache-Control: public, max-age=' . $lifetime . ', immutable');
|
|
header('Accept-Ranges: none');
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
if (!min_enable_zlib_compression()) {
|
|
header('Content-Length: ' . filesize($path));
|
|
}
|
|
|
|
readfile($path);
|
|
die;
|
|
}
|
|
|
|
/**
|
|
* Sends the content directly without caching it.
|
|
*
|
|
* @param string[] $strings
|
|
*/
|
|
protected function send_uncached(array $strings): void {
|
|
header('Content-Disposition: inline; filename="styles_debug.php"');
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
|
header('Pragma: ');
|
|
header('Accept-Ranges: none');
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
echo json_encode($strings, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES);
|
|
die;
|
|
}
|
|
|
|
/**
|
|
* Send file not modified headers.
|
|
*
|
|
* @param int $lastmodified
|
|
*/
|
|
protected function send_unmodified_headers($lastmodified): void {
|
|
// 90 days only - based on Moodle point release cadence being every 3 months.
|
|
$lifetime = 60 * 60 * 24 * 90;
|
|
header('HTTP/1.1 304 Not Modified');
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');
|
|
header('Cache-Control: public, max-age=' . $lifetime);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Etag: "' . $this->get_etag() . '"');
|
|
if ($lastmodified) {
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastmodified) . ' GMT');
|
|
}
|
|
die;
|
|
}
|
|
|
|
/**
|
|
* Sends a 404 message to indicate that the content was not found.
|
|
*
|
|
* @param null|string $message An optional informative message to include to help debugging
|
|
*/
|
|
protected function send_not_found(?string $message = null): void {
|
|
header('HTTP/1.0 404 not found');
|
|
|
|
if ($message) {
|
|
die($message);
|
|
} else {
|
|
die('Language data was not found, sorry.');
|
|
}
|
|
}
|
|
};
|
|
|
|
$loader = new lang();
|