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/>.
16
17/**
18 * @package    backup-convert
19 * @subpackage cc-library
20 * @copyright  2011 Darko Miletic <dmiletic@moodlerooms.com>
21 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23
24require_once('xmlbase.php');
25
26/**
27 *
28 * Various helper utils
29 * @author Darko Miletic dmiletic@moodlerooms.com
30 *
31 */
32abstract class cc_helpers {
33
34    /**
35     * Checks extension of the supplied filename
36     *
37     * @param string $filename
38     */
39    public static function is_html($filename) {
40        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
41        return in_array($extension, array('htm', 'html'));
42    }
43
44    /**
45     * Generates unique identifier
46     * @param string $prefix
47     * @param string $suffix
48     * @return string
49     */
50    public static function uuidgen($prefix = '', $suffix = '', $uppercase = true) {
51        $uuid = trim(sprintf('%s%04x%04x%s', $prefix, mt_rand(0, 65535), mt_rand(0, 65535), $suffix));
52        $result = $uppercase ? strtoupper($uuid) : strtolower($uuid);
53        return $result;
54    }
55
56    /**
57     * Creates new folder with random name
58     * @param string $where
59     * @param string $prefix
60     * @param string $suffix
61     * @return mixed - directory short name or false in case of failure
62     */
63    public static function randomdir($where, $prefix = '', $suffix = '') {
64        global $CFG;
65
66        $dirname    = false;
67        $randomname = self::uuidgen($prefix, $suffix, false);
68        $newdirname = $where.DIRECTORY_SEPARATOR.$randomname;
69        if (mkdir($newdirname)) {
70            chmod($newdirname, $CFG->directorypermissions);
71            $dirname = $randomname;
72        }
73        return $dirname;
74    }
75
76    public static function build_query($attributes, $search) {
77        $result = '';
78        foreach ($attributes as $attribute) {
79            if ($result != '') {
80                $result .= ' | ';
81            }
82            $result .= "//*[starts-with(@{$attribute},'{$search}')]/@{$attribute}";
83        }
84        return $result;
85    }
86
87    public static function process_embedded_files(&$doc, $attributes, $search, $customslash = null) {
88        $result = array();
89        $query = self::build_query($attributes, $search);
90        $list = $doc->nodeList($query);
91        foreach ($list as $filelink) {
92            $rvalue = str_replace($search, '', $filelink->nodeValue);
93            if (!empty($customslash)) {
94                $rvalue = str_replace($customslash, '/', $rvalue);
95            }
96            $result[] = rawurldecode($rvalue);
97        }
98        return $result;
99    }
100
101    /**
102     *
103     * Get list of embedded files
104     * @param string $html
105     * @return multitype:mixed
106     */
107    public static function embedded_files($html) {
108        $result = array();
109        $doc = new XMLGenericDocument();
110        $doc->doc->validateOnParse = false;
111        $doc->doc->strictErrorChecking = false;
112        if (!empty($html) && $doc->loadHTML($html)) {
113            $attributes = array('src', 'href');
114            $result1 = self::process_embedded_files($doc, $attributes, '@@PLUGINFILE@@');
115            $result2 = self::process_embedded_files($doc, $attributes, '$@FILEPHP@$', '$@SLASH@$');
116            $result = array_merge($result1, $result2);
117        }
118        return $result;
119    }
120
121    public static function embedded_mapping($packageroot, $contextid = null) {
122        $main_file = $packageroot . DIRECTORY_SEPARATOR . 'files.xml';
123        $mfile = new XMLGenericDocument();
124        if (!$mfile->load($main_file)) {
125            return false;
126        }
127        $query = "/files/file[filename!='.']";
128        if (!empty($contextid)) {
129            $query .= "[contextid='{$contextid}']";
130        }
131        $files = $mfile->nodeList($query);
132        $depfiles = array();
133        foreach ($files as $node) {
134            $mainfile   = intval($mfile->nodeValue('sortorder', $node));
135            $filename   = $mfile->nodeValue('filename', $node);
136            $filepath   = $mfile->nodeValue('filepath', $node);
137            $source     = $mfile->nodeValue('source', $node);
138            $author     = $mfile->nodeValue('author', $node);
139            $license    = $mfile->nodeValue('license', $node);
140            $hashedname = $mfile->nodeValue('contenthash', $node);
141            $hashpart   = substr($hashedname, 0, 2);
142            $location   = 'files'.DIRECTORY_SEPARATOR.$hashpart.DIRECTORY_SEPARATOR.$hashedname;
143            $type       = $mfile->nodeValue('mimetype', $node);
144            $depfiles[$filepath.$filename] = array( $location,
145                                                    ($mainfile == 1),
146                                                    strtolower(str_replace(' ', '_', $filename)),
147                                                    $type,
148                                                    $source,
149                                                    $author,
150                                                    $license,
151                                                    strtolower(str_replace(' ', '_', $filepath)));
152        }
153
154        return $depfiles;
155    }
156
157    public static function add_files(cc_i_manifest &$manifest, $packageroot, $outdir, $allinone = true) {
158        global $CFG;
159
160        if (pkg_static_resources::instance()->finished) {
161            return;
162        }
163        $files = cc_helpers::embedded_mapping($packageroot);
164        $rdir = $allinone ? new cc_resource_location($outdir) : null;
165        foreach ($files as $virtual => $values) {
166            $clean_filename = $values[2];
167            if (!$allinone) {
168                $rdir = new cc_resource_location($outdir);
169            }
170            $rtp = $rdir->fullpath().$values[7].$clean_filename;
171            //Are there any relative virtual directories?
172            //let us try to recreate them
173            $justdir = $rdir->fullpath(false).$values[7];
174            if (!file_exists($justdir)) {
175                if (!mkdir($justdir, $CFG->directorypermissions, true)) {
176                    throw new RuntimeException('Unable to create directories!');
177                }
178            }
179
180            $source = $packageroot.DIRECTORY_SEPARATOR.$values[0];
181            if (!copy($source, $rtp)) {
182                throw new RuntimeException('Unable to copy files!');
183            }
184            $resource = new cc_resource($rdir->rootdir(),
185                                        $values[7].$clean_filename,
186                                        $rdir->dirname(false));
187            $res = $manifest->add_resource($resource, null, cc_version11::webcontent);
188            pkg_static_resources::instance()->add($virtual,
189                                                  $res[0],
190                                                  $rdir->dirname(false).$values[7].$clean_filename,
191                                                  $values[1],
192                                                  $resource);
193        }
194
195        pkg_static_resources::instance()->finished = true;
196    }
197
198    /**
199     *
200     * Excerpt from IMS CC 1.1 overview :
201     * No spaces in filenames, directory and file references should
202     * employ all lowercase or all uppercase - no mixed case
203     *
204     * @param cc_i_manifest $manifest
205     * @param string $packageroot
206     * @param integer $contextid
207     * @param string $outdir
208     * @param boolean $allinone
209     * @throws RuntimeException
210     */
211    public static function handle_static_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) {
212        self::add_files($manifest, $packageroot, $outdir, $allinone);
213        return pkg_static_resources::instance()->get_values();
214    }
215
216    public static function handle_resource_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) {
217        $result = array();
218        self::add_files($manifest, $packageroot, $outdir, $allinone);
219        $files = self::embedded_mapping($packageroot, $contextid);
220        $rootnode = null;
221        $rootvals = null;
222        $depfiles = array();
223        $depres = array();
224        $flocation = null;
225        foreach ($files as $virtual => $values) {
226            $vals = pkg_static_resources::instance()->get_identifier($virtual);
227            $resource = $vals[3];
228            $identifier = $resource->identifier;
229            $flocation = $vals[1];
230            if ($values[1]) {
231                $rootnode = $resource;
232                $rootvals = $flocation;
233                continue;
234            }
235
236            $depres[] = $identifier;
237            $depfiles[] = $vals[1];
238            $result[$virtual] = array($identifier, $flocation, false);
239        }
240
241        if (!empty($rootnode)) {
242            $rootnode->files = array_merge($rootnode->files, $depfiles);
243            $result[$virtual] = array($rootnode->identifier, $rootvals, true);
244        }
245
246        return $result;
247    }
248
249    public static function process_linked_files($content, cc_i_manifest &$manifest, $packageroot,
250                                                $contextid, $outdir, $webcontent = false) {
251        // Detect all embedded files
252        // locate their physical counterparts in moodle 2 backup
253        // copy all files in the cc package stripping any spaces and using only lowercase letters
254        // add those files as resources of the type webcontent to the manifest
255        // replace the links to the resource using $IMS-CC-FILEBASE$ and their new locations
256        // cc_resource has array of files and array of dependencies
257        // most likely we would need to add all files as independent resources and than
258        // attach them all as dependencies to the forum tag.
259        $lfiles = self::embedded_files($content);
260        $text = $content;
261        $deps = array();
262        if (!empty($lfiles)) {
263            $files = self::handle_static_content($manifest,
264                                                 $packageroot,
265                                                 $contextid,
266                                                 $outdir);
267            $replaceprefix = $webcontent ? '' : '$IMS-CC-FILEBASE$';
268            foreach ($lfiles as $lfile) {
269                if (isset($files[$lfile])) {
270                    $filename = str_replace('%2F', '/', rawurlencode($lfile));
271                    $content = str_replace('@@PLUGINFILE@@'.$filename,
272                                           $replaceprefix.'../'.$files[$lfile][1],
273                                           $content);
274                    // For the legacy stuff.
275                    $content = str_replace('$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $filename),
276                                           $replaceprefix.'../'.$files[$lfile][1],
277                                           $content);
278                    $deps[] = $files[$lfile][0];
279                }
280            }
281            $text = $content;
282        }
283        return array($text, $deps);
284    }
285
286    public static function relative_location($originpath, $linkingpath) {
287        return false;
288    }
289
290}
291
292
293final class cc_resource_location {
294    /**
295     *
296     * Root directory
297     * @var string
298     */
299    private $rootdir = null;
300    /**
301     *
302     * new directory
303     * @var string
304     */
305    private $dir = null;
306    /**
307     *
308     * Full precalculated path
309     * @var string
310     */
311    private $fullpath = null;
312
313    /**
314     *
315     * ctor
316     * @param string $rootdir - path to the containing directory
317     * @throws InvalidArgumentException
318     * @throws RuntimeException
319     */
320    public function __construct($rootdir) {
321        $rdir = realpath($rootdir);
322        if (empty($rdir)) {
323            throw new InvalidArgumentException('Invalid path!');
324        }
325        $dir = cc_helpers::randomdir($rdir, 'i_');
326        if ($dir === false) {
327            throw new RuntimeException('Unable to create directory!');
328        }
329        $this->rootdir  = $rdir;
330        $this->dir      = $dir;
331        $this->fullpath = $rdir.DIRECTORY_SEPARATOR.$dir;
332    }
333
334    /**
335     *
336     * Newly created directory
337     * @return string
338     */
339    public function dirname($endseparator=false) {
340        return $this->dir.($endseparator ? '/' : '');
341    }
342
343    /**
344     *
345     * Full path to the new directory
346     * @return string
347     */
348    public function fullpath($endseparator=false) {
349        return $this->fullpath.($endseparator ? DIRECTORY_SEPARATOR : '');
350    }
351
352    /**
353     * Returns containing dir
354     * @return string
355     */
356    public function rootdir($endseparator=false) {
357        return $this->rootdir.($endseparator ? DIRECTORY_SEPARATOR : '');
358    }
359}
360
361class pkg_static_resources {
362
363    /**
364     * @var array
365     */
366    private $values = array();
367
368    /**
369     * @var boolean
370     */
371    public $finished = false;
372
373    /**
374     * @var pkg_static_resources
375     */
376    private static $instance = null;
377
378    private function __clone() {
379    }
380
381    private function __construct() {
382    }
383
384    /**
385     * @return pkg_static_resources
386     */
387    public static function instance() {
388        if (empty(self::$instance)) {
389            $c = __CLASS__;
390            self::$instance = new $c();
391        }
392        return self::$instance;
393    }
394
395    /**
396     *
397     * add new element
398     * @param string $identifier
399     * @param string $file
400     * @param boolean $main
401     */
402    public function add($key, $identifier, $file, $main, $node = null) {
403        $this->values[$key] = array($identifier, $file, $main, $node);
404    }
405
406    /**
407     * @return array
408     */
409    public function get_values() {
410        return $this->values;
411    }
412
413    public function get_identifier($location) {
414        return isset($this->values[$location]) ? $this->values[$location] : false;
415    }
416
417    public function reset() {
418        $this->values   = array();
419        $this->finished = false;
420    }
421}
422
423
424class pkg_resource_dependencies {
425    /**
426     * @var array
427     */
428    private $values = array();
429
430    /**
431     * @var pkg_resource_dependencies
432     */
433    private static $instance = null;
434
435    private function __clone() {
436    }
437    private function __construct() {
438    }
439
440    /**
441     * @return pkg_resource_dependencies
442     */
443    public static function instance() {
444        if (empty(self::$instance)) {
445            $c = __CLASS__;
446            self::$instance = new $c();
447        }
448        return self::$instance;
449    }
450
451    /**
452     * @param array $deps
453     */
454    public function add(array $deps) {
455        $this->values = array_merge($this->values, $deps);
456    }
457
458    public function reset() {
459        $this->values = array();
460    }
461
462    /**
463     * @return array
464     */
465    public function get_deps() {
466        return $this->values;
467    }
468}
469