Merge branch 'MDL-69583-master' of git://github.com/ferranrecio/moodle
[moodle.git] / admin / tool / customlang / classes / local / importer.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Custom lang importer.
19  *
20  * @package    tool_customlang
21  * @copyright  2020 Ferran Recio <ferran@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace tool_customlang\local;
27 use tool_customlang\local\mlang\phpparser;
28 use tool_customlang\local\mlang\logstatus;
29 use tool_customlang\local\mlang\langstring;
30 use core\output\notification;
31 use stored_file;
32 use coding_exception;
33 use moodle_exception;
34 use core_component;
35 use stdClass;
37 /**
38  * Class containing tha custom lang importer
39  *
40  * @package    tool_customlang
41  * @copyright  2020 Ferran Recio <ferran@moodle.com>
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class importer {
46     /** @var int imports will only create new customizations */
47     public const IMPORTNEW = 1;
48     /** @var int imports will only update the current customizations */
49     public const IMPORTUPDATE = 2;
50     /** @var int imports all strings */
51     public const IMPORTALL = 3;
53     /**
54      * @var string the language name
55      */
56     protected $lng;
58     /**
59      * @var int the importation mode (new, update, all)
60      */
61     protected $importmode;
63     /**
64      * @var string request folder path
65      */
66     private $folder;
68     /**
69      * @var array import log messages
70      */
71     private $log;
73     /**
74      * Constructor for the importer class.
75      *
76      * @param string $lng the current language to import.
77      * @param int $importmode the import method (IMPORTALL, IMPORTNEW, IMPORTUPDATE).
78      */
79     public function __construct(string $lng, int $importmode = self::IMPORTALL) {
80         $this->lng = $lng;
81         $this->importmode = $importmode;
82         $this->log = [];
83     }
85     /**
86      * Returns the last parse log.
87      *
88      * @return logstatus[] mlang logstatus with the messages
89      */
90     public function get_log(): array {
91         return $this->log;
92     }
94     /**
95      * Import customlang files.
96      *
97      * @param stored_file[] $files array of files to import
98      */
99     public function import(array $files): void {
100         // Create a temporal folder to store the files.
101         $this->folder = make_request_directory(false);
103         $langfiles = $this->deploy_files($files);
105         $this->process_files($langfiles);
106     }
108     /**
109      * Deploy all files into a request folder.
110      *
111      * @param stored_file[] $files array of files to deploy
112      * @return string[] of file paths
113      */
114     private function deploy_files(array $files): array {
115         $result = [];
116         // Desploy all files.
117         foreach ($files as $file) {
118             if ($file->get_mimetype() == 'application/zip') {
119                 $result = array_merge($result, $this->unzip_file($file));
120             } else {
121                 $path = $this->folder.'/'.$file->get_filename();
122                 $file->copy_content_to($path);
123                 $result = array_merge($result, [$path]);
124             }
125         }
126         return $result;
127     }
129     /**
130      * Unzip a file into the request folder.
131      *
132      * @param stored_file $file the zip file to unzip
133      * @return string[] of zip content paths
134      */
135     private function unzip_file(stored_file $file): array {
136         $fp = get_file_packer('application/zip');
137         $zipcontents = $fp->extract_to_pathname($file, $this->folder);
138         if (!$zipcontents) {
139             throw new moodle_exception("Error Unzipping file", 1);
140         }
141         $result = [];
142         foreach ($zipcontents as $contentname => $success) {
143             if ($success) {
144                 $result[] = $this->folder.'/'.$contentname;
145             }
146         }
147         return $result;
148     }
150     /**
151      * Import strings from a list of langfiles.
152      *
153      * @param string[] $langfiles an array with file paths
154      */
155     private function process_files(array $langfiles): void {
156         $parser = phpparser::get_instance();
157         foreach ($langfiles as $filepath) {
158             $component = $this->component_from_filepath($filepath);
159             if ($component) {
160                 $strings = $parser->parse(file_get_contents($filepath));
161                 $this->import_strings($strings, $component);
162             }
163         }
164     }
166     /**
167      * Try to get the component from a filepath.
168      *
169      * @param string $filepath the filepath
170      * @return stdCalss|null the DB record of that component
171      */
172     private function component_from_filepath(string $filepath) {
173         global $DB;
175         // Get component from filename.
176         $pathparts = pathinfo($filepath);
177         if (empty($pathparts['filename'])) {
178             throw new coding_exception("Cannot get filename from $filepath", 1);
179         }
180         $filename = $pathparts['filename'];
182         $normalized = core_component::normalize_component($filename);
183         if (count($normalized) == 1 || empty($normalized[1])) {
184             $componentname = $normalized[0];
185         } else {
186             $componentname = implode('_', $normalized);
187         }
189         $result = $DB->get_record('tool_customlang_components', ['name' => $componentname]);
191         if (!$result) {
192             $this->log[] = new logstatus('notice_missingcomponent', notification::NOTIFY_ERROR, null, $componentname);
193             return null;
194         }
195         return $result;
196     }
198     /**
199      * Import an array of strings into the customlang tables.
200      *
201      * @param langstring[] $strings the langstring to set
202      * @param stdClass $component the target component
203      */
204     private function import_strings(array $strings, stdClass $component): void {
205         global $DB;
207         foreach ($strings as $newstring) {
208             // Check current DB entry.
209             $customlang = $DB->get_record('tool_customlang', [
210                 'componentid' => $component->id,
211                 'stringid' => $newstring->id,
212                 'lang' => $this->lng,
213             ]);
214             if (!$customlang) {
215                 $customlang = null;
216             }
218             if ($this->can_save_string($customlang, $newstring, $component)) {
219                 $customlang->local = $newstring->text;
220                 $customlang->timecustomized = $newstring->timemodified;
221                 $customlang->outdated = 0;
222                 $customlang->modified = 1;
223                 $DB->update_record('tool_customlang', $customlang);
224             }
225         }
226     }
228     /**
229      * Determine if a specific string can be saved based on the current importmode.
230      *
231      * @param stdClass $customlang customlang original record
232      * @param langstring $newstring the new strign to store
233      * @param stdClass $component the component target
234      * @return bool if the string can be stored
235      */
236     private function can_save_string(?stdClass $customlang, langstring $newstring, stdClass $component): bool {
237         $result = false;
238         $message = 'notice_success';
239         if (empty($customlang)) {
240             $message = 'notice_inexitentstring';
241             $this->log[] = new logstatus($message, notification::NOTIFY_ERROR, null, $component->name, $newstring);
242             return $result;
243         }
245         switch ($this->importmode) {
246             case self::IMPORTNEW:
247                 $result = empty($customlang->local);
248                 $warningmessage = 'notice_ignoreupdate';
249                 break;
250             case self::IMPORTUPDATE:
251                 $result = !empty($customlang->local);
252                 $warningmessage = 'notice_ignorenew';
253                 break;
254             case self::IMPORTALL:
255                 $result = true;
256                 break;
257         }
258         if ($result) {
259             $errorlevel = notification::NOTIFY_SUCCESS;
260         } else {
261             $errorlevel = notification::NOTIFY_ERROR;
262             $message = $warningmessage;
263         }
264         $this->log[] = new logstatus($message, $errorlevel, null, $component->name, $newstring);
266         return $result;
267     }