MDL-69583 tool_customlang: add customization import

This commit is contained in:
Ferran Recio 2020-08-25 11:06:14 +02:00
parent 46f977a8bf
commit 489f6f2684
17 changed files with 2468 additions and 1 deletions

View File

@ -0,0 +1,68 @@
<?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/>.
/**
* Upload a zip of custom lang php files.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\form;
use tool_customlang\local\importer;
/**
* Upload a zip/php of custom lang php files.
*
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class import extends \moodleform {
/**
* Form definition.
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('header', 'settingsheader', get_string('import', 'tool_customlang'));
$mform->addElement('hidden', 'lng');
$mform->setType('lng', PARAM_LANG);
$mform->setDefault('lng', $this->_customdata['lng']);
$filemanageroptions = array(
'accepted_types' => array('.php', '.zip'),
'maxbytes' => 0,
'maxfiles' => 1,
'subdirs' => 0
);
$mform->addElement('filepicker', 'pack', get_string('langpack', 'tool_customlang'),
null, $filemanageroptions);
$mform->addRule('pack', null, 'required');
$modes = [
importer::IMPORTALL => get_string('import_all', 'tool_customlang'),
importer::IMPORTUPDATE => get_string('import_update', 'tool_customlang'),
importer::IMPORTNEW => get_string('import_new', 'tool_customlang'),
];
$mform->addElement('select', 'importmode', get_string('import_mode', 'tool_customlang'), $modes);
$mform->addElement('submit', 'importcustomstrings', get_string('importfile', 'tool_customlang'));
}
}

View File

@ -0,0 +1,268 @@
<?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/>.
/**
* Custom lang importer.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\local;
use tool_customlang\local\mlang\phpparser;
use tool_customlang\local\mlang\logstatus;
use tool_customlang\local\mlang\langstring;
use core\output\notification;
use stored_file;
use coding_exception;
use moodle_exception;
use core_component;
use stdClass;
/**
* Class containing tha custom lang importer
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class importer {
/** @var int imports will only create new customizations */
public const IMPORTNEW = 1;
/** @var int imports will only update the current customizations */
public const IMPORTUPDATE = 2;
/** @var int imports all strings */
public const IMPORTALL = 3;
/**
* @var string the language name
*/
protected $lng;
/**
* @var int the importation mode (new, update, all)
*/
protected $importmode;
/**
* @var string request folder path
*/
private $folder;
/**
* @var array import log messages
*/
private $log;
/**
* Constructor for the importer class.
*
* @param string $lng the current language to import.
* @param int $importmode the import method (IMPORTALL, IMPORTNEW, IMPORTUPDATE).
*/
public function __construct(string $lng, int $importmode = self::IMPORTALL) {
$this->lng = $lng;
$this->importmode = $importmode;
$this->log = [];
}
/**
* Returns the last parse log.
*
* @return logstatus[] mlang logstatus with the messages
*/
public function get_log(): array {
return $this->log;
}
/**
* Import customlang files.
*
* @param stored_file[] $files array of files to import
*/
public function import(array $files): void {
// Create a temporal folder to store the files.
$this->folder = make_request_directory(false);
$langfiles = $this->deploy_files($files);
$this->process_files($langfiles);
}
/**
* Deploy all files into a request folder.
*
* @param stored_file[] $files array of files to deploy
* @return string[] of file paths
*/
private function deploy_files(array $files): array {
$result = [];
// Desploy all files.
foreach ($files as $file) {
if ($file->get_mimetype() == 'application/zip') {
$result = array_merge($result, $this->unzip_file($file));
} else {
$path = $this->folder.'/'.$file->get_filename();
$file->copy_content_to($path);
$result = array_merge($result, [$path]);
}
}
return $result;
}
/**
* Unzip a file into the request folder.
*
* @param stored_file $file the zip file to unzip
* @return string[] of zip content paths
*/
private function unzip_file(stored_file $file): array {
$fp = get_file_packer('application/zip');
$zipcontents = $fp->extract_to_pathname($file, $this->folder);
if (!$zipcontents) {
throw new moodle_exception("Error Unzipping file", 1);
}
$result = [];
foreach ($zipcontents as $contentname => $success) {
if ($success) {
$result[] = $this->folder.'/'.$contentname;
}
}
return $result;
}
/**
* Import strings from a list of langfiles.
*
* @param string[] $langfiles an array with file paths
*/
private function process_files(array $langfiles): void {
$parser = phpparser::get_instance();
foreach ($langfiles as $filepath) {
$component = $this->component_from_filepath($filepath);
if ($component) {
$strings = $parser->parse(file_get_contents($filepath));
$this->import_strings($strings, $component);
}
}
}
/**
* Try to get the component from a filepath.
*
* @param string $filepath the filepath
* @return stdCalss|null the DB record of that component
*/
private function component_from_filepath(string $filepath) {
global $DB;
// Get component from filename.
$pathparts = pathinfo($filepath);
if (empty($pathparts['filename'])) {
throw new coding_exception("Cannot get filename from $filepath", 1);
}
$filename = $pathparts['filename'];
$normalized = core_component::normalize_component($filename);
if (count($normalized) == 1 || empty($normalized[1])) {
$componentname = $normalized[0];
} else {
$componentname = implode('_', $normalized);
}
$result = $DB->get_record('tool_customlang_components', ['name' => $componentname]);
if (!$result) {
$this->log[] = new logstatus('notice_missingcomponent', notification::NOTIFY_ERROR, null, $componentname);
return null;
}
return $result;
}
/**
* Import an array of strings into the customlang tables.
*
* @param langstring[] $strings the langstring to set
* @param stdClass $component the target component
*/
private function import_strings(array $strings, stdClass $component): void {
global $DB;
foreach ($strings as $newstring) {
// Check current DB entry.
$customlang = $DB->get_record('tool_customlang', [
'componentid' => $component->id,
'stringid' => $newstring->id,
'lang' => $this->lng,
]);
if (!$customlang) {
$customlang = null;
}
if ($this->can_save_string($customlang, $newstring, $component)) {
$customlang->local = $newstring->text;
$customlang->timecustomized = $newstring->timemodified;
$customlang->outdated = 0;
$customlang->modified = 1;
$DB->update_record('tool_customlang', $customlang);
}
}
}
/**
* Determine if a specific string can be saved based on the current importmode.
*
* @param stdClass $customlang customlang original record
* @param langstring $newstring the new strign to store
* @param stdClass $component the component target
* @return bool if the string can be stored
*/
private function can_save_string(?stdClass $customlang, langstring $newstring, stdClass $component): bool {
$result = false;
$message = 'notice_success';
if (empty($customlang)) {
$message = 'notice_inexitentstring';
$this->log[] = new logstatus($message, notification::NOTIFY_ERROR, null, $component->name, $newstring);
return $result;
}
switch ($this->importmode) {
case self::IMPORTNEW:
$result = empty($customlang->local);
$warningmessage = 'notice_ignoreupdate';
break;
case self::IMPORTUPDATE:
$result = !empty($customlang->local);
$warningmessage = 'notice_ignorenew';
break;
case self::IMPORTALL:
$result = true;
break;
}
if ($result) {
$errorlevel = notification::NOTIFY_SUCCESS;
} else {
$errorlevel = notification::NOTIFY_ERROR;
$message = $warningmessage;
}
$this->log[] = new logstatus($message, $errorlevel, null, $component->name, $newstring);
return $result;
}
}

