Commit f720b77a authored by jun's avatar jun
Browse files

Merge branch 'MDL-69583-master' of git://github.com/ferranrecio/moodle

parents 8252b24f fed691aa
<?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'));
}
}
<?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;
}
}
<?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;
}
}
<?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);