1<?php 2/** 3 * DokuWiki Plugin struct (Helper Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Szymon Olewniczak <it@rid.pl> 7 */ 8 9// must be run within Dokuwiki 10use dokuwiki\File\MediaResolver; 11use dokuwiki\Parsing\Parser; 12use dokuwiki\plugin\struct\meta\AccessTable; 13use dokuwiki\plugin\struct\meta\Schema; 14use dokuwiki\plugin\struct\meta\Search; 15use dokuwiki\plugin\struct\meta\StructException; 16use dokuwiki\plugin\struct\meta\Value; 17use dokuwiki\plugin\struct\types\Wiki; 18use splitbrain\PHPArchive\FileInfo; 19use splitbrain\PHPArchive\Zip; 20 21if(!defined('DOKU_INC')) die(); 22 23class helper_plugin_structodt extends DokuWiki_Plugin { 24 /** 25 * Generate temporary file name with full path in temporary directory 26 * 27 * @param string $ext 28 * @return string 29 */ 30 public function tmpFileName($ext='') { 31 global $conf; 32 $name = $conf['tmpdir'] . '/structodt/' . uniqid(); 33 if ($ext) { 34 $name .= ".$ext"; 35 } 36 return $name; 37 } 38 39 /** 40 * Render ODT file from template 41 * 42 * @param $template 43 * @param $schemas 44 * @param $pid 45 * 46 * @return string|bool 47 * @throws \splitbrain\PHPArchive\ArchiveIOException 48 * @throws \splitbrain\PHPArchive\FileInfoException 49 * @throws Exception 50 */ 51 public function renderODT($template, $row) { 52 $template_file = mediaFN($template); 53 $tmp_dir = $this->tmpFileName() . '/'; 54 if (!mkdir($tmp_dir, 0777, true)) { 55 throw new \Exception("could not create tmp dir - bad permissions?", -1); 56 } 57 58 $template_zip = new Zip(); 59 $template_zip->open($template_file); 60 $template_zip->extract($tmp_dir); 61 62 //do replacements 63 $files = array('content.xml', 'styles.xml'); 64 foreach ($files as $file) { 65 $content_file = $tmp_dir . $file; 66 $content = file_get_contents($content_file); 67 if ($content === false) { 68 $this->rmdir_recursive($tmp_dir); 69 throw new \Exception("Cannot open: $content_file"); 70 } 71 72 $content = $this->replace($content, $row); 73 file_put_contents($content_file, $content); 74 } 75 76 77 $tmp_file = $this->tmpFileName('odt'); 78 79 $tmp_zip = new Zip(); 80 $tmp_zip->create($tmp_file); 81 foreach($this->readdir_recursive($tmp_dir) as $file) { 82 $fileInfo = FileInfo::fromPath($file); 83 $fileInfo->strip(substr($tmp_dir, 1)); 84 $tmp_zip->addFile($file, $fileInfo); 85 } 86 $tmp_zip->close(); 87 88 //remove temp dir 89 $this->rmdir_recursive($tmp_dir); 90 91 return $tmp_file; 92 } 93 94 /** 95 * Render PDF file from template 96 * 97 * @param $template 98 * @param $schemas 99 * @param $pid 100 * 101 * @return string|bool 102 * @throws \splitbrain\PHPArchive\ArchiveIOException 103 * @throws \splitbrain\PHPArchive\FileInfoException 104 * @throws Exception 105 */ 106 public function renderPDF($template, $row) { 107 $tmp_file = $this->renderODT($template, $row); 108 109 $wd = dirname($tmp_file); 110 $bn = basename($tmp_file); 111 $cmd = "cd $wd && HOME=$wd unoconv -f pdf $bn 2>&1"; 112 exec($cmd, $output, $result_code); 113 @unlink($tmp_file); // remove odt file 114 if ($result_code != 0) { 115 throw new \Exception("PDF conversion error($result_code): " . implode('<br>', $output), -1); 116 } 117 //change extension to pdf 118 $tmp_file = substr($tmp_file, 0, -3) . 'pdf'; 119 120 return $tmp_file; 121 } 122 123 public function concatenate($rendered_pages) { 124 $tmp_file = $this->tmpFileName('pdf'); 125 $cmd = "gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$tmp_file "; 126 // Add each pdf file to the end of the command 127 foreach($rendered_pages as $file) { 128 $cmd .= $file.' '; 129 } 130 $cmd .= '2>&1'; 131 exec($cmd, $output, $result_code); 132 if ($result_code != 0) { 133 throw new \Exception("PDF concatenation error($result_code): " . implode('<br>', $output), -1); 134 } 135 return $tmp_file; 136 } 137 138 /** 139 * Send ODT file using range request 140 * 141 * @param $tmp_file string path of sending file 142 * @param $filename string name of sending file 143 * $param $ext odt or pdf 144 */ 145 public function sendFile($tmp_file, $filename, $ext='odt') { 146 $mime = "application/$ext"; 147 header("Content-Type: $mime"); 148 header("Content-Disposition: attachment; filename=\"$filename.$ext\";"); 149 150 http_sendfile($tmp_file); 151 152 $fp = @fopen($tmp_file, "rb"); 153 if($fp) { 154 //we have to remove file before exit 155 define('SIMPLE_TEST', true); 156 http_rangeRequest($fp, filesize($tmp_file), $mime); 157 } else { 158 header("HTTP/1.0 500 Internal Server Error"); 159 print "Could not read file - bad permissions?"; 160 } 161 } 162 163 /** 164 * Read directory recursively 165 * 166 * @param string $path 167 * @return array of file full paths 168 */ 169 public function readdir_recursive($path) { 170 $directory = new \RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); 171 $iterator = new \RecursiveIteratorIterator($directory); 172 $files = array(); 173 foreach ($iterator as $info) { 174 if ($info->isFile()) { 175 $files[] = $info->getPathname(); 176 } 177 } 178 179 return $files; 180 } 181 182 /** 183 * Remove director recursively 184 * 185 * @param $path 186 */ 187 public function rmdir_recursive($path) { 188 $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); 189 $iterator = new RecursiveIteratorIterator($directory, 190 RecursiveIteratorIterator::CHILD_FIRST); 191 foreach($iterator as $file) { 192 if ($file->isDir()){ 193 @rmdir($file->getRealPath()); 194 } else { 195 @unlink($file->getRealPath()); 196 } 197 } 198 rmdir($path); 199 } 200 201 /** 202 * @param $schemas 203 * @param Schema $first_schema 204 * @return Search 205 */ 206 public function getSearch($schemas, &$first_schema) { 207 $search = new Search(); 208 if (!empty($schemas)) foreach ($schemas as $schema) { 209 $search->addSchema($schema[0], $schema[1]); 210 } 211 $search->addColumn('*'); 212 $first_schema = $search->getSchemas()[0]; 213 214 $search->addColumn('%rowid%'); 215 $search->addColumn('%pageid%'); 216 $search->addColumn('%title%'); 217 $search->addColumn('%lastupdate%'); 218 $search->addColumn('%lasteditor%'); 219 220 return $search; 221 } 222 223 /** 224 * Get rows data, optionally filtered by pid 225 * 226 * @param string|array $schemas 227 * @param Schema $first_schema 228 * @return Value[][] 229 */ 230 public function getRows($schemas, &$first_schema, $filters=array()) 231 { 232 $search = $this->getSearch($schemas, $first_schema); 233 foreach ($filters as $filter) { 234 $colname = $filter[0]; 235 $value = $filter[2]; 236 $comp = $filter[1]; 237 $op = $filter[3]; 238 $search->addFilter($colname, $value, $comp, $op); 239 } 240 $result = $search->execute(); 241 return $result; 242 } 243 244 /** 245 * Get single row by pid 246 * 247 * @param $schemas 248 * @param $pid 249 * @return Value[]|null 250 */ 251 public function getRow($table, $pid, $rev, $rid) { 252 // second value of schema array is alias. ignore it for now 253 $schema = [$table, '']; 254 $search = $this->getSearch([$schema], $ignore); 255 if (AccessTable::isTypePage($pid, $rev)) { 256 $search->addFilter('%pageid%', $pid, '='); 257 } elseif (AccessTable::isTypeSerial($pid, $rev)) { 258 $search->addFilter('%pageid%', $pid, '='); 259 $search->addFilter('%rowid%', $rid, '='); 260 } else { 261 $search->addFilter('%rowid%', $rid, '='); 262 } 263 $result = $search->execute(); 264 return $result[0]; 265 } 266 267 /** 268 * Perform $content replacements basing on $row Values 269 * 270 * @param string $content 271 * @param Value[] $row 272 * @return string 273 */ 274 public function replace($content, $row) { 275 /** @var Value $value */ 276 foreach ($row as $value) { 277 $label = $value->getColumn()->getLabel(); 278 $pattern = '/@@' . preg_quote($label) . '(?:\[(\d+)\])?@@/'; 279 $content = preg_replace_callback($pattern, function($matches) use ($value) { 280 $dvalue = $value->getDisplayValue(); 281 if (isset($matches[1])) { 282 $index = (int)$matches[1]; 283 if (!is_array($dvalue)) { 284 $dvalue = array_map('trim', explode('|', $dvalue)); 285 } 286 if (isset($dvalue[$index])) { 287 return $dvalue[$index]; 288 } 289 return 'Array: index out of bound'; 290 } elseif (is_array($dvalue)) { 291 return implode(',', $dvalue); 292 } elseif ($value->getColumn()->getType() instanceof Wiki) { 293 //import parser classes and mode definitions 294 require_once DOKU_INC . 'inc/parser/parser.php'; 295 296 // Create the parser and handler 297 $Parser = new Parser(new Doku_Handler()); 298 299 // add default modes 300 $std_modes = array('linebreak', 'eol'); 301 302 foreach($std_modes as $m){ 303 $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m); 304 $obj = new $class(); 305 $modes[] = array( 306 'sort' => $obj->getSort(), 307 'mode' => $m, 308 'obj' => $obj 309 ); 310 } 311 312 //add modes to parser 313 foreach($modes as $mode){ 314 $Parser->addMode($mode['mode'],$mode['obj']); 315 } 316 // Do the parsing 317 $instructions = $Parser->parse($dvalue); 318 $Renderer = p_get_renderer('structodt'); 319 320 // Loop through the instructions 321 foreach ($instructions as $instruction) { 322 // Execute the callback against the Renderer 323 if(method_exists($Renderer, $instruction[0])){ 324 call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array()); 325 } 326 } 327 328 return $Renderer->doc; 329 } 330 return $dvalue; 331 }, $content); 332 } 333 334 return $content; 335 } 336 337 /** 338 * @param $row 339 * @param string $template 340 * @return string 341 * @throws Exception 342 */ 343 public function rowTemplate($row, $template) { 344 return preg_replace_callback('/\$(.*?)\$/', function ($matches) use ($row) { 345 $possibleValueTypes = array('getValue', 'getCompareValue', 'getDisplayValue', 'getRawValue'); 346 $explode = explode('.', $matches[1], 2); 347 $label = $explode[0]; 348 $valueType = 'getDisplayValue'; 349 if (isset($explode[1]) && in_array($explode[1], $possibleValueTypes)) { 350 $valueType = $explode[1]; 351 } 352 foreach ($row as $value) { 353 $column = $value->getColumn(); 354 if ($column->getLabel() == $label) { 355 return call_user_func(array($value, $valueType)); 356 } 357 } 358 return ''; 359 }, $template); 360 } 361}