View File

@ -0,0 +1,177 @@
<?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/>.
/**
* Language string based on David Mudrak langstring from local_amos.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\local\mlang;
use moodle_exception;
use stdclass;
/**
* Class containing a lang string cleaned.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Represents a single string
*/
class langstring {
/** @var string identifier */
public $id = null;
/** @var string */
public $text = '';
/** @var int the time stamp when this string was saved */
public $timemodified = null;
/** @var bool is deleted */
public $deleted = false;
/** @var stdclass extra information about the string */
public $extra = null;
/**
* Class constructor.
*
* @param string $id string identifier
* @param string $text string text
* @param int $timemodified
* @param int $deleted
* @param stdclass $extra
*/
public function __construct(string $id, string $text = '', int $timemodified = null,
int $deleted = 0, stdclass $extra = null) {
if (is_null($timemodified)) {
$timemodified = time();
}
$this->id = $id;
$this->text = $text;
$this->timemodified = $timemodified;
$this->deleted = $deleted;
$this->extra = $extra;
}
/**
* Given a string text, returns it being formatted properly for storing in AMOS repository.
*
* Note: This method is taken directly from local_amos as it is highly tested and robust.
* The Moodle 1.x part is keep on puspose to make it easier the copy paste from both codes.
* This could change in the future when AMOS stop suporting the 1.x langstrings.
*
* We need to know for what branch the string should be prepared due to internal changes in
* format required by get_string()
* - for get_string() in Moodle 1.6 - 1.9 use $format == 1
* - for get_string() in Moodle 2.0 and higher use $format == 2
*
* Typical usages of this methods:
* $t = langstring::fix_syntax($t); // sanity new translations of 2.x strings
* $t = langstring::fix_syntax($t, 1); // sanity legacy 1.x strings
* $t = langstring::fix_syntax($t, 2, 1); // convert format of 1.x strings into 2.x
*
* Backward converting 2.x format into 1.x is not supported
*
* @param string $text string text to be fixed
* @param int $format target get_string() format version
* @param int $from which format version does the text come from, defaults to the same as $format
* @return string
*/
public static function fix_syntax(string $text, int $format = 2, ?int $from = null): string {
if (is_null($from)) {
$from = $format;
}
// Common filter.
$clean = trim($text);
$search = [
// Remove \r if it is part of \r\n.
'/\r(?=\n)/',
// Control characters to be replaced with \n
// LINE TABULATION, FORM FEED, CARRIAGE RETURN, END OF TRANSMISSION BLOCK,
// END OF MEDIUM, SUBSTITUTE, BREAK PERMITTED HERE, NEXT LINE, START OF STRING,
// STRING TERMINATOR and Unicode character categorys Zl and Zp.
'/[\x{0B}-\r\x{17}\x{19}\x{1A}\x{82}\x{85}\x{98}\x{9C}\p{Zl}\p{Zp}]/u',
// Control characters to be removed
// NULL, ENQUIRY, ACKNOWLEDGE, BELL, SHIFT {OUT,IN}, DATA LINK ESCAPE,
// DEVICE CONTROL {ONE,TWO,THREE,FOUR}, NEGATIVE ACKNOWLEDGE, SYNCHRONOUS IDLE, ESCAPE,
// DELETE, PADDING CHARACTER, HIGH OCTET PRESET, NO BREAK HERE, INDEX,
// {START,END} OF SELECTED AREA, CHARACTER TABULATION {SET,WITH JUSTIFICATION},
// LINE TABULATION SET, PARTIAL LINE {FORWARD,BACKWARD}, REVERSE LINE FEED,
// SINGLE SHIFT {TWO,THREE}, DEVICE CONTROL STRING, PRIVATE USE {ONE,TWO},
// SET TRANSMIT STATE, MESSAGE WAITING, {START,END} OF GUARDED AREA,
// {SINGLE {GRAPHIC,} CHARACTER,CONTROL SEQUENCE} INTRODUCER, OPERATING SYSTEM COMMAND,
// PRIVACY MESSAGE, APPLICATION PROGRAM COMMAND, ZERO WIDTH {,NO-BREAK} SPACE,
// REPLACEMENT CHARACTER.
'/[\0\x{05}-\x{07}\x{0E}-\x{16}\x{1B}\x{7F}\x{80}\x{81}\x{83}\x{84}\x{86}-\x{93}\x{95}-\x{97}\x{99}-\x{9B}\x{9D}-\x{9F}\x{200B}\x{FEFF}\x{FFFD}]++/u',
// Remove trailing whitespace at the end of lines in a multiline string.
'/[ \t]+(?=\n)/',
];
$replace = [
'',
"\n",
'',
'',
];
$clean = preg_replace($search, $replace, $clean);
if (($format === 2) && ($from === 2)) {
// Sanity translations of 2.x strings.
$clean = preg_replace("/\n{3,}/", "\n\n\n", $clean); // Collapse runs of blank lines.
} else if (($format === 2) && ($from === 1)) {
// Convert 1.x string into 2.x format.
$clean = preg_replace("/\n{3,}/", "\n\n\n", $clean); // Collapse runs of blank lines.
$clean = preg_replace('/%+/', '%', $clean); // Collapse % characters.
$clean = str_replace('\$', '@@@___XXX_ESCAPED_DOLLAR__@@@', $clean); // Remember for later.
$clean = str_replace("\\", '', $clean); // Delete all slashes.
$clean = preg_replace('/(^|[^{])\$a\b(\->[a-zA-Z0-9_]+)?/', '\\1{$a\\2}', $clean); // Wrap placeholders.
$clean = str_replace('@@@___XXX_ESCAPED_DOLLAR__@@@', '$', $clean);
$clean = str_replace('&#36;', '$', $clean);
} else if (($format === 1) && ($from === 1)) {
// Sanity legacy 1.x strings.
$clean = preg_replace("/\n{3,}/", "\n\n", $clean); // Collapse runs of blank lines.
$clean = str_replace('\$', '@@@___XXX_ESCAPED_DOLLAR__@@@', $clean);
$clean = str_replace("\\", '', $clean); // Delete all slashes.
$clean = str_replace('$', '\$', $clean); // Escape all embedded variables.
// Unescape placeholders: only $a and $a->something are allowed. All other $variables are left escaped.
$clean = preg_replace('/\\\\\$a\b(\->[a-zA-Z0-9_]+)?/', '$a\\1', $clean); // Unescape placeholders.
$clean = str_replace('@@@___XXX_ESCAPED_DOLLAR__@@@', '\$', $clean);
$clean = str_replace('"', "\\\"", $clean); // Add slashes for ".
$clean = preg_replace('/%+/', '%', $clean); // Collapse % characters.
$clean = str_replace('%', '%%', $clean); // Duplicate %.
} else {
throw new moodle_exception('Unknown get_string() format version');
}
return $clean;
}
}

