* @version 0.1beta
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();
if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
class action_plugin_file2dw extends DokuWiki_Action_Plugin {
/**
* Registers a callback function for a given event
*/
function register(Doku_Event_Handler $controller) {
// File parser hook
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, '_parser', array());
// Display form hook before the wiki page (on top)
$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, '_render', array());
//Add MENU_ITEMS_ASSEMBLY
$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, '_addsvgbutton', array());
}
/**
* Add 'import'-button to menu
*
* @param Doku_Event $event
* @param mixed $param not defined
*/
function _addsvgbutton(&$event, $param) {
if($event->data['view'] == 'page') {
array_push($event->data['items'],new \dokuwiki\plugin\file2dw\MenuItem());
}
}
/**
* Displays the upload form in the pages according to authorized action
*
* @param Doku_Event $event It's a dokuwiki event function
* @param mixed $param Not defined
*/
function _render(&$event, $param) {
// $ID: Page identifier
global $ID;
// Check if should display the form
if ( strpos( $this->getConf('formDisplayRule'), $event->data) === false ) return;
// If the page exists but $event->data != "file2dw", return
if ( page_exists( $ID ) && $event->data != "file2dw" ) return;
// Check auth user can edit this page
if ( auth_quickaclcheck( $ID ) < AUTH_EDIT ) return;
// If page exists, show warning to the user
if ( page_exists( $ID ) ) echo p_render('xhtml',p_get_instructions( $this->getLang( 'formPageExistMessage' ) ), $info );
// Show form
echo $this->_createForm();
if ( $event->data == 'file2dw' ) $event->preventDefault();
}
/**
* Creates an returns a string with the HTML upload form to show to the user
*
* @return string HTML upload form
*/
function _createForm() {
global $ID;
$form = new dokuwiki\Form\Form(array('id' => 'file2dw_form', 'enctype' => 'multipart/form-data'));
// Intro message
$message = $this->getConf('formIntroMessage');
if ( $message == 'default' ) $message = $this->getLang('formIntroMessage');
if ( $message ) {
$message = p_render('xhtml',p_get_instructions($message),$info);
$form->addHTML($message);
}
//Open fieldset
$form->addFieldsetOpen();
//legend tag
$legend = $form->addTag('legend');
$legend->attr('value',$this->getLang('formLegend'));
//hidden
$form->setHiddenField('MAX_FILE_SIZE',$this->getConf('formMaxFileSize'));
$form->setHiddenField('do','file2dw');
$form->setHiddenField('id',$ID);
//userFile file input
$userFileInputElement = new dokuwiki\Form\InputElement('file','userFile');
$form->addElement($userFileInputElement);
// submit
$submitInputElement = new dokuwiki\Form\InputElement('submit','btn_upload');
$submitInputElement->attr('value',$this->getLang('import_button'));
$form->addElement($submitInputElement);
//Close fieldset
$form->addFieldsetClose();
return $form->toHTML();
}
/**
* Checks if the file might be uploaded, then call the file2dw converter
*
* @param Doku_Event $event It's a dokuwiki event function
* @param mixed $param Not defined
*/
function _parser(&$event, $param) {
// Check action is file2dw
if ( $event->data != 'file2dw' ) return;
// Preparation of the message renderer
// Set the debug lvl
$this->logLevel = $this->getConf( 'logLevel' );
$this->debugShowInfo = $this->getConf( 'debugShowInfo' );
//If used, open the logFile
if ( $this->logLevel > 0 ) {
$this->logFile = $this->getConf( 'logFile' );
if ( isset( $this->logFile ) ) {
if ( file_exists( dirname( $this->logFile ) ) || mkdir( dirname( $this->logFile ) ) ) {
if ( ! ( $this->logFileHandle = @fopen( $this->logFile, 'a' ) ) ) {
unset( $this->logFileHandle, $this->logFile );
}
} else {
unset( $this->logFile );
}
}
if ( ! isset( $this->logFileHandle ) ) {
$this->_msg('er_logFile');
}
}
// Check upload file defined
$retorno = false;
if ( $_FILES['userFile'] && $_FILES['userFile']['error'] == 0 ) {
$this->_msg( array('ok_info','userFile found: '.$_FILES['userFile']['name']) );
// If parse work, change action to defined one in conf/local.php file
$retorno = $this->_file2dw();
// Delete temp folder
$this->_purge_env();
}
// if the file is correctly parsed, change the action to "show"
// otherwise the action stay file2dw
if ( $retorno === true ) {
$event->data = 'show';
} else {
$event->preventDefault();
}
// Clear the message renderer
// Close the log file if used
if ( isset( $this->logFileHandle ) ) {
@fclose( $this->logFileHandle );
}
}
/**
* Converts uploaded file to Dokuwiki syntax
*
* @return bool true if conversion ended ok; false if conversion failed
*/
function _file2dw() {
global $ID;
### Check parameter ###
// Page receive content
if ( ! $this->pageName = $ID ) return $this->_msg('er_id');
$this->nsName = getNS($this->pageName);
// Check rights to change the page
if ( page_exists($ID) ) {
if ( auth_quickaclcheck($ID) < AUTH_EDIT ) return $this->_msg('er_acl_edit');
} else {
if ( auth_quickaclcheck($ID) < AUTH_CREATE ) return $this->_msg('er_acl_create');
}
// Check uploaded file
$this->_checkUploadedFile();
// Need OpenOffice conversion?
// workFile is the file that will be converted by pandoc to dokuwiki syntax
// workFile is the userFile by default
// It will be changed if OpenOffice conversion is needed (example: .doc files)
$this->workFileName = substr($this->userFileName,0);
$this->workFile = substr($this->userFile,0);
if ($this->getConf( 'parserMimeTypeSOffice' ) != ''
&& strpos( $this->getConf( 'parserMimeTypeSOffice' ), $_FILES['userFile']['type'] ) !== false) {
if ( !$this->_OOConversion() ) return false;
}
// pandoc conversion
// Resulting file name: dwpage
// Images folder: img
$this->dwpageFileName = 'dwpage';
$this->dwpageFile = $this->workDir.'/'.$this->dwpageFileName;
$this->dwimgDir = $this->workDir.'/img';
$output = array();
$command = 'pandoc -s -w dokuwiki --extract-media="'.$this->dwimgDir;
$command .= '" -o "'.$this->dwpageFile.'" "'.$this->workFile.'"';
exec( $command, $output, $return_var );
$this->_msg(array('ok_info','Executed command: '.$command));
if ( !file_exists($this->dwpageFile) ) {
$message = '
Missing file: ' . $this->dwpageFile;
$message .= '
Command: ' . $command;
$message .= '
Output: '. print_r($output,true);
$message .= '
Return: '. $return_var;
return $this->_msg( array('er_pandoc',$message) );
}
$this->_msg(array('ok_info','pandoc conversion done'));
// Initial result
$this->result = '====== '.basename($this->userFileName).' ======
';
if ( $this->getConf('parserLinkToOriginalFile') && auth_quickaclcheck($ID) >= AUTH_UPLOAD ) {
$this->result .= '{{'.$this->userFileName.'|'.$this->getLang('parserOriginalFile').'}}
';
}
$this->result .= file_get_contents ($this->dwpageFile);
// If dwimgDir does not exist, we do not need to porcess it
if (is_dir($this->dwimgDir)) {
$this->_msg(array('ok_info','Start processing dir '.$this->dwimgDir));
// Use $this->now to put a timestamp in images name
$this->now = date('Y-m-d_H-i-s');
// Use $this->importedImages to count (and store, if we need to delete them after an error)
$this->importedImages = array();
if ( !$this->_processImgDir($this->dwimgDir) ) {
//Delete all imported images until error from dokuwiki
foreach ($this->importedImages as $imgId) {
media_delete($imgId, null);
}
// Return error
return $this->_msg('er_img_dir');
}
}
$this->_msg(array('ok_info','Resultado: '.$this->result));
// Keep the original file (import the upload file in the mediaManager)
if ( auth_quickaclcheck($ID) >= AUTH_UPLOAD ) {
$destFile = mediaFN( $this->nsName.':'.$this->userFileName );
list( $ext, $mime ) = mimetype($this->userFile);
if ( media_upload_finish($this->userFile, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' ) != $this->nsName ) {
return $this->_msg( array( 'er_apply_file' ) );
}
} else {
// If not allowed to upload, return error.
return $this->_msg('er_acl_upload');
}
// Save wiki page
saveWikiText( $this->pageName, $this->result, $this->getLang( 'parserSummary' ).$this->userFileName );
if ( ! page_exists($this->pageName) ) return $this->_msg('er_apply_content');
return true;
}
/**
* Add images in a directory (and its subdirectories) to Dokuwiki mediaManager.
* Also updates $this->result (it will be wiki page content).
*
* @param string $imgDir Full path directory to process
* @return bool true if process ended ok; false if failed
*/
function _processImgDir($imgDir) {
// In $imgDir is not a directory, return error
if (!is_dir($imgDir)) return $this->_msg(array('er_img_dir',$imgDir.' is not a directory'));
// list and process directory items
$items = array_diff(scandir($imgDir), array('.','..'));
foreach ($items as $item) {
$itemPath = "$imgDir/$item";
if (is_dir($itemPath)) {
if (!$this->_processImgDir($itemPath)) {
return $this->_msg(array ('er_img_dir','Error processing directory '.$itemPath) );
}
} else {
if (!$this->_processImg($itemPath)) {
return $this->_msg(array('er_img_dir','Error processing image '.$itemPath));
}
}
}
$this->_msg(array('ok_info','Processed image directory: '.$imgDir));
return true;
}
/**
* Add single image to Dokuwiki mediaManager.
* Also updates $this->result (it will be wiki page content).
*
* @param string $imgPath Full path image to process
* @return bool true if process ended ok; false if failed
*/
function _processImg($imgPath) {
list( $ext, $mime ) = mimetype( $imgPath );
// Sanitize original file name
$userFileBasename = basename($this->userFileName);
$userFileBasename = mb_ereg_replace("([^\w\d\-_\[\]\(\)])", '_', $userFileBasename);
$userFileBasename = mb_ereg_replace("(_{1,})", '_', $userFileBasename);
// Trying to get a meaningful and unique file name
// It will be something like "Uploaded_file_docx_2018-11-24_23-00-00_img1.jpg"
$imgBasename = $userFileBasename.'_'.$this->now.'_img'.strval( count($this->importedImages)+1 ).'.'.$ext;
$imgId = $this->nsName.':'.$imgBasename;
$destFile = mediaFN( $imgId );
// Add to mediaManagerif authorized
if ( auth_quickaclcheck($ID) >= AUTH_UPLOAD ) {
// Import the image file in the mediaManager (data/media)
$destDir = mediaFN( $this->nsName );
if ( ! ( file_exists( $destDir ) || mkdir( $destDir, 0777, true ) ) ) {
return $this->_msg( array( 'er_dirCreate', 'Directory: '.$destDir ) );
}
// This works, but do not know if it is a hack... Meybe it can be done other way?
$mediaReturn = media_upload_finish($imgPath, $destFile, $this->nsName, $mime, @file_exists($destFile), 'rename' );
if ( $mediaReturn == $this->nsName ) {
// "Upload" OK
$this->importedImages[] = $imgId;
// Replace string in result
$this->result = str_replace( '{{'.$imgPath, '{{:'.$imgId, $this->result );
} else {
// Return error
return $this->_msg( array( 'er_img_upload', 'Image: '.$imgPath.' Return: '.print_r($mediaReturn,true) ) );
}
} else {
// If not allowed to upload, return error.
return $this->_msg('er_acl_upload');
}
$this->_msg(array('ok_info','Processed image: '.$imgPath));
return true;
}
/**
* Converts $this->userFile to odt and stores it in $this->workFile
*
* @return bool true if process ended ok; false if failed
*/
function _OOConversion() {
// Conversion to odt file
$output = array();
$command = 'cd ' . $this->workDir;
$command .= ' && sudo soffice --nofirststartwizard --headless --convert-to odt:"writer8" "' . $this->userFileName . '"';
$return_var = shell_exec( $command );
// Change original extension to ".odt"
$info = pathinfo($this->userFile);
$this->workFileName = $info['filename'] . '.odt';
$this->workFile = $this->workDir.'/'. $this->workFileName;
if ( !file_exists($this->workFile) ) {
$message = '
Missing file: ' . $this->workFile;
$message .= '
Command: ' . $command;
$message .= '
Return: '. $return_var;
return $this->_msg( array('er_soffice',$message) );
}
$this->_msg(array('ok_info','Open Office conversion done'));
return true;
}
/**
* Move uploaded file to a temp directory
*
* @return bool true if process ended ok; false if failed
*/
function _checkUploadedFile() {
### _checkUploadedFile : group all process about the uploaded file ###
# OUTPUT :
# * true -> process successfully
# * false -> something wrong; using _msg to display what's wrong
//Check if file exists
if ( ! $_FILES['userFile'] ) return $this->_msg('er_file_miss');
// Check the file status
if ( $_FILES['userFile']['error'] > 0 ) {
return $this->_msg( array( 'er_file_upload', $_FILES['userFile']['error'] ) );
}
// Removed: check file mimetype.
// If pandoc can convert it, then it should work.
// If not,then it should give an error
// Create an unique temp work dir name
$confUploadDir = $this->getConf('parserUploadDir');
if ( !file_exists($confUploadDir) ) {
$confUploadDir = null;
}
$this->workDir = $this->tempdir($confUploadDir, 'file2dw_', 0777);
if ($this->workDir == false) {
return $this->_msg('er_file_tmpDir');
}
chmod( $this->workDir, 0777 );
// Move the upload file into the work directory
$this->userFileName = $_FILES['userFile']['name'];
$this->userFile = $this->workDir.'/'.$this->userFileName;
if ( ! move_uploaded_file( $_FILES['userFile']['tmp_name'], $this->userFile ) ) {
return $this->_msg('er_file_getFromDownload');
}
$this->_msg( array('ok_info','userFile moved to '.$this->userFile) );
return true;
}
/**
* Display and/or log message using the debugLvl value
*
* @param string|array $message string: key for $this->getLang();
* array: $message[0]: string: key for $this->getLang(), $message[1]: string: additional information
* @param int $type -1 -> error message, 0 -> normal message, 1 -> info message.
* If null, the first 3 char of the key define the message type:er_ -> -1, ok_ -> 1, otherwise -> 0
* @param bool $force force displaying the message without checking debugLvl
* @return bool true -> Display normal message; false ->Display an error message
*/
function _msg( $message, $type=null, $force=false ) {
### _msg : display message using the debugLvl value
# $message : mixed :
# * string : key for $this->getLang() function
# * array :
# $message[0] : string : key for $this->getLang() function
# $message[1] : string : additional information
# $type : integer : (check the dokuwiki msg function)
# * -1 : error message
# * 0 : normal message
# * 1 : info message
# if type == null, the first 3 char of the key define the message type
# * er_ : -1
# * ok_ : 1
# * otherwise : 0
# $force : boolean : force displaying the message without checking debugLvl
# OUTPUT :
# * true -> display a normal message
# * false -> display an error message
# DISPLAY : call dokuwiki msg function
if ( is_array( $message ) ) {
$output = $message[0];
} else {
$output = $message;
}
// If output is empty, crash with error display;
if ( ! $output ) die( $this->getLang( 'er_msg_nomessage' ) );
// If no $type defined, get it from key
if ( is_null( $type ) ) {
$val = substr( $output, 0, strpos( $output, '_' )+1 );
switch ($val) {
case 'er_' :
$err = -1;
break;
case 'ok_' :
$err = 1;
break;
default :
$err = 0;
}
} else {
if ( $type < -1 || $type > 1 ) return false;
$err = $type;
}
// Message content
$content = $output.' : '.$this->getLang( $output ).( is_array( $message ) ? ' : '.$message[1] : '' );
// Determine if should show message
if ( $force || $this->debugShowInfo == 1 || $err == -1 ) {
msg( 'file2dw : '.$content, $err );
};
//Determine if should log message
if ( $this->logLevel > 0 && isset( $this->logFileHandle ) ) {
fwrite( $this->logFileHandle, date(DATE_ATOM).':'.$_SERVER['REMOTE_USER'].':'.$content.' ' );
};
return ( $err == -1 ? false : true);
}
/**
* Delete temp folder $this->workDir
*
* @return bool true if process ended ok; false if failed
*/
function _purge_env() {
if ( file_exists($this->workDir) ) {
return $this->_delTree($this->workDir);
}
return true;
}
/**
* Creates a random unique temporary directory, with specified parameters,
* that does not already exist (like tempnam(), but for dirs).
*
* Created dir will begin with the specified prefix, followed by random
* numbers.
*
* @link https://php.net/manual/en/function.tempnam.php
*
* @param string|null $dir Base directory under which to create temp dir.
* If null, the default system temp dir (sys_get_temp_dir()) will be
* used.
* @param string $prefix String with which to prefix created dirs.
* @param int $mode Octal file permission mask for the newly-created dir.
* Should begin with a 0.
* @param int $maxAttempts Maximum attempts before giving up (to prevent
* endless loops).
* @return string|bool Full path to newly-created dir, or false on failure.
*/
function tempdir($dir = null, $prefix = 'tmp_', $mode = 0700, $maxAttempts = 1000)
{
/* Use the system temp dir by default. */
if (is_null($dir))
{
$dir = sys_get_temp_dir();
}
/* Trim trailing slashes from $dir. */
$dir = rtrim($dir, '/');
/* If we don't have permission to create a directory, fail, otherwise we will
* be stuck in an endless loop.
*/
if (!is_dir($dir) || !is_writable($dir))
{
return false;
}
/* Make sure characters in prefix are safe. */
if (strpbrk($prefix, '\\/:*?"<>|') !== false)
{
return false;
}
/* Attempt to create a random directory until it works. Abort if we reach
* $maxAttempts. Something screwy could be happening with the filesystem
* and our loop could otherwise become endless.
*/
$attempts = 0;
do
{
$path = sprintf('%s/%s%s', $dir, $prefix, mt_rand(100000, mt_getrandmax()));
} while (
!mkdir($path, $mode) &&
$attempts++ < $maxAttempts
);
return $path;
}
/**
* Deletes (recursively) a directory that may not be empty
*
* @return bool true if process ended ok; false if failed
*/
function _delTree($dir) {
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? $this->_delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
}