root/trunk/midcom/fi.hut.htmlimport/importer.php

Revision 17354, 22.8 kB (checked in by flack, 1 month ago)

switch to PHP5-style constructors, part 5

Line 
1 <?php
2 if (!class_exists('HTMLPurifier'))
3 {
4     require('HTMLPurifier.php');
5 }
6 /**
7  * Helper for importing directory of HTML files as n.n.static content tree
8  *
9  * @package fi.hut.htmlimport
10  */
11 class fi_hut_htmlimport_importer extends midcom_baseclasses_components_purecode
12 {
13     var $purifier = false;
14     var $purifier2 = false;
15     var $_schemadb = false;
16     var $_schema = false;
17     var $_dm2 = false;
18     var $rulesets = false;
19     var $ruleset = false;
20     var $field_map = false;
21     var $encoding = 'UTF-8';
22
23     /**
24      * Contsructors, loads configurations and tries to select the default
25      * ruleset as active
26      *
27      * @see select_ruleset()
28      */
29     function __construct()
30     {
31         $this->_component = 'fi.hut.htmlimport';
32         parent::__construct();
33
34         $purifier_common_config = array
35         (
36             'Cache' => array
37             (
38                 'SerializerPath' => $GLOBALS['midcom_config']['cache_base_directory'] . 'htmlpurifier',
39             ),
40         );
41
42         if (isset($purifier_common_config['Cache']['SerializerPath'])
43             && !file_exists($purifier_common_config['Cache']['SerializerPath']))
44         {
45             mkdir($purifier_common_config['Cache']['SerializerPath']);
46         }
47
48         $this->purifier = new HTMLPurifier($purifier_common_config);
49         $this->purifier->config->set('HTML', 'EnableAttrID', true);
50         $this->purifier->config->set('HTML', 'Doctype', 'XHTML 1.0 Strict');
51         $this->purifier->config->set('HTML', 'TidyLevel', 'light');
52         $this->purifier->config->set('Core', 'EscapeNonASCIICharacters', true);
53
54         $this->purifier2 = new HTMLPurifier($purifier_common_config);
55         $this->purifier2->config->set('HTML', 'Doctype', 'XHTML 1.0 Strict');
56         $this->purifier2->config->set('HTML', 'TidyLevel', 'heavy');
57         $this->purifier2->config->set('Core', 'EscapeNonASCIICharacters', true);
58
59         $this->rulesets = $this->_config->get('rulesets');
60         $this->select_ruleset($this->_config->get('default_ruleset'));
61     }
62
63     /**
64      * Selects given ruleset as active ruleset
65      *
66      * @param string $ruleset name of ruleset (key of the config rulesets array)
67      * @return bool indicating success/failure
68      */
69     function select_ruleset($ruleset)
70     {
71         if (!isset($this->rulesets[$ruleset]))
72         {
73             return false;
74         }
75         $this->ruleset = $ruleset;
76         $this->field_map = $this->rulesets[$this->ruleset]['field_map'];
77         $this->_load_dm2();
78         return true;
79     }
80
81     /**
82      * Converts given string to $this->encoding, copied from org_openpsa_mail
83      *
84      * @param string to be converted
85      * @param string encoding from header or such, used as default in case mb_detect_endoding is not available
86      * @return string converted string (or original string in case we cannot convert for some reason)
87      */
88     function charset_convert($data, $given_encoding = false)
89     {
90         debug_push_class(__CLASS__, __FUNCTION__);
91         // Some headers are multi-dimensional, recurse if needed
92         if (is_array($data))
93         {
94             debug_add('Given data is an array, iterating trough it');
95             foreach($data as $k => $v)
96             {
97                 debug_add("Recursing key {$k}");
98                 $data[$k] = $this->charset_convert($v, $given_encoding);
99             }
100             debug_add('Done');
101             debug_pop();
102             return $data;
103         }
104         if (empty($data))
105         {
106             debug_add('Data is empty, returning as is'MIDCOM_LOG_WARN);
107             debug_pop();
108             return $data;
109         }
110         if (!function_exists('iconv'))
111         {
112             debug_add('Function \'iconv()\' not available, returning data as is'MIDCOM_LOG_WARN);
113             debug_pop();
114             return $data;
115         }
116         $encoding = false;
117         if (   !function_exists('mb_detect_encoding')
118             && !empty($given_encoding))
119         {
120             $encoding =& $given_encoding;
121         }
122         else
123         {
124             $encoding = mb_detect_encoding($data, $this->_config->get('mb_detect_encoding_list'));
125         }
126         if (empty($encoding))
127         {
128             debug('Given/Detected encoding is empty, cannot convert, aborting', MIDCOM_LOG_WARN);
129             debug_pop();
130             return $data;
131         }
132         $encoding_lower = strtolower($encoding);
133         $this_encoding_lower = strtolower($this->encoding);
134         if (   $encoding_lower == $this_encoding_lower
135             || (   $encoding_lower == 'ascii'
136                 /* ASCII is a subset of the following encodings, and thus requires no conversion to them */
137                 && (   $this_encoding_lower == 'utf-8'
138                     || $this_encoding_lower == 'iso-8859-1'
139                     || $this_encoding_lower == 'iso-8859-15')
140                 )
141             )
142         {
143             debug_add("Given/Detected encoding '{$encoding}' and desired encoding '{$this->encoding}' require no conversion between them", MIDCOM_LOG_INFO);
144             debug_pop();
145             return $data;
146         }
147         $append_target = $this->_config->get('iconv_append_target');
148         debug_add("Calling iconv('{$encoding_lower}', '{$this_encoding_lower}{$append_target}', \$data)");
149         $stat = @iconv($encoding_lower, $this_encoding_lower . $append_target, $data);
150         if (empty($stat))
151         {
152             debug_add("Failed to convert from '{$encoding}' to '{$this->encoding}'", MIDCOM_LOG_WARN);
153             debug_pop();
154             return $data;
155         }
156         debug_add("Converted from '{$encoding}' to '{$this->encoding}'", MIDCOM_LOG_INFO);
157         debug_pop();
158         return $stat;
159     }
160
161     /**
162      * Internal helper to instance DM2 for active configruation
163      * @see select_ruleset()
164      */
165     function _load_dm2()
166     {
167         $this->_load_schemadb();
168         $this->_dm2 = new midcom_helper_datamanager2_datamanager($this->_schemadb);
169         if (!$this->_dm2->set_schema($this->_schema))
170         {
171             /*
172             echo "DEBUG: \$this->_schemadb<pre>\n";
173             ob_start();
174             var_dump($this->_schemadb);
175             $schemadb_r = ob_get_contents();
176             ob_end_clean();
177             echo htmlentities($schemadb_r);
178             unset($schemadb_r);
179             echo "</pre>\n";
180             */
181             $_MIDCOM->generate_error(MIDCOM_ERRCRIT, "\$this->_dm2->set_schema({$this->_schema}) failed");
182         }
183     }
184
185     /**
186      * Internal helper to load DM2 schemadb for active configruation
187      * @see _load_dm2()
188      */
189     function _load_schemadb()
190     {
191         $this->_schemadb = midcom_helper_datamanager2_schema::load_database($this->rulesets[$this->ruleset]['schemadb']);
192         if (!$this->_schemadb)
193         {
194             $_MIDCOM->generate_error(MIDCOM_ERRCRIT, "Failed to load schemadb from {$this->rulesets[$this->ruleset]['schemadb']}");
195         }
196         $this->_schema = $this->rulesets[$this->ruleset]['schema_name'];
197         return true;
198     }
199
200     /**
201      * Parses given path to fi_hut_htmlimport_importer_file object populated according
202      * to the selected ruleset
203      *
204      * @param string $path full path to file
205      * @return object populated fi_hut_htmlimport_importer_file instance, or null on failure
206      */
207     function parse_file($path)
208     {
209         //echo "DEBUG: parsing file {$path}<br>\n";
210         $file = new fi_hut_htmlimport_importer_file();
211         $file->name = preg_replace('/\.html?$/', '', basename($path));
212         $file->schema = $this->_schema;
213
214         $file_data_raw = file_get_contents($path);
215         $file_data_raw = $this->charset_convert($file_data_raw);
216         /*
217         echo "DEBUG: file_data_raw<pre>\n";
218         echo htmlentities($file_data_raw);
219         echo "</pre>\n";
220         */
221         // HTMLpurifier doesn't allow IDs starting with underscore
222         $file_data_purified = preg_replace("%id=(['\"])_(.*?)\\1%", "id=\\1\\2\\1", $file_data_raw);
223         /* I wonder about this, maybe xpath doesn't like spans ??
224         $file_data_purified = str_replace('span', 'div', $file_data_purified);
225         */
226         // Sanitize XHTML here
227         $file_data_purified = $this->purifier->purify($file_data_purified);
228
229         // PONDER: we could skip this if we have no xpath rules
230         $simplexml = @simplexml_load_string($file_data_purified);
231         if (!$simplexml)
232         {
233             echo "WARN: Could not parse file {$path} with simplexml<br>\n";
234             return;
235         }
236
237         // PONDER: some logic for schema selection based on file_data_purified ??
238
239 /*
240         echo "DEBUG: file_data_purified<pre>\n";
241         echo htmlentities($file_data_purified);
242         echo "</pre>\n";
243 */
244
245         $field_set = array();
246         foreach ($this->field_map as $map)
247         {
248             $field_value = '';
249             if (   !isset($map['field'])
250                 || empty($map['field']))
251             {
252                 // No field set at all!
253                 continue;
254             }
255             if (isset($field_set[$map['field']]))
256             {
257                 // Field already has value set by us
258                 continue;
259             }
260             if (   !isset($map['type'])
261                 || empty($map['type']))
262             {
263                 // No type set at all!
264                 continue;
265             }
266             switch ($map['type'])
267             {
268                 /**
269                  * NOTE: When adding new match types remember to update the USAGE.lang.txt file(s)
270                  * in the documentation folder
271                  */
272                 case 'preg_match':
273                     if (   !isset($map['regex'])
274                         || empty($map['regex']))
275                     {
276                         // invalid type config
277                         continue 2;
278                     }
279                     if (!isset($map['matches_key']))
280                     {
281                         $map['matches_key'] = 0;
282                     }
283                     $matches = array();
284                     if (   !preg_match($map['regex'], $file_data_raw, $matches)
285                         || !isset($matches[$map['matches_key']]))
286                     {
287                         $regex_safe = htmlentities($map['regex']);
288                         //echo "DEBUG: no preg_match for {$regex_safe} (field: {$map['field']})<br>\n";
289                         // no valid match
290                         continue 2;
291                     }
292                     /*
293                     $regex_safe = htmlentities($map['regex']);
294                     echo "DEBUG: matches for {$regex_safe}<pre>\n";
295                     ob_start();
296                     var_dump($matches);
297                     $matches_r = ob_get_contents();
298                     ob_end_clean();
299                     echo htmlentities($matches_r);
300                     unset($matches_r);
301                     echo "</pre>\n";
302                     */
303                     $field_value = $matches[$map['matches_key']];
304                     break;
305                 case 'xpath':
306                     if (   !isset($map['path'])
307                         || empty($map['path']))
308                     {
309                         // invalid type config
310                         continue 2;
311                     }
312                     if (!isset($map['matches_key']))
313                     {
314                         $map['matches_key'] = -1;
315                     }
316                     $matches = $simplexml->xpath($map['path']);
317                     if (   empty($matches)
318                         || (   $map['matches_key'] !== -1
319                             && !isset($matches[$map['matches_key']]))
320                         )
321                     {
322                         // No valid match
323                         $path_safe = htmlentities($map['path']);
324                         //echo "DEBUG: no xpath match for {$path_safe} (field: {$map['field']})<br>\n";
325                         continue 2;
326                     }
327                     /*
328                     echo "DEBUG: matches for {$map['path']}<pre>\n";
329                     ob_start();
330                     var_dump($matches);
331                     $matches_r = ob_get_contents();
332                     ob_end_clean();
333                     echo htmlentities($matches_r);
334                     unset($matches_r);
335                     echo "</pre>\n";
336                     */
337                     if ($map['matches_key'] !== -1)
338                     {
339                         // specific key
340                         $field_value = (string)$matches[$map['matches_key']];
341                         break;
342                     }
343                     // all keys
344                     foreach ($matches as $match)
345                     {
346                         /* I have no iade why the original code did something like this, probably to avoid htmlpurifier errors
347                         $match_string = (string)$match;
348                         if (empty($match_string))
349                         {
350                             continue;
351                         }
352                         */
353                         $field_value .= (string)$match->asXml();
354                     }
355                     break;
356                 default:
357                     // type not supported
358                     echo "WARN: mapping type {$map['type']} not supported<br>\n";
359                     continue 2;
360             }
361             $field_value = trim($field_value);
362             // PONDER: allow empty values ??
363             if (empty($field_value))
364             {
365                 echo "WARN: mapped to empty value, skipping<br>\n";
366                 continue;
367             }
368             if (!isset($map['purify']))
369             {
370                 $map['purify'] = true;
371             }
372
373             // In fact this should be unneccessary since DM2 does it for us...
374             if ($map['purify'])
375             {
376                 $field_value = trim($this->purifier2->purify($field_value));
377             }
378
379             // Store value for future saving via DM2
380             $file->field_data[$map['field']] = $field_value;
381             if (property_exists($file, $map['field']))
382             {
383                 // Set any fields that we have also in the file object
384                 $file->$map['field'] = $field_value;
385             }
386
387             $field_set[$map['field']] = true;
388         }
389
390         if (empty($file->title))
391         {
392             $file->title = ucfirst($file->name);
393         }
394
395         return $file;
396     }
397
398     /**
399      * Parses files in given path, returns fi_hut_htmlimport_importer_folder object usable with import_folder
400      *
401      * @param string $path full path to directory to parse
402      * @return object instance of fi_hut_htmlimport_importer_folder or false on critical failure
403      * @see parse_file()
404      * @see import_folder()
405      */
406     function list_files($path)
407     {
408         if (empty($this->field_map))
409         {
410             echo "ERROR: no filed map, have you called select_ruleset yet ?<br>\n";
411             return false;
412         }
413
414         $files = array();
415         $directory = dir($path);
416         
417         $folder = new fi_hut_htmlimport_importer_folder();
418         $folder->name = basename($path);
419         $folder->title = ucfirst(basename($path));
420         
421         $index = false;
422
423         while (false !== ($entry = $directory->read()))
424         {
425             if (substr($entry, 0, 1) == '.')
426             {
427                 // Ignore dotfiles
428                 continue;
429             }
430     
431             if (is_dir("{$path}/{$entry}"))
432             {
433                 // Recurse deeper
434                 $folder->folders[] = $this->list_files("{$path}/{$entry}");
435             }
436             else
437             {
438                 $path_parts = pathinfo($entry);
439
440                 if (preg_match('/^index\.html?$/', $path_parts['basename']))
441                 {
442                     $folder->has_index = true;
443                 }
444                 if (preg_match('/html?$/', $path_parts['extension']))
445                 {
446                     
447                     $file = $this->parse_file("{$path}/{$entry}");
448                     if (!is_null($file))
449                     {
450                         $folder->files[] = $file;
451                     }
452                 }       
453             }
454         }
455         
456         $directory->close();
457         
458         return $folder;
459     }
460
461     /**
462      * Import a given fi_hut_htmlimport_importer_folder object (including
463      * files and subfolders)
464      *
465      * @see list_files()
466      * @see import_file()
467      * @param fi_hut_htmlimport_importer_folder $folder to import
468      * @param int $parent_id id of topic to import to
469      * @return bool indicating success/failure
470      */
471     function import_folder($folder, $parent_id)
472     {
473         //echo "DEBUG: importing folder '{$folder->name}' to parent #{$parent_id}<br>\n";
474         $qb = midcom_db_topic::new_query_builder();
475         $qb->add_constraint('up', '=', (int) $parent_id);
476         $qb->add_constraint('name', '=', $folder->name);
477         $existing = $qb->execute();
478         if (   count($existing) > 0
479             && $existing[0]->up == $parent_id)
480         {
481             $topic = $existing[0];
482             echo "Using existing topic {$topic->name} (#{$topic->id}) from #{$topic->up}<br/>\n";
483         }
484         else
485         {
486             $topic = new midcom_db_topic();
487             $topic->up = $parent_id;
488             $topic->name = $folder->name;
489             if (!$topic->create())
490             {
491                 echo "Failed to create folder {$folder->name}: " . mgd_errstr() . "<br/>\n";
492                 return false;
493             }
494             echo "Created folder {$topic->name} (#{$topic->id}) under #{$topic->up}<br/>\n";
495         }
496
497         $topic->extra = $folder->title;
498         $topic->component = $folder->component;
499         $topic->update();
500
501         if ($folder->component == 'net.nehmer.static')
502         {
503             if (!$folder->has_index)
504             {
505                 $topic->parameter('net.nehmer.static', 'autoindex', 1);
506             }
507             else
508             {
509                 $topic->parameter('net.nehmer.static', 'autoindex', '');
510             }
511         }
512
513         foreach ($folder->files as $file)
514         {
515             if (!$this->import_file($file, $topic->id))
516             {
517                 echo "ERROR: Failed to import file {$file->name} to #{$topic->id}<br>\n";
518                 // PONDER: abort ??
519             }
520         }
521
522         foreach ($folder->folders as $subfolder)
523         {
524             if (!$this->import_folder($subfolder, $topic->id))
525             {
526                 echo