View File

@ -0,0 +1,92 @@
<?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/>.
/**
* Language string based on David Mudrak langstring from local_amos.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\local\mlang;
use moodle_exception;
use stdclass;
/**
* Class containing a lang string cleaned.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Represents a single string
*/
class logstatus {
/** @var langstring the current string */
public $langstring = null;
/** @var string the component */
public $component = null;
/** @var string the string ID */
public $stringid = null;
/** @var string the original filename */
public $filename = null;
/** @var int the error level */
public $errorlevel = null;
/** @var string the message identifier */
private $message;
/**
* Class creator.
*
* @param string $message the message identifier to display
* @param string $errorlevel the notice level
* @param string|null $filename the filename of this log
* @param string|null $component the component of this log
* @param langstring|null $langstring the langstring of this log
*/
public function __construct(string $message, string $errorlevel, ?string $filename = null,
?string $component = null, ?langstring $langstring = null) {
$this->filename = $filename;
$this->component = $component;
$this->langstring = $langstring;
$this->message = $message;
$this->errorlevel = $errorlevel;
if ($langstring) {
$this->stringid = $langstring->id;
}
}
/**
* Get the log message.
*
* @return string the log message.
*/
public function get_message(): string {
return get_string($this->message, 'tool_customlang', $this);
}
}

