1<?php 2/** 3 * Action Plugin 4 * 5 * @license MIT License (https://opensource.org/licenses/MIT) 6 * @author José Torrecilla <qky669@gmail.com> 7 * @version 0.1beta 8 */ 9 10// must be run within Dokuwiki 11if(!defined('DOKU_INC')) die(); 12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 13 14class action_plugin_file2dw extends DokuWiki_Action_Plugin { 15 16 /** 17 * Registers a callback function for a given event 18 */ 19 function register(Doku_Event_Handler $controller) { 20 21 // File parser hook 22 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, '_parser', array()); 23 24 // Display form hook before the wiki page (on top) 25 $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, '_render', array()); 26 27 //Add MENU_ITEMS_ASSEMBLY 28 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, '_addsvgbutton', array()); 29 } 30 31 /** 32 * Add 'import'-button to menu 33 * 34 * @param Doku_Event $event 35 * @param mixed $param not defined 36 */ 37 function _addsvgbutton(&$event, $param) { 38 if($event->data['view'] == 'page') { 39 array_push($event->data['items'],new \dokuwiki\plugin\file2dw\MenuItem()); 40 } 41 } 42 43 /** 44 * Displays the upload form in the pages according to authorized action 45 * 46 * @param Doku_Event $event It's a dokuwiki event function 47 * @param mixed $param Not defined 48 */ 49 function _render(&$event, $param) { 50 // $ID: Page identifier 51 global $ID; 52 53 // Check if should display the form 54 if ( strpos( $this->getConf('formDisplayRule'), $event->data) === false ) return; 55 56 // If the page exists but $event->data != "file2dw", return 57 if ( page_exists( $ID ) && $event->data != "file2dw" ) return; 58 59 // Check auth user can edit this page 60 if ( auth_quickaclcheck( $ID ) < AUTH_EDIT ) return; 61 62 // If page exists, show warning to the user 63 if ( page_exists( $ID ) ) echo p_render('xhtml',p_get_instructions( $this->getLang( 'formPageExistMessage' ) ), $info ); 64 65 // Show form 66 echo $this->_createForm(); 67 68 if ( $event->data == 'file2dw' ) $event->preventDefault(); 69 70 } 71 72 /** 73 * Creates an returns a string with the HTML upload form to show to the user 74 * 75 * @return string HTML upload form 76 */ 77 function _createForm() { 78 79 global $ID; 80 81 $form = new dokuwiki\Form\Form(array('id' => 'file2dw_form', 'enctype' => 'multipart/form-data')); 82 83 // Intro message 84 $message = $this->getConf('formIntroMessage'); 85 if ( $message == 'default' ) $message = $this->getLang('formIntroMessage'); 86 if ( $message ) { 87 $message = p_render('xhtml',p_get_instructions($message),$info); 88 $form->addHTML($message); 89 } 90 91 //Open fieldset 92 $form->addFieldsetOpen(); 93 94 //legend tag 95 $legend = $form->addTag('legend'); 96 $legend->attr('value',$this->getLang('formLegend')); 97 98 //hidden 99 $form->setHiddenField('MAX_FILE_SIZE',$this->getConf('formMaxFileSize')); 100 $form->setHiddenField('do','file2dw'); 101 $form->setHiddenField('id',$ID); 102 103 //userFile file input 104 $userFileInputElement = new dokuwiki\Form\InputElement('file','userFile'); 105 $form->addElement($userFileInputElement); 106 107 // submit 108 $submitInputElement = new dokuwiki\Form\InputElement('submit','btn_upload'); 109 $submitInputElement->attr('value',$this->getLang('import_button')); 110 $form->addElement($submitInputElement); 111 112 //Close fieldset 113 $form->addFieldsetClose(); 114 115 return $form->toHTML(); 116 } 117 118 119 /** 120 * Checks if the file might be uploaded, then call the file2dw converter 121 * 122 * @param Doku_Event $event It's a dokuwiki event function 123 * @param mixed $param Not defined 124 */ 125 function _parser(&$event, $param) { 126 127 // Check action is file2dw 128 if ( $event->data != 'file2dw' ) return; 129 130 // Preparation of the message renderer 131 // Set the debug lvl 132 $this->logLevel = $this->getConf( 'logLevel' ); 133 $this->debugShowInfo = $this->getConf( 'debugShowInfo' ); 134 //If used, open the logFile 135 if ( $this->logLevel > 0 ) { 136 $this->logFile = $this->getConf( 'logFile' ); 137 if ( isset( $this->logFile ) ) { 138 if ( file_exists( dirname( $this->logFile ) ) || mkdir( dirname( $this->logFile ) ) ) { 139 if ( ! ( $this->logFileHandle = @fopen( $this->logFile, 'a' ) ) ) { 140 unset( $this->logFileHandle, $this->logFile ); 141 } 142 } else { 143 unset( $this->logFile ); 144 } 145 } 146 if ( ! isset( $this->logFileHandle ) ) { 147 $this->_msg('er_logFile'); 148 } 149 } 150 151 // Check upload file defined 152 $retorno = false; 153 if ( $_FILES['userFile'] && $_FILES['userFile']['error'] == 0 ) { 154 $this->_msg( array('ok_info','userFile found: '.$_FILES['userFile']['name']) ); 155 // If parse work, change action to defined one in conf/local.php file 156 $retorno = $this->_file2dw(); 157 // Delete temp folder 158 $this->_purge_env(); 159 } 160 161 // if the file is correctly parsed, change the action to "show" 162 // otherwise the action stay file2dw 163 if ( $retorno === true ) { 164 $event->data = 'show'; 165 } else { 166 $event->preventDefault(); 167 } 168 169 // Clear the message renderer 170 // Close the log file if used 171 if ( isset( $this->logFileHandle ) ) { 172 @fclose( $this->logFileHandle ); 173 } 174 } 175 176 177 /** 178 * Converts uploaded file to Dokuwiki syntax 179 * 180 * @return bool true if conversion ended ok; false if conversion failed 181 */ 182 function _file2dw() { 183 184 global $ID; 185 186 ### Check parameter ### 187 188 // Page receive content 189 if ( ! $this->pageName = $ID ) return $this->_msg('er_id'); 190 $this->nsName = getNS($this->pageName); 191 // Check rights to change the page 192 if ( page_exists($ID) ) { 193 if ( auth_quickaclcheck($ID) < AUTH_EDIT ) return $this->_msg('er_acl_edit'); 194 } else { 195 if ( auth_quickaclcheck($ID) < AUTH_CREATE ) return $this->_msg('er_acl_create'); 196 } 197 198 // Check uploaded file 199 $this->_checkUploadedFile(); 200 201 // Need OpenOffice conversion? 202 // workFile is the file that will be converted by pandoc to dokuwiki syntax 203 // workFile is the userFile by default 204 // It will be changed if OpenOffice conversion is needed (example: .doc files) 205 $this->workFileName = substr($this->userFileName,0); 206 $this->workFile = substr($this->userFile,0); 207 if ($this->getConf( 'parserMimeTypeSOffice' ) != '' 208 && strpos( $this->getConf( 'parserMimeTypeSOffice' ), $_FILES['userFile']['type'] ) !== false) { 209 if ( !$this->_OOConversion() ) return false; 210 } 211 212 // pandoc conversion 213 // Resulting file name: dwpage 214 // Images folder: img 215 $this->dwpageFileName = 'dwpage'; 216 $this->dwpageFile = $this->workDir.'/'.$this->dwpageFileName; 217 $this->dwimgDir = $this->workDir.'/img'; 218 $output = array(); 219 $command = 'pandoc -s -w dokuwiki --extract-media="'.$this->dwimgDir; 220 $command .= '" -o "'.$this->dwpageFile.'" "'.$this->workFile.'"'; 221 exec( $command, $output, $return_var ); 222 223 $this->_msg(array('ok_info','Executed command: '.$command)); 224 225 if ( !file_exists($this->dwpageFile) ) { 226 $message = '<br>Missing file: ' . $this->dwpageFile; 227 $message .= '<br>Command: ' . $command; 228 $message .= '<br>Output: '. print_r($output,true); 229 $message .= '<br>Return: '. $return_var; 230 return $this->_msg( array('er_pandoc',$message) ); 231 } 232 233 $this->_msg(array('ok_info','pandoc conversion done')); 234 235 236 // Initial result 237 $this->result = '====== '.basename($this->userFileName).' ====== 238'; 239 if ( $this->getConf('parserLinkToOriginalFile') && auth_quickaclcheck($ID) >= AUTH_UPLOAD ) { 240 $this->result .= '<sub>{{'.$this->userFileName.'|'.$this->getLang('parserOriginalFile').'}}</sub> 241 242'; 243 } 244 245 $this->result .= file_get_contents ($this->dwpageFile); 246 247 // If dwimgDir does not exist, we do not need to porcess it 248 if (is_dir($this->dwimgDir)) { 249 $this->_msg(array('ok_info','Start processing dir '.$this->dwimgDir)); 250 // Use $this->now to put a timestamp in images name 251 $this->now = date('Y-m-d_H-i-s'); 252 // Use $this->importedImages to count (and store, if we need to delete them after an error) 253 $this->importedImages = array(); 254 if ( !$this->_processImgDir($this->dwimgDir) ) { 255 //Delete all imported images until error from dokuwiki 256 foreach ($this->importedImages as $imgId) { 257 media_delete($imgId, null); 258 } 259 // Return error 260 return $this->_msg('er_img_dir'); 261 } 262 } 263 264 $this->_msg(array('ok_info','Resultado: '.$this->result)); 265 266 // Keep the original file (import the upload file in the mediaManager) 267 if ( auth_quickaclcheck($ID) >= AUTH_UPLOAD ) { 268 $destFile = mediaFN( $this->nsName.':'.$this->userFileName ); 269 list( $ext, $mime ) = mimetype($this->userFile); 270 if ( media_upload_finish($this->userFile, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' ) != $this->nsName ) { 271 return $this->_msg( array( 'er_apply_file' ) ); 272 } 273 } else { 274 // If not allowed to upload, return error. 275 return $this->_msg('er_acl_upload'); 276 } 277 278 // Save wiki page 279 saveWikiText( $this->pageName, $this->result, $this->getLang( 'parserSummary' ).$this->userFileName ); 280 if ( ! page_exists($this->pageName) ) return $this->_msg('er_apply_content'); 281 282 return true; 283 } 284 285 286 /** 287 * Add images in a directory (and its subdirectories) to Dokuwiki mediaManager. 288 * Also updates $this->result (it will be wiki page content). 289 * 290 * @param string $imgDir Full path directory to process 291 * @return bool true if process ended ok; false if failed 292 */ 293 function _processImgDir($imgDir) { 294 295 // In $imgDir is not a directory, return error 296 if (!is_dir($imgDir)) return $this->_msg(array('er_img_dir',$imgDir.' is not a directory')); 297 298 // list and process directory items 299 $items = array_diff(scandir($imgDir), array('.','..')); 300 foreach ($items as $item) { 301 $itemPath = "$imgDir/$item"; 302 if (is_dir($itemPath)) { 303 if (!$this->_processImgDir($itemPath)) { 304 return $this->_msg(array ('er_img_dir','Error processing directory '.$itemPath) ); 305 } 306 } else { 307 if (!$this->_processImg($itemPath)) { 308 return $this->_msg(array('er_img_dir','Error processing image '.$itemPath)); 309 } 310 } 311 } 312 313 $this->_msg(array('ok_info','Processed image directory: '.$imgDir)); 314 315 return true; 316 } 317 318 /** 319 * Add single image to Dokuwiki mediaManager. 320 * Also updates $this->result (it will be wiki page content). 321 * 322 * @param string $imgPath Full path image to process 323 * @return bool true if process ended ok; false if failed 324 */ 325 function _processImg($imgPath) { 326 327 list( $ext, $mime ) = mimetype( $imgPath ); 328 329 // Sanitize original file name 330 $userFileBasename = basename($this->userFileName); 331 $userFileBasename = mb_ereg_replace("([^\w\d\-_\[\]\(\)])", '_', $userFileBasename); 332 $userFileBasename = mb_ereg_replace("(_{1,})", '_', $userFileBasename); 333 334 // Trying to get a meaningful and unique file name 335 // It will be something like "Uploaded_file_docx_2018-11-24_23-00-00_img1.jpg" 336 $imgBasename = $userFileBasename.'_'.$this->now.'_img'.strval( count($this->importedImages)+1 ).'.'.$ext; 337 $imgId = $this->nsName.':'.$imgBasename; 338 $destFile = mediaFN( $imgId ); 339 340 // Add to mediaManagerif authorized 341 if ( auth_quickaclcheck($ID) >= AUTH_UPLOAD ) { 342 // Import the image file in the mediaManager (data/media) 343 $destDir = mediaFN( $this->nsName ); 344 if ( ! ( file_exists( $destDir ) || mkdir( $destDir, 0777, true ) ) ) { 345 return $this->_msg( array( 'er_dirCreate', 'Directory: '.$destDir ) ); 346 } 347 348 // This works, but do not know if it is a hack... Meybe it can be done other way? 349 $mediaReturn = media_upload_finish($imgPath, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' ); 350 351 if ( $mediaReturn == $this->nsName ) { 352 // "Upload" OK 353 $this->importedImages[] = $imgId; 354 // Replace string in result 355 $this->result = str_replace( '{{'.$imgPath, '{{:'.$imgId, $this->result ); 356 } else { 357 // Return error 358 return $this->_msg( array( 'er_img_upload', 'Image: '.$imgPath.' Return: '.print_r($mediaReturn,true) ) ); 359 } 360 } else { 361 // If not allowed to upload, return error. 362 return $this->_msg('er_acl_upload'); 363 } 364 365 $this->_msg(array('ok_info','Processed image: '.$imgPath)); 366 367 return true; 368 } 369 370 371 /** 372 * Converts $this->userFile to odt and stores it in $this->workFile 373 * 374 * @return bool true if process ended ok; false if failed 375 */ 376 function _OOConversion() { 377 378 // Conversion to odt file 379 $output = array(); 380 $command = 'cd ' . $this->workDir; 381 $command .= ' && sudo soffice --nofirststartwizard --headless --convert-to odt:"writer8" "' . $this->userFileName . '"'; 382 $return_var = shell_exec( $command ); 383 384 // Change original extension to ".odt" 385 $info = pathinfo($this->userFile); 386 $this->workFileName = $info['filename'] . '.odt'; 387 $this->workFile = $this->workDir.'/'. $this->workFileName; 388 389 if ( !file_exists($this->workFile) ) { 390 $message = '<br>Missing file: ' . $this->workFile; 391 $message .= '<br>Command: ' . $command; 392 $message .= '<br>Return: '. $return_var; 393 return $this->_msg( array('er_soffice',$message) ); 394 } 395 396 $this->_msg(array('ok_info','Open Office conversion done')); 397 398 return true; 399 } 400 401 /** 402 * Move uploaded file to a temp directory 403 * 404 * @return bool true if process ended ok; false if failed 405 */ 406 function _checkUploadedFile() { 407 ### _checkUploadedFile : group all process about the uploaded file ### 408 # OUTPUT : 409 # * true -> process successfully 410 # * false -> something wrong; using _msg to display what's wrong 411 412 //Check if file exists 413 if ( ! $_FILES['userFile'] ) return $this->_msg('er_file_miss'); 414 415 // Check the file status 416 if ( $_FILES['userFile']['error'] > 0 ) { 417 return $this->_msg( array( 'er_file_upload', $_FILES['userFile']['error'] ) ); 418 } 419 420 // Removed: check file mimetype. 421 // If pandoc can convert it, then it should work. 422 // If not,then it should give an error 423 424 // Create an unique temp work dir name 425 $confUploadDir = $this->getConf('parserUploadDir'); 426 if ( !file_exists($confUploadDir) ) { 427 $confUploadDir = null; 428 } 429 $this->workDir = $this->tempdir($confUploadDir, 'file2dw_', 0777); 430 if ($this->workDir == false) { 431 return $this->_msg('er_file_tmpDir'); 432 } 433 chmod( $this->workDir, 0777 ); 434 435 // Move the upload file into the work directory 436 $this->userFileName = $_FILES['userFile']['name']; 437 $this->userFile = $this->workDir.'/'.$this->userFileName; 438 if ( ! move_uploaded_file( $_FILES['userFile']['tmp_name'], $this->userFile ) ) { 439 return $this->_msg('er_file_getFromDownload'); 440 } 441 442 $this->_msg( array('ok_info','userFile moved to '.$this->userFile) ); 443 444 return true; 445 } 446 447 /** 448 * Display and/or log message using the debugLvl value 449 * 450 * @param string|array $message string: key for $this->getLang(); 451 * array: $message[0]: string: key for $this->getLang(), $message[1]: string: additional information 452 * @param int $type -1 -> error message, 0 -> normal message, 1 -> info message. 453 * If null, the first 3 char of the key define the message type:er_ -> -1, ok_ -> 1, otherwise -> 0 454 * @param bool $force force displaying the message without checking debugLvl 455 * @return bool true -> Display normal message; false ->Display an error message 456 */ 457 function _msg( $message, $type=null, $force=false ) { 458 459 ### _msg : display message using the debugLvl value 460 # $message : mixed : 461 # * string : key for $this->getLang() function 462 # * array : 463 # $message[0] : string : key for $this->getLang() function 464 # $message[1] : string : additional information 465 # $type : integer : (check the dokuwiki msg function) 466 # * -1 : error message 467 # * 0 : normal message 468 # * 1 : info message 469 # if type == null, the first 3 char of the key define the message type 470 # * er_ : -1 471 # * ok_ : 1 472 # * otherwise : 0 473 # $force : boolean : force displaying the message without checking debugLvl 474 # OUTPUT : 475 # * true -> display a normal message 476 # * false -> display an error message 477 # DISPLAY : call dokuwiki msg function 478 479 if ( is_array( $message ) ) { 480 $output = $message[0]; 481 } else { 482 $output = $message; 483 } 484 485 // If output is empty, crash with error display; 486 if ( ! $output ) die( $this->getLang( 'er_msg_nomessage' ) ); 487 488 // If no $type defined, get it from key 489 if ( is_null( $type ) ) { 490 $val = substr( $output, 0, strpos( $output, '_' )+1 ); 491 switch ($val) { 492 case 'er_' : 493 $err = -1; 494 break; 495 case 'ok_' : 496 $err = 1; 497 break; 498 default : 499 $err = 0; 500 } 501 } else { 502 if ( $type < -1 || $type > 1 ) return false; 503 $err = $type; 504 } 505 506 // Message content 507 $content = $output.' : '.$this->getLang( $output ).( is_array( $message ) ? ' : '.$message[1] : '' ); 508 509 // Determine if should show message 510 if ( $force || $this->debugShowInfo == 1 || $err == -1 ) { 511 msg( 'file2dw : '.$content, $err ); 512 }; 513 514 //Determine if should log message 515 if ( $this->logLevel > 0 && isset( $this->logFileHandle ) ) { 516 fwrite( $this->logFileHandle, date(DATE_ATOM).':'.$_SERVER['REMOTE_USER'].':'.$content.' ' ); 517 }; 518 519 return ( $err == -1 ? false : true); 520 521 } 522 523 /** 524 * Delete temp folder $this->workDir 525 * 526 * @return bool true if process ended ok; false if failed 527 */ 528 function _purge_env() { 529 530 if ( file_exists($this->workDir) ) { 531 return $this->_delTree($this->workDir); 532 } 533 return true; 534 535 } 536 537 /** 538 * Creates a random unique temporary directory, with specified parameters, 539 * that does not already exist (like tempnam(), but for dirs). 540 * 541 * Created dir will begin with the specified prefix, followed by random 542 * numbers. 543 * 544 * @link https://php.net/manual/en/function.tempnam.php 545 * 546 * @param string|null $dir Base directory under which to create temp dir. 547 * If null, the default system temp dir (sys_get_temp_dir()) will be 548 * used. 549 * @param string $prefix String with which to prefix created dirs. 550 * @param int $mode Octal file permission mask for the newly-created dir. 551 * Should begin with a 0. 552 * @param int $maxAttempts Maximum attempts before giving up (to prevent 553 * endless loops). 554 * @return string|bool Full path to newly-created dir, or false on failure. 555 */ 556 function tempdir($dir = null, $prefix = 'tmp_', $mode = 0700, $maxAttempts = 1000) 557 { 558 /* Use the system temp dir by default. */ 559 if (is_null($dir)) 560 { 561 $dir = sys_get_temp_dir(); 562 } 563 564 /* Trim trailing slashes from $dir. */ 565 $dir = rtrim($dir, '/'); 566 567 /* If we don't have permission to create a directory, fail, otherwise we will 568 * be stuck in an endless loop. 569 */ 570 if (!is_dir($dir) || !is_writable($dir)) 571 { 572 return false; 573 } 574 575 /* Make sure characters in prefix are safe. */ 576 if (strpbrk($prefix, '\\/:*?"<>|') !== false) 577 { 578 return false; 579 } 580 581 /* Attempt to create a random directory until it works. Abort if we reach 582 * $maxAttempts. Something screwy could be happening with the filesystem 583 * and our loop could otherwise become endless. 584 */ 585 $attempts = 0; 586 do 587 { 588 $path = sprintf('%s/%s%s', $dir, $prefix, mt_rand(100000, mt_getrandmax())); 589 } while ( 590 !mkdir($path, $mode) && 591 $attempts++ < $maxAttempts 592 ); 593 594 595 return $path; 596 } 597 598 /** 599 * Deletes (recursively) a directory that may not be empty 600 * 601 * @return bool true if process ended ok; false if failed 602 */ 603 function _delTree($dir) { 604 $files = array_diff(scandir($dir), array('.','..')); 605 foreach ($files as $file) { 606 (is_dir("$dir/$file")) ? $this->_delTree("$dir/$file") : unlink("$dir/$file"); 607 } 608 return rmdir($dir); 609 } 610 611} 612