1<?php
2/**
3 * Action Plugin
4 *
5 * @license     GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author      Greg BELLAMY <garlik.crx@gmail.com> [Gag]
7 * @version     0.08beta
8 */
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
12
13class action_plugin_odt2dw extends DokuWiki_Action_Plugin {
14
15  /**
16  * Registers a callback function for a given event
17  */
18  function register(Doku_Event_Handler $controller) {
19    // OdtFile Parser hook
20    $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, '_parser', array());
21    // Display form hook before the wiki page (on top); Maybe create a param to display the form after the page
22    $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, '_render', array());
23    $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton', array());
24  }
25
26  /**
27   * Add 'import odt'-button to pagetools
28   *
29   * @param Doku_Event $event
30   * @param mixed      $param not defined
31   */
32  public function addbutton(&$event, $param) {
33    global $ID, $REV, $conf;
34
35    if($this->getConf('showimportbutton') && $event->data['view'] == 'main') {
36      $params = array('do' => 'odt2dw');
37      if($REV) $params['rev'] = $REV;
38
39      switch($conf['template']) {
40      case 'dokuwiki':
41      case 'arago':
42	$event->data['items']['import_odt'] =
43	  '<li>'
44	  .'<a href='.wl($ID, $params).'  class="action import_odt" rel="nofollow" title="'.$this->getLang('import_odt_button').'">'
45	  .'<span>'.$this->getLang('import_odt_button').'</span>'
46	  .'</a>'
47	  .'</li>';
48	break;
49      }
50    }
51  }
52
53  function _render(&$event, $param) {
54    ### _render : displays the upload form in the pages according to authorized action
55    # INPUT : it's a dokuwiki event function
56    # OUTPUT : void
57    # DISPLAY : upload form
58    global $ID, $lang;
59    // Check if the current action is in the action allow table
60    if ( strpos( $this->getConf('formDisplayRule'), $event->data) === false ) return;
61    // Check if the page exists
62    if ( page_exists( $ID ) && $event->data != "odt2dw" ) return;
63    if ( page_exists( $ID ) ) echo p_render('xhtml',p_get_instructions( $this->getLang( 'formPageExistMessage' ) ), $info );
64    // Check auth user can edit this page
65    if ( auth_quickaclcheck( $ID ) < AUTH_EDIT ) return;
66    // If all check is ok, display the form
67    $message = $this->getConf('formIntroMessage');
68    if ( $message == 'default' ) $message = $this->getLang('formIntroMessage');
69    if ($message) echo p_render('xhtml',p_get_instructions($message),$info);
70    // FIXME create the form with dokuwiki method ?
71    echo '<form method="post" action="" enctype="multipart/form-data">
72<fieldset>
73<legend>'.$this->getLang('formLegend').'</legend>
74<input type="hidden" name="MAX_FILE_SIZE" value="'.$this->getConf('formMaxFileSize').'"/>
75<input type="hidden" name="do" value="odt2dw"/>
76<input type="hidden" name="id" value="'.$ID.'"/>
77<input type="file" name="odtFile"/>
78<input type="submit" value="'.$lang['btn_upload'].'"/>
79</fieldset>
80</form>';
81    if ( $event->data == 'odt2dw' ) $event->preventDefault();
82  }
83
84  function _parser(&$event, $param) {
85    ### _parser : check if an odtFile migth be upload than call the odt2dw converter
86    # INPUT : it's a dokuwiki event function
87    # OUTPUT : void
88
89    // Check action is odt2dw
90    if ( $event->data != 'odt2dw' ) return;
91
92    ###Preparation of the message renderer
93    //Set the debug lvl
94    $this->debug = $this->getConf( 'debugLvl' );
95    //If used, open the logFile
96    if ( $this->debug >= 2 ) {
97      $this->logFile = $this->getConf( 'logFile' );
98      if ( isset( $this->logFile ) ) if ( file_exists( dirname( $this->logFile ) ) || mkdir( dirname( $this->logFile ) ) ) {
99        if ( ! ( $this->logFileHandle = @fopen( $this->logFile, 'a' ) ) ) unset( $this->logFileHandle, $this->logFile );
100      } else unset( $this->logFile );
101      if ( ! isset( $this->logFileHandle ) ) $this->_msg( 'er_logFile' );
102    }
103    ###
104
105    // Check upload file defined
106    $retour = false;
107    if ( $_FILES['odtFile'] ) {
108      // If parse work, change action to defined one in conf/local.php file
109      $retour = $this->_odt2dw();
110      # Delete temp file
111      $this->_purge_env();
112    }
113    //if the file is correctly parsed, change the action to the action defined in the conf
114    //otherwise the action stay odt2dw -> the display form hook will be call by render trigger
115    if ( $retour === true ) {
116      $event->data = $this->getConf('parserPostDisplay');
117    } else {
118      $event->preventDefault();
119    }
120
121    ### Clear the message renderer
122    // Close the log file if used
123    if ( isset( $this->logFileHandle ) ) @fclose( $this->logFileHandle );
124    ###
125  }
126
127  function _odt2dw() {
128    ### _odt2dw : Translate an odt File into dokuwiki syntax
129    # OUTPUT :
130    #   * true -> process successfully
131    #   * false -> something wrong; using _msg to display what's wrong
132
133    global $ID, $conf;
134
135    //Table use to convert urn to url -> without this, xslProc won t parse correctly
136    //Table corrigeant les attributs de la racine du fichier content.xml : urn -> url
137    $this->conversion = array(
138      "xmlns:office" => "http://openoffice.org/2000/office",
139      "xmlns:style" => "http://openoffice.org/2000/style",
140      "xmlns:text" => "http://openoffice.org/2000/text",
141      "xmlns:table" => "http://openoffice.org/2000/table",
142      "xmlns:draw" => "http://openoffice.org/2000/drawing",
143      "xmlns:fo" => "http://www.w3.org/1999/XSL/Format",
144      "xmlns:xlink" => "http://www.w3.org/1999/xlink",
145      "xmlns:dc" => "http://purl.org/dc/elements/1.1/",
146      "xmlns:meta" => "http://openoffice.org/2000/meta",
147      "xmlns:number" => "http://openoffice.org/2000/datastyle",
148      "xmlns:svg" => "http://www.w3.org/2000/svg",
149      "xmlns:chart" => "http://openoffice.org/2000/chart",
150      "xmlns:dr3d" => "http://openoffice.org/2000/dr3d",
151      "xmlns:math" => "http://www.w3.org/1998/Math/MathML",
152      "xmlns:form" => "http://openoffice.org/2000/form",
153      "xmlns:script" => "http://openoffice.org/2000/script",
154      "xmlns:config" => "http://openoffice.org/2001/config",
155      "xmlns:ooo" => "http://openoffice.org/2004/office",
156      "xmlns:ooow" => "http://openoffice.org/2004/writer",
157      "xmlns:oooc" => "http://openoffice.org/2004/calc",
158      "xmlns:dom" => "http://www.w3.org/2001/xml-events",
159      "xmlns:xforms" => "http://www.w3.org/2002/xforms",
160      "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
161      "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
162      "xmlns:rpt" => "http://openoffice.org/2005/report",
163      "xmlns:of" => "urn:oasis:names:tc:opendocument:xmlns:of:1.2",
164      "xmlns:xhtml" => "http://www.w3.org/1999/xhtml",
165      "xmlns:grddl" => "http://www.w3.org/2003/g/data-view#",
166      "xmlns:tableooo" => "http://openoffice.org/2009/table",
167      "xmlns:css3t" => "http://www.w3.org/TR/css3-text/"
168    );
169    // urn wont be/need to convert -- keep for further odtFile version
170    // "xmlns:field" => "urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0",
171    //"xmlns:formx" => "urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0",
172    // CONSTANT : Content file extract from the odt file
173    $this->xmlFile = "content.xml";
174
175    ### Check parameter ###
176
177    // Page receive content
178    if ( ! $this->pageName = $ID ) return $this->_msg('er_id');
179    $this->nsName = getNS($this->pageName);
180    // Check right to change the page
181    if ( page_exists($ID) ) {
182      if ( auth_quickaclcheck($ID) < AUTH_EDIT ) return $this->_msg('er_acl_edit');
183    } else {
184      if ( auth_quickaclcheck($ID) < AUTH_CREATE ) return $this->_msg('er_acl_create');
185    }
186
187    // Check the Odt file uploaded
188    if ( ! $this->_checkUploadFile() ) return $this->_msg('er_checkUploadResult');
189
190    // Check the xslFile
191    if ( ! $this->getConf( 'parserXslFile' ) )  return $this->_msg('er_xslFile_notset');
192    $this->xslFile = DOKU_PLUGIN.'odt2dw/'.$this->getConf('parserXslFile');
193    if ( ! file_exists($this->xslFile) ) return $this->_msg('er_xslFile_exists');
194    if ( ! is_file($this->xslFile) ) return $this->_msg('er_xslFile_isfile');
195
196    // Class Control
197    if ( ! class_exists( XSLTProcessor ) ) return $this->_msg('er_class_xsltProcessor');
198    if ( ! class_exists( ZipArchive ) ) return $this->_msg('er_class_zipArchive');
199    if ( ! class_exists( DOMDocument ) ) return $this->_msg('er_class_domDocument');
200    // Create instance of needed class
201    $this->XSLT = new XSLTProcessor;
202    $this->ZIP  = new ZipArchive;
203    $this->XSL  = new DOMDocument;
204    $this->XML  = new DOMDocument;
205
206    // Load the xslFile
207    if ( ! ($this->XSL->load( $this->xslFile ) ) ) return $this->_msg('er_loadXsl');
208    // Build the xsl processor
209    if ( ! $this->_set_xsltProcessor() ) return $this->_msg('er_xsltProc');
210    // Extract content file from odtFile
211    if ( ! $this->_unzip( $this->xmlFile ) ) return $this->_msg('er_odtFile_unzip');
212    // Load the xmlFile
213    if ( ! $this->XML->load($this->uploadDir.'/'.$this->xmlFile) ) return $this->_msg('er_loadXml');
214    if ( ! $this->racine = $this->XML->getElementsByTagName('document-content')->item(0) ) return $this->_msg('er_invalidRoot');
215    // Correction for urn bug
216    foreach ( $this->conversion as $attr => $value ) if ( $this->racine->hasAttribute($attr) ) $this->racine->setAttributeNS( "http://www.w3.org/2000/xmlns/", $attr, $value );
217    // Transformation du fichier XML
218    $this->result = '====== '.basename($this->odtFileName,'.odt').' ======
219';
220    if ( $this->getConf('parserLinkToOriginalFile') && auth_quickaclcheck($ID) >= AUTH_UPLOAD ) $this->result .= '<sub>{{'.$this->odtFileName.'|'.$this->getLang('parserOriginalFile').'}}</sub>
221
222';
223
224    ### Parameters have been checked successfully ###
225
226
227    // Set specific time out to parse the odtfile into dw syntax
228    set_time_limit( $this->getConf('parserCoreTimeOut') );
229    // Parse the content - This is the CORE
230    if ( ! $tmp = html_entity_decode($this->XSLT->transformToDoc( $this->XML )->saveHTML(), ENT_COMPAT, 'UTF-8') ) return $this->_msg('er_transform');
231    $this->result .= $tmp;
232    // Set the time out to default
233    set_time_limit(30);
234    // Extract and store image files from odtFile to Dokuwiki mediaManager
235    $this->_parse_image();
236
237    // Store the result
238    if ( ! $this->_apply_result() ) return $this->_msg('er_apply');
239
240    return true;
241  }
242
243  function _msg( $message, $type=null, $force=false ) {
244    ### _msg : display message using the debugLvl value
245    # $message : mixed :
246    #   * string : key for $this->getLang() function
247    #   * array :
248    #       $message[0] : string : key for $this->getLang() function
249    #       $message[1] : string : additional information
250    # $type : integer : (check the dokuwiki msg function)
251    #   * -1 : error message
252    #   *  0 : normal message
253    #   *  1 : info message
254    # if type == null, the first 3 char of the key define the message type
255    #   * er_ : -1
256    #   * ok_ :  1
257    #   * otherwise : 0
258    # $force : boolean : force displaying the message without checking debugLvl
259    # OUTPUT :
260    #   * true -> display a normal message
261    #   * false -> display an error message
262    # DISPLAY : call dokuwiki msg function
263    if ( is_array( $message ) ) {
264      $output = $message[0];
265    } else {
266      $output = $message;
267    }
268    // If output is empty, crash with error display;
269    if ( ! $output ) die( $this->getLang( 'er_msg_nomessage' ) );
270    if ( is_null( $type ) ) {
271      $val = substr( $output, 0, strpos( $output, '_' )+1 );
272      switch ($val) {
273        case 'er_' :
274          $err = -1;
275          break;
276        case 'ok_' :
277          $err = 1;
278          break;
279        default :
280          $err = 0;
281      }
282    } else {
283      if ( $type < -1 || $type > 1 ) return false;
284      $err = $type;
285    }
286    // Dev debugging mode; manually set to 4; this dirtily display some informations
287    if ( $this->debug > 3 ) echo '<p>message : '.$message.' |output : '.$output.' |val : '.$val.' |err : '.$err.'</p>';
288
289    // Debug = 0 => No message
290    if ( !$force && $this->debug == 0 ) return ( $err == -1 ? false : true );
291
292    // Debug < 3 => Only error message; If it s not an error message, message return true;
293    if ( !$force && $err != -1 && $this->debug < 3 ) return true;
294    // Otherwise display the message
295    $content = $output.' : '.$this->getLang( $output ).( is_array( $message ) ? ' : '.$message[1] : '' );
296    msg( 'odt2dw : '.$content, $err );
297    if ( isset( $this->logFileHandle ) ) fwrite( $this->logFileHandle, date(DATE_ATOM).':'.$_SERVER['REMOTE_USER'].':'.$content.'
298' );
299    // If error message, return false
300    if ( $err == -1 ) return false;
301    // Otherwise return true;
302    return true;
303  }
304
305
306
307  function _checkUploadFile() {
308    ### _checkUploadFile : group all process about the uploadFile, like uploadStatus, file format, move it in a working directory, etc. ###
309    # OUTPUT :
310    #   * true -> process successfully
311    #   * false -> something wrong; using _msg to display what's wrong
312    // Check a file will be upload
313    if ( ! $_FILES['odtFile'] ) return $this->_msg('er_odtFile_miss');
314    // Check the file status
315    if ( $_FILES['odtFile']['error'] > 0 ) return $this->_msg( array( 'er_odtFile_upload', $_FILES['odtFile']['error'] ) );
316    // Check the file has an authorized mimetype
317    if ( $this->getConf( 'parserMimeTypeAuthorized' ) != "" && strpos( $this->getConf( 'parserMimeTypeAuthorized' ), $_FILES['odtFile']['type'] ) === false ) return $this->_msg( array( 'er_odtFile_format', $_FILES['odtFile']['type'] ) );
318    // Create an unique temp work dir name
319    while ( file_exists( $this->uploadDir = $this->getConf( 'parserUploadDir' ).rand( 10000, 100000 ) ) ) {};
320    // Create the directory
321    if ( ! mkdir( $this->uploadDir, 0777, true ) ) return $this->_msg( 'er_odtFile_tmpDir' );
322    // Move the upload file into the work directory
323    $this->odtFileName = $_FILES['odtFile']['name'];
324    $this->odtFile = $this->uploadDir.'/'.$this->odtFileName;
325    if ( ! move_uploaded_file( $_FILES['odtFile']['tmp_name'], $this->odtFile ) ) return $this->_msg('er_odtFile_getFromDownload');
326    // All upload file checking are OK
327    return true;
328  }
329
330  function _purge_env() {
331    ### _purge_env : clean the system from temporary file ###
332    # OUTPUT :
333    #   void
334    # Display some error message if something wrong in the delete process (might delete the file manually)
335
336    // Perhaps this would not be needed if use temp dir.
337    // No timeOut : the cleanning process wont be interrupted.
338    set_time_limit(0);
339    // use @ to catch the system error message
340    // If exists, delete the download file
341    if ( file_exists( $this->odtFile ) ) if ( ! @unlink( $this->odtFile ) ) $this->_msg( array( 'er_pg_file', $this->odtFile ) );
342    // Delete each file extracted for the uploaded file
343    if ( $this->file_extract ) foreach ($this->file_extract as $file) if ( file_exists( $file ) ) if ( ! @unlink( $file ) ) $this->_msg( array( 'er_pg_file', $file ) );
344    // Delete each image would be rename and not move to the wiki
345    if ( $this->file_import ) foreach ( $this->file_import as $file ) if ( file_exists( $this->uploadDir.'/'.$this->pictpath.'/'.$file ) ) if ( ! @unlink( $this->uploadDir.'/'.$this->pictpath.'/'.$file ) ) $this->_msg( array( 'er_pg_file', $this->uploadDir.'/'.$this->pictpath.'/'.$file ) );
346    // Delete the Pictures directory
347    if ( file_exists( $this->uploadDir.'/'.$this->pictpath) ) if ( ! @rmdir( $this->uploadDir.'/'.$this->pictpath ) ) $this->_msg( array( 'er_pg_dir', $this->uploadDir.'/'.$this->pictpath ) );
348    // Than delete the temporary directory
349    if ( file_exists( $this->uploadDir ) ) if ( ! @rmdir( $this->uploadDir ) ) $this->_msg( array( 'er_pg_dir', $this->uploadDir ) );
350    // Set back default timeOut
351    set_time_limit(30);
352  }
353
354  function _set_xsltProcessor(){
355    ### _set_xsltProcessor : set all xslt param regarding the dokuwiki plugin installed ###
356    # OUTPUT :
357    #   * true -> process successfully
358    #   * false -> something wrong; using _msg to display what's wrong
359    # _msg info report ( debugLvl >= 2 ) display message about active plugin
360
361    // Gag : I think it s a Nasty way to check plugin - must be rewrite but i don t know how
362    $tmp_plugin_lst = plugin_list();
363    if ( ! $this->XSLT->importStylesheet( $this->XSL ) ) return $this->_msg('er_xslt_invalid');
364    foreach ( array('numberedheadings') as $param ) if ( array_search( $param, $tmp_plugin_lst ) !== false ) {
365      if ( ! $this->XSLT->setParameter( '', $param, '1' ) ) return $this->_msg( array( 'inf_xslt_param', $param ), -1 );
366      // _msg info report
367      $this->_msg( array( 'ok_infoPlugin', $param ), 1 );
368    }
369    //
370    foreach ( array('subtable_message') as $lang_elt ) if ( ! $this->XSLT->setParameter( '', $lang_elt, $this->getLang('xsl_'.$lang_elt ) ) ) $this->_msg( array( 'inf_xslt_lang', $param ), 0 );
371    return true;
372  }
373
374  function _apply_result() {
375    ### _apply_result : store the content in dokuwiki page and the attache file (img) in dokuwiki media
376    # OUTPUT :
377    #   * true -> process successfully
378    #   * false -> something wrong; using _msg to display what's wrong
379    global $INFO;
380    // Save the content in data/page
381    saveWikiText( $this->pageName, $this->result, $this->getLang( 'parserSummary' ).$this->odtFileName );
382    if ( ! page_exists($this->pageName) ) return $this->_msg('er_apply_content');
383    // Check if the user could upload file (ACL : permission lvl 8)
384    if ( auth_quickaclcheck($ID) >= AUTH_UPLOAD ) {
385      // Import the image file in the mediaManager (data/media)
386      $destDir = mediaFN( $this->nsName );
387      if ( ! ( file_exists( $destDir ) || mkdir( $destDir, 0777, true ) ) ) return $this->_msg( array( 'er_apply_dirCreate' ) );
388      if ( $this->file_import ) foreach ( $this->file_import as $pict ) {
389        $destFile = mediaFN( $this->nsName.':'.$pict );
390        list( $ext, $mime ) = mimetype( $this->uploadDir.'/'.$this->pictpath.'/'.$pict );
391        if ( media_upload_finish($this->uploadDir.'/'.$this->pictpath.'/'.$pict, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' ) != $this->nsName ) return $this->_msg( array( 'er_apply_img', $this->uploadDir.'/'.$this->pictpath.'/'.$pict ) );
392      }
393      // Keep the original file (import the upload file in the mediaManager)
394      $destFile = mediaFN( $this->nsName.':'.$this->odtFileName );
395      list( $ext, $mime ) = mimetype( $this->uploadDir.'/'.$this->odtFileName );
396      if ( media_upload_finish($this->uploadDir.'/'.$this->odtFileName, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' ) != $this->nsName ) return $this->_msg( array( 'er_apply_odtFile' ) );
397    } else {
398      // If not allowed to upload, display a message.
399      $this->_msg( 'inf_acl_upload', 0, true );
400    }
401    # Refresh info about the current page (see doku.php where $INFO is initiate) - Needed for edit or preview "parserPostDisplay" option
402    $INFO = pageinfo();
403    return true;
404  }
405
406  function _parse_image() {
407    ### _parse_image : search dokuwiki img markup in $this->result than extract the img file and rename it to easier name ###
408    # OUTPUT :
409    #   void
410    # using _msg to display each img file wont be process successfully
411
412    global $ID;
413    $imgs = array();
414    if ( preg_match_all( '|{{((?:[^/}]+/)*[^/}]+)/([0-9a-zA-Z]+)(\.[a-z]+)(\?[0-9]+(?:x[0-9]+)?)?}}|', $this->result, $imgs, PREG_SET_ORDER ) ) {
415      if ( auth_quickaclcheck( $ID ) < AUTH_UPLOAD ) return $this->_msg( 'er_acl_upload' );
416      $this->err['ok'] = array();
417      foreach ( $imgs as $key => $value ) {
418        set_time_limit(20);
419        $this->pictpath = $value[1];
420        $pict = $value[2].$value[3];
421        $ext  = $value[3];
422        $other = $value[4];
423        if ( $this->_unzip($this->pictpath.'/'.$pict) ) {
424          $newname = noNS($this->pageName).'_Image_'.$key.$ext;
425          if ( rename( $this->uploadDir.'/'.$this->pictpath.'/'.$pict, $this->uploadDir.'/'.$this->pictpath.'/'.$newname ) ) {
426            $this->result = str_replace( '{{'.$this->pictpath.'/'.$pict.$other.'}}' , '{{'.$newname.$other.'}}' , $this->result );
427            $this->file_import[] = $newname;
428            if ( $this->debug ) $this->err['ok'][] = $pict.' : '.$newname;
429          } else $this->err[$pict] = 'rename';
430        } else $this->err[$pict] = 'unzip';
431      }
432    }
433    if ( $this->err ) foreach ( $this->err as $key => $value ) {
434      switch ( $key ) {
435        case 'ok':
436          foreach ( $value as $msg ) $this->_msg( array( 'ok_img', $msg ) );
437          break;
438        default :
439          // $value E ( rename, unzip) => er_img_rename, er_img_unzip
440          $this->_msg( array( 'er_img_'.$value, $key ) );
441      }
442    }
443  }
444
445  function _unzip( $entrie ) {
446    ### _unzip : extract $entrie file from $this->odtFile to $this->uploadDir using $this->ZIP object instance of ZipArchive Class ###
447    # $entrie : string : fullFileName (with the internal path in the archive)
448    # OUTPUT :
449    #   * true -> extraction ok
450    #   * false -> something wrong; using _msg to display what's wrong
451
452    if ( ! $this->ZIP ) return $this->_msg('er_unzip_object');
453    if ( ! file_exists( $this->odtFile ) ) return $this->_msg('er_unzip_nofile');
454    if ( ! ( $this->ZIP->open( $this->odtFile ) === true ) ) return $this->_msg( 'er_unzip_open' );
455    $res = $this->ZIP->extractTo( $this->uploadDir, $entrie );
456    $this->ZIP->close();
457    if ( ! $res ) return $this->_msg( array( 'er_unzip_error', $entrie ) );
458    $this->file_extract[] = $this->uploadDir.'/'.$entrie;
459    return $this->_msg( array( 'ok_unzip', $entrie ) );
460  }
461
462}
463