View File

@ -0,0 +1,260 @@
<?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/>.
/**
* Mlang PHP based on David Mudrak phpparser for local_amos.
*
* @package tool_customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\local\mlang;
use coding_exception;
use moodle_exception;
/**
* Parser of Moodle strings defined as associative array.
*
* Moodle core just includes this file format directly as normal PHP code. However
* for security reasons, we must not do this for files uploaded by anonymous users.
* This parser reconstructs the associative $string array without actually including
* the file.
*/
class phpparser {
/** @var holds the singleton instance of self */
private static $instance = null;
/**
* Prevents direct creation of object
*/
private function __construct() {
}
/**
* Prevent from cloning the instance
*/
public function __clone() {
throw new coding_exception('Cloning os singleton is not allowed');
}
/**
* Get the singleton instance fo this class
*
* @return phpparser singleton instance of phpparser
*/
public static function get_instance(): phpparser {
if (is_null(self::$instance)) {
self::$instance = new phpparser();
}
return self::$instance;
}
/**
* Parses the given data in Moodle PHP string format
*
* Note: This method is adapted from local_amos as it is highly tested and robust.
* The priority is keeping it similar to the original one to make it easier to mantain.
*
* @param string $data definition of the associative array
* @param int $format the data format on the input, defaults to the one used since 2.0
* @return langstring[] array of langstrings of this file
*/
public function parse(string $data, int $format = 2): array {
$result = [];
$strings = $this->extract_strings($data);
foreach ($strings as $id => $text) {
$cleaned = clean_param($id, PARAM_STRINGID);
if ($cleaned !== $id) {
continue;
}
$text = langstring::fix_syntax($text, 2, $format);
$result[] = new langstring($id, $text);
}
return $result;
}
/**
* Low level parsing method
*
* Note: This method is adapted from local_amos as it is highly tested and robust.
* The priority is keeping it similar to the original one to make it easier to mantain.
*
* @param string $data
* @return string[] the data strings
*/
protected function extract_strings(string $data): array {
$strings = []; // To be returned.
if (empty($data)) {
return $strings;
}
// Tokenize data - we expect valid PHP code.
$tokens = token_get_all($data);
// Get rid of all non-relevant tokens.
$rubbish = [T_WHITESPACE, T_INLINE_HTML, T_COMMENT, T_DOC_COMMENT, T_OPEN_TAG, T_CLOSE_TAG];
foreach ($tokens as $i => $token) {
if (is_array($token)) {
if (in_array($token[0], $rubbish)) {
unset($tokens[$i]);
}
}
}
$id = null;
$text = null;
$line = 0;
$expect = 'STRING_VAR'; // The first expected token is '$string'.
// Iterate over tokens and look for valid $string array assignment patterns.
foreach ($tokens as $token) {
$foundtype = null;
$founddata = null;
if (is_array($token)) {
$foundtype = $token[0];
$founddata = $token[1];
if (!empty($token[2])) {
$line = $token[2];
}
} else {
$foundtype = 'char';
$founddata = $token;
}
if ($expect == 'STRING_VAR') {
if ($foundtype === T_VARIABLE and $founddata === '$string') {
$expect = 'LEFT_BRACKET';
continue;
} else {
// Allow other code at the global level.
continue;
}
}
if ($expect == 'LEFT_BRACKET') {
if ($foundtype === 'char' and $founddata === '[') {
$expect = 'STRING_ID';
continue;
} else {
throw new moodle_exception('Parsing error. Expected character [ at line '.$line);
}
}
if ($expect == 'STRING_ID') {
if ($foundtype === T_CONSTANT_ENCAPSED_STRING) {
$id = $this->decapsulate($founddata);
$expect = 'RIGHT_BRACKET';
continue;
} else {
throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array key at line '.$line);
}
}
if ($expect == 'RIGHT_BRACKET') {
if ($foundtype === 'char' and $founddata === ']') {
$expect = 'ASSIGNMENT';
continue;
} else {
throw new moodle_exception('Parsing error. Expected character ] at line '.$line);
}
}
if ($expect == 'ASSIGNMENT') {
if ($foundtype === 'char' and $founddata === '=') {
$expect = 'STRING_TEXT';
continue;
} else {
throw new moodle_exception('Parsing error. Expected character = at line '.$line);
}
}
if ($expect == 'STRING_TEXT') {
if ($foundtype === T_CONSTANT_ENCAPSED_STRING) {
$text = $this->decapsulate($founddata);
$expect = 'SEMICOLON';
continue;
} else {
throw new moodle_exception(
'Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array item value at line '.$line
);
}
}
if ($expect == 'SEMICOLON') {
if (is_null($id) or is_null($text)) {
throw new moodle_exception('Parsing error. NULL string id or value at line '.$line);
}
if ($foundtype === 'char' and $founddata === ';') {
if (!empty($id)) {
$strings[$id] = $text;
}
$id = null;
$text = null;
$expect = 'STRING_VAR';
continue;
} else {
throw new moodle_exception('Parsing error. Expected character ; at line '.$line);
}
}
}
return $strings;
}
/**
* Given one T_CONSTANT_ENCAPSED_STRING, return its value without quotes
*
* Also processes escaped quotes inside the text.
*
* Note: This method is taken directly from local_amos as it is highly tested and robust.
*
* @param string $text value obtained by token_get_all()
* @return string value without quotes
*/
protected function decapsulate(string $text): string {
if (strlen($text) < 2) {
throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING in decapsulate()');
}
if (substr($text, 0, 1) == "'" and substr($text, -1) == "'") {
// Single quoted string.
$text = trim($text, "'");
$text = str_replace("\'", "'", $text);
$text = str_replace('\\\\', '\\', $text);
return $text;
} else if (substr($text, 0, 1) == '"' and substr($text, -1) == '"') {
// Double quoted string.
$text = trim($text, '"');
$text = str_replace('\"', '"', $text);
$text = str_replace('\\\\', '\\', $text);
return $text;
} else {
throw new moodle_exception(
'Parsing error. Unexpected quotation in T_CONSTANT_ENCAPSED_STRING in decapsulate(): '.$text
);
}
}
}

View File

@ -0,0 +1,74 @@
<?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/>.
/**
* Import custom lang files.
*
* @package tool_customlang
* @subpackage customlang
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use tool_customlang\form\import;
use tool_customlang\local\importer;
require(__DIR__ . '/../../../config.php');
require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/customlang/locallib.php');
require_once($CFG->libdir.'/adminlib.php');
require_login(SITEID, false);
require_capability('tool/customlang:edit', context_system::instance());
$lng = required_param('lng', PARAM_LANG);
admin_externalpage_setup('toolcustomlang', '', null,
new moodle_url('/admin/tool/customlang/import.php', ['lng' => $lng]));
$output = $PAGE->get_renderer('tool_customlang');
$form = new import(null, ['lng' => $lng]);
if ($data = $form->get_data()) {
require_sesskey();
// Get the file from the users draft area.
$usercontext = context_user::instance($USER->id);
$fs = get_file_storage();
$files = $fs->get_area_files($usercontext->id, 'user', 'draft', $data->pack, 'id',
false);
// Send files to the importer.
$importer = new importer($data->lng, $data->importmode);
$importer->import($files);
echo $output->header();
// Display logs.
$log = $importer->get_log();
foreach ($log as $message) {
echo $output->notification($message->get_message(), $message->errorlevel);
}
// Show continue button.
echo $output->continue_button(new moodle_url('index.php', array('lng' => $lng)));
} else {
echo $output->header();
$form->display();
}
echo $OUTPUT->footer();

View File

@ -132,6 +132,11 @@ if (has_capability('tool/customlang:edit', context_system::instance())) {
'method' => 'post',
);
}
$menu['import'] = array(
'title' => get_string('import', 'tool_customlang'),
'url' => new moodle_url($PAGE->url, ['action' => 'checkout', 'lng' => $lng, 'next' => 'import']),
'method' => 'post',
);
}
if (has_capability('tool/customlang:export', context_system::instance())) {
$langdir = tool_customlang_utils::get_localpack_location($lng);

View File

@ -55,12 +55,24 @@ $string['headingcomponent'] = 'Component';
$string['headinglocal'] = 'Local customisation';
$string['headingstandard'] = 'Standard text';
$string['headingstringid'] = 'String';
$string['import'] = 'Import custom strings';
$string['import_mode'] = 'Import mode';
$string['import_new'] = 'Create only strings without local customisation';
$string['import_update'] = 'Update only strings with local customisation';
$string['import_all'] = 'Create or update all strings from the component(s)';
$string['importfile'] = 'Import file';
$string['langpack'] = 'Language component(s)';
$string['markinguptodate'] = 'Marking the customisation as up-to-date';
$string['markinguptodate_help'] = 'The customised translation may get outdated if either the English original or the master translation has modified since the string was customised on your site. Review the customised translation. If you find it up-to-date, click the checkbox. Edit it otherwise.';
$string['markuptodate'] = 'mark as up-to-date';
$string['modifiedno'] = 'There are no modified strings to save.';
$string['modifiednum'] = 'There are {$a} modified strings. Do you wish to save these changes to your local language pack?';
$string['nolocallang'] = 'No local strings found.';
$string['notice_ignorenew'] = 'Ignoring string {$a->component}/{$a->stringid} because it is not customised.';
$string['notice_ignoreupdate'] = 'Ignoring string {$a->component}/{$a->stringid} because it is already defined.';
$string['notice_inexitentstring'] = 'String {$a->component}/{$a->stringid} not found.';
$string['notice_missingcomponent'] = 'Missing component {$a->component}.';
$string['notice_success'] = 'String {$a->component}/{$a->stringid} updated successfully.';
$string['nostringsfound'] = 'No strings found, please modify the filter settings';
$string['placeholder'] = 'Placeholders';
$string['placeholder_help'] = 'Placeholders are special statements like `{$a}` or `{$a->something}` within the string. They are replaced with a value when the string is actually printed.

View File

@ -0,0 +1,48 @@
@tool @tool_customlang @_file_upload
Feature: Within a moodle instance, an administrator should be able to import modified langstrings.
In order to import modified langstrings in the adminsettings from one to another instance,
As an admin
I need to be able to import the zips and php files of the language customisation of a language.
Background:
Given I log in as "admin"
And I navigate to "Language > Language customisation" in site administration
And I set the field "lng" to "en"
And I click on "Import custom strings" "button"
And I press "Continue"
@javascript
Scenario: Import a PHP file to add a new core lang customization
When I upload "admin/tool/customlang/tests/fixtures/tool_customlang.php" file to "Language component(s)" filemanager
And I press "Import file"
Then I should see "String tool_customlang/pluginname updated successfully."
And I should see "String tool_customlang/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "An amazing import feature" in the "page-header" "region"
@javascript
Scenario: Try to import a PHP file from a non existent component
When I upload "admin/tool/customlang/tests/fixtures/mod_fakecomponent.php" file to "Language component(s)" filemanager
And I press "Import file"
Then I should see "Missing component mod_fakecomponent."
@javascript
Scenario: Import a zip file with some PHP files in it.
When I upload "admin/tool/customlang/tests/fixtures/customlang.zip" file to "Language component(s)" filemanager
And I press "Import file"
Then I should see "String core/administrationsite updated successfully."
And I should see "String core/language updated successfully."
And I should see "String core/nonexistentinvetedstring not found."
And I should see "String tool_customlang/pluginname updated successfully."
And I should see "String tool_customlang/nonexistentinvetedstring not found."
And I should see "Missing component mod_fakecomponent."
And I click on "Continue" "button"
And I should see "There are 3 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"
And I should see "An amazing import feature" in the "page-header" "region"

View File

@ -0,0 +1,81 @@
@tool @tool_customlang @_file_upload
Feature: Within a moodle instance, an administrator should be able to import langstrings with several modes.
In order to import modified langstrings in the adminsettings from one to another instance,
As an admin
I need to be able to import only some language customisation strings depending on some conditions.
Background:
# Add one customization.
Given I log in as "admin"
And I navigate to "Language > Language customisation" in site administration
And I set the field "lng" to "en"
And I press "Open language pack for editing"
And I press "Continue"
And I set the field "Show strings of these components" to "moodle.php"
And I set the field "String identifier" to "administrationsite"
And I press "Show strings"
And I set the field "core/administrationsite" to "Custom string example"
And I press "Save changes to the language pack"
And I should see "There are 1 modified strings."
And I click on "Continue" "button"
And I should see "Custom string example" in the "page-header" "region"
@javascript
Scenario: Update only customized strings
When I set the field "lng" to "en"
And I click on "Import custom strings" "button"
And I press "Continue"
And I upload "admin/tool/customlang/tests/fixtures/moodle.php" file to "Language component(s)" filemanager
And I set the field "Import mode" to "Update only strings with local customisation"
And I press "Import file"
Then I should see "String core/administrationsite updated successfully."
And I should see "Ignoring string core/language because it is not customised."
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should not see "Custom string example" in the "page-header" "region"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should not see "Another Uploaded string" in the "page-header" "region"
@javascript
Scenario: Create only new strings
When I set the field "lng" to "en"
And I click on "Import custom strings" "button"
And I press "Continue"
And I upload "admin/tool/customlang/tests/fixtures/moodle.php" file to "Language component(s)" filemanager
And I set the field "Import mode" to "Create only strings without local customisation"
And I press "Import file"
Then I should see "Ignoring string core/administrationsite because it is already defined."
And I should see "String core/language updated successfully."
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should see "Custom string example" in the "page-header" "region"
And I should not see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"
@javascript
Scenario: Import all strings
When I set the field "lng" to "en"
And I click on "Import custom strings" "button"
And I press "Continue"
And I upload "admin/tool/customlang/tests/fixtures/moodle.php" file to "Language component(s)" filemanager
And I set the field "Import mode" to "Create or update all strings from the component(s)"
And I press "Import file"
Then I should see "String core/administrationsite updated successfully."
And I should see "String core/language updated successfully."
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 2 modified strings."
And I should not see "Uploaded custom string" in the "page-header" "region"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
And I should not see "Custom string example" in the "page-header" "region"
And I should see "Uploaded custom string" in the "page-header" "region"
And I should see "Another Uploaded string" in the "page-header" "region"

Binary file not shown.

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Local language pack.
*
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['administrationsite'] = 'Uploaded custom string';
$string['language'] = 'Another Uploaded string';
$string['nonexistentinvetedstring'] = 'This should not be imported';

View File

@ -0,0 +1,28 @@
<?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/>.
/**
* Local language pack.
*
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['administrationsite'] = 'Uploaded custom string';
$string['language'] = 'Another Uploaded string';
$string['nonexistentinvetedstring'] = 'This should not be imported';

View File

@ -0,0 +1,28 @@
<?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/>.
/**
* Local language pack from http://localhost/m/MDL-69583
*
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'An amazing import feature';
$string['nonexistentinvetedstring'] = 'This string should not be imported';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,207 @@
<?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/>.
/**
* PHP lang parser test.
*
* @package tool_customlang
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_customlang\local\mlang;
use advanced_testcase;
use moodle_exception;
/**
* PHP lang parser test class.
*
* @package tool_customlang
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpparser_testcase extends advanced_testcase {
/**
* Test get instance static method.
*
*/
public function test_get_instance(): void {
$instance = phpparser::get_instance();
$this->assertInstanceOf('tool_customlang\local\mlang\phpparser', $instance);
$this->assertEquals($instance, phpparser::get_instance());
}
/**
* Test get instance parse method.
*
* @dataProvider parse_provider
* @param string $phpcode PHP code to test
* @param array $expected Expected result
* @param bool $exception if an exception is expected
*/
public function test_parse(string $phpcode, array $expected, bool $exception): void {
$instance = phpparser::get_instance();
if ($exception) {
$this->expectException(moodle_exception::class);
}
$strings = $instance->parse($phpcode);
$this->assertEquals(count($expected), count($strings));
foreach ($strings as $key => $langstring) {
$this->assertEquals($expected[$key][0], $langstring->id);
$this->assertEquals($expected[$key][1], $langstring->text);
}
}
/**
* Data provider for the test_parse.
*
* @return array
*/
public function parse_provider() : array {
return [
'Invalid PHP code' => [
'No PHP code', [], false
],
'No PHP open tag' => [
"\$string['example'] = 'text';\n", [], false
],
'One string code' => [
"<?php \$string['example'] = 'text';\n", [['example', 'text']], false
],
'Extra spaces' => [
"<?php \$string['example'] = 'text';\n", [['example', 'text']], false
],
'Extra tabs' => [
"<?php \$string['example']\t=\t'text';\n", [['example', 'text']], false
],
'Double quote string' => [
"<?php
\$string['example'] = \"text\";
\$string[\"example2\"] = 'text2';
\$string[\"example3\"] = \"text3\";
", [
['example', 'text'],
['example2', 'text2'],
['example3', 'text3'],
], false
],
'Multiple lines strings' => [
"<?php
\$string['example'] = 'First line\nsecondline';
\$string['example2'] = \"First line\nsecondline2\";
", [
['example', "First line\nsecondline"],
['example2', "First line\nsecondline2"],
], false
],
'Two strings code' => [
"<?php
\$string['example'] = 'text';
\$string['example2'] = 'text2';
", [
['example', 'text'],
['example2', 'text2'],
], false
],
'Scaped characters' => [
"<?php
\$string['example'] = 'Thos are \\' quotes \" 1';
\$string['example2'] = \"Thos are ' quotes \\\" 2\";
", [
['example', "Thos are ' quotes \" 1"],
['example2', "Thos are ' quotes \" 2"],
], false
],
'PHP with single line comments' => [
"<?php
// This is a comment.
\$string['example'] = 'text';
// This is another commment.
", [
['example', 'text'],
], false
],
'PHP with block comments' => [
"<?php
/* This is a block comment. */
\$string['example'] = 'text';
/* This is another
block comment. */
", [
['example', 'text'],
], false
],
'Wrong variable name' => [
"<?php
\$stringwrong['example'] = 'text';
\$wringstring['example'] = 'text';
", [], false
],
'Single line commented valid line' => [
"<?php
// \$string['example'] = 'text';
", [], false
],
'Block commented valid line' => [
"<?php
/*
\$string['example'] = 'text';
*/
", [], false
],
'Syntax error 1 (double assignation)' => [
"<?php
\$string['example'] = 'text' = 'wrong';
", [], true
],
'Syntax error 2 (no closing string)' => [
"<?php
\$string['example'] = 'wrong;
", [], true
],
'Syntax error 3 (Array without key)' => [
"<?php
\$string[] = 'wrong';
", [], true
],
'Syntax error 4 (Array not open)' => [
"<?php
\$string'example'] = 'wrong';
", [], true
],
'Syntax error 5 (Array not closed)' => [
"<?php
\$string['example' = 'wrong';
", [], true
],
'Syntax error 6 (Missing assignment)' => [
"<?php
\$string['example'] 'wrong';
", [], true
],
];
}
}

View File

@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2021052501;
$plugin->version = 2021052502;
$plugin->requires = 2021052500;
$plugin->component = 'tool_customlang'; // Full name of the plugin (used for diagnostics)