1<?php 2/** 3 * DokuWiki Plugin PreserveFilenames / action_anteater.php 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Kazutaka Miyasaka <kazmiya@gmail.com> 7 */ 8 9// must be run within DokuWiki 10if (!defined('DOKU_INC')) { 11 die(); 12} 13 14if (!defined('DOKU_PLUGIN')) { 15 define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 16} 17 18require_once(DOKU_PLUGIN . 'action.php'); 19require_once(DOKU_PLUGIN . 'preservefilenames/common.php'); 20 21class action_plugin_preservefilenames_anteater extends DokuWiki_Action_Plugin 22{ 23 /** 24 * Common functions 25 */ 26 protected $common; 27 28 /** 29 * Registers event handlers 30 */ 31 function register(&$controller) 32 { 33 $this->common = new PreserveFilenames_Common(); 34 35 $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, '_saveMeta'); 36 $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, '_deleteMeta'); 37 $controller->register_hook('MEDIA_SENDFILE', 'BEFORE', $this, '_sendFile'); 38 $controller->register_hook('PARSER_HANDLER_DONE', 'BEFORE', $this, '_replaceLinkTitle'); 39 $controller->register_hook('RENDERER_CONTENT_POSTPROCESS', 'AFTER', $this, '_replaceLinkURL'); 40 $controller->register_hook('MEDIAMANAGER_STARTED', 'AFTER', $this, '_exportToJSINFO'); 41 $controller->register_hook('MEDIAMANAGER_CONTENT_OUTPUT', 'BEFORE', $this, '_showMediaList'); 42 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_showMediaListAjax'); 43 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, '_replaceSnippetDownload'); 44 } 45 46 /** 47 * Saves the name of the uploaded media file to a meta file 48 */ 49 function _saveMeta(&$event) 50 { 51 global $conf; 52 53 $id = $event->data[2]; 54 $filename_tidy = noNS($id); 55 56 // retrieve original filename 57 if (!empty($_POST['id'])) { 58 // via normal uploader 59 $filename_pat = $conf['useslash'] ? '/([^:;\/]*)$/' : '/([^:;]*)$/'; 60 preg_match($filename_pat, $_POST['id'], $matches); 61 $filename_orig = $matches[1]; 62 } elseif (isset($_FILES['Filedata'])) { 63 // via multiuploader 64 $filename_orig = $_FILES['upload']['name']; 65 } else { 66 return; 67 } 68 69 $filename_safe = $this->common->_sanitizeFileName($filename_orig); 70 71 // no need to save original filename 72 if ($filename_tidy === $filename_safe) { 73 return; 74 } 75 76 // fallback if suspicious characters found 77 if ($filename_orig !== $filename_safe) { 78 return; 79 } 80 81 // save original filename to metadata 82 $metafile = metaFN($id, '.filename'); 83 io_saveFile($metafile, serialize(array( 84 'filename' => $filename_safe, 85 ))); 86 } 87 88 /** 89 * Deletes a meta file associated with the deleted media file 90 */ 91 function _deleteMeta(&$event) 92 { 93 $id = $event->data['id']; 94 $metafile = metaFN($id, '.filename'); 95 96 if (@unlink($metafile)) { 97 io_sweepNS($id, 'metadir'); 98 } 99 } 100 101 /** 102 * Sends a media file with its original filename 103 * 104 * @see sendFile() in lib/exe/fetch.php 105 */ 106 function _sendFile(&$event) 107 { 108 global $conf; 109 global $MEDIA; 110 111 $d = $event->data; 112 $event->preventDefault(); 113 list($file, $mime, $dl, $cache) = array($d['file'], $d['mime'], $d['download'], $d['cache']); 114 115 $fmtime = @filemtime($file); 116 117 // send headers 118 header("Content-Type: $mime"); 119 120 // smart http caching headers 121 if ($cache == -1) { 122 // cache 123 // cachetime or one hour 124 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + max($conf['cachetime'], 3600)) . ' GMT'); 125 header('Cache-Control: public, proxy-revalidate, no-transform, max-age=' . max($conf['cachetime'], 3600)); 126 header('Pragma: public'); 127 } elseif ($cache > 0) { 128 // recache 129 // remaining cachetime + 10 seconds so the newly recached media is used 130 header('Expires: ' . gmdate("D, d M Y H:i:s", $fmtime + $conf['cachetime'] + 10) . ' GMT'); 131 header('Cache-Control: public, proxy-revalidate, no-transform, max-age=' . max($fmtime - time() + $conf['cachetime'] + 10, 0)); 132 header('Pragma: public'); 133 } elseif ($cache == 0) { 134 // nocache 135 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 136 header('Pragma: public'); 137 } 138 139 // send important headers first, script stops here if '304 Not Modified' response 140 http_conditionalRequest($fmtime); 141 142 // retrieve original filename and send Content-Disposition header 143 $filename = $this->_getOriginalFileName($MEDIA); 144 145 if ($filename === false) { 146 $filename = utf8_decodeFN($this->common->_correctBasename($d['file'])); 147 } 148 149 header($this->common->_buildContentDispositionHeader($dl, $filename)); 150 151 // use x-sendfile header to pass the delivery to compatible webservers 152 if (http_sendfile($file)) { 153 exit; 154 } 155 156 // send file contents 157 $fp = @fopen($file, 'rb'); 158 159 if ($fp) { 160 http_rangeRequest($fp, filesize($file), $mime); 161 } else { 162 header('HTTP/1.0 500 Internal Server Error'); 163 print "Could not read $file - bad permissions?"; 164 } 165 } 166 167 /** 168 * Replaces titles of non-labeled internal media links with their original filenames 169 */ 170 function _replaceLinkTitle(&$event) 171 { 172 global $ID; 173 global $conf; 174 175 require_once(DOKU_INC . 'inc/JpegMeta.php'); 176 177 $ns = getNS($ID); 178 179 // get the instructions list from the handler 180 $calls =& $event->data->calls; 181 182 // array index numbers for readability 183 list($handler_name, $instructions, $source, $title, $linking) = array(0, 1, 0, 1, 6); 184 185 // scan media link and mark it with its original filename 186 $last = count($calls) - 1; 187 188 for ($i = 0; $i <= $last; $i++) { 189 // NOTE: 'externalmedia' is processed here because there is a 190 // basename() bug in fetching its auto-filled linktext. 191 // For more details please see common->_correctBasename(). 192 if (!preg_match('/^(?:in|ex)ternalmedia$/', $calls[$i][$handler_name])) { 193 continue; 194 } 195 196 $inst =& $calls[$i][$instructions]; 197 $filename = false; 198 $linktext = $inst[$title]; 199 $linkonly = ($inst[$linking] === 'linkonly'); 200 list($src, $hash) = explode('#', $inst[$source], 2); 201 202 // get original filename 203 if ($calls[$i][$handler_name] === 'internalmedia') { 204 resolve_mediaid($ns, $src, $exists); 205 list($ext, $mime, $dl) = mimetype($src); 206 $filename = $this->_getOriginalFileName($src); 207 } else { 208 list($ext, $mime, $dl) = mimetype($src); 209 } 210 211 // prefetch auto-filled linktext 212 if (!$linktext) { 213 if ( 214 substr($mime, 0, 5) === 'image' 215 && ($ext === 'jpg' || $ext === 'jpeg') 216 && ($jpeg = new JpegMeta(mediaFN($src))) 217 && ($caption = $jpeg->getTitle()) 218 ) { 219 $linktext = $caption; 220 } else { 221 $linktext = $this->common->_correctBasename(noNS($src)); 222 } 223 } 224 225 // add a marker (normally you cannot put '}}' in a media link title 226 // and cannot put ':' in a filename) 227 if ($filename === false) { 228 $inst[$title] = $linktext; 229 } elseif ($inst[$title] !== $linktext) { 230 $inst[$title] = $linktext . '}}preservefilenames:autofilled:' . $filename; 231 } else { 232 $inst[$title] = $linktext . '}}preservefilenames::' . $filename; 233 } 234 } 235 } 236 237 /** 238 * Replaces url and title of a link which has original filename info 239 */ 240 function _replaceLinkURL(&$event) 241 { 242 if ($event->data[0] !== 'xhtml') { 243 return; 244 } 245 246 // image link 247 $event->data[1] = preg_replace_callback( 248 '/ 249 <a ([^>]*)> 250 (?: 251 ([^<>\}]*)\}\}preservefilenames:(autofilled)?:([^<]*) 252 | 253 (<img [^>]*?alt="([^"]*)\}\}preservefilenames:(autofilled)?:([^"]*)"[^>]*>) 254 ) 255 <\/a> 256 /x', 257 array(self, '_replaceLinkURL_callback_a'), 258 $event->data[1] 259 ); 260 261 // embedded image 262 $event->data[1] = preg_replace_callback( 263 '/<img [^>]*?alt="([^"]*)\}\}preservefilenames:(autofilled)?:([^"]*)"[^>]*>/', 264 array(self, '_replaceLinkURL_callback_img'), 265 $event->data[1] 266 ); 267 } 268 269 /** 270 * Callback function for _replaceLinkURL (link) 271 */ 272 static function _replaceLinkURL_callback_a($matches) 273 { 274 list($atag, $attr_str, $linktext, $autofilled, $filename) = array_slice($matches, 0, 5); 275 276 if (!preg_match('/class="media[" ]/', $attr_str)) { 277 return $atag; 278 } 279 280 if (isset($matches[5])) { 281 // image link 282 $filename = $matches[8]; 283 $linktext = self::_replaceLinkURL_callback_img(array_slice($matches, 5, 4)); 284 } else { 285 // text link 286 if ($autofilled) { 287 $linktext = $filename; 288 } 289 } 290 291 $filename = htmlspecialchars_decode($filename, ENT_QUOTES); 292 $pageid = cleanID($filename); 293 294 $attr_str = preg_replace( 295 array( 296 '/(href="[^"]*)' . preg_quote(rawurlencode($pageid)) . '((?:\?[^#]*)?(?:#[^"]*)?")/', 297 '/(title="[^"]*)' . preg_quote($pageid) . '(")/' 298 ), 299 array( 300 "\${1}" . rawurlencode($filename) . '\2', 301 "\${1}" . hsc($filename) . '\2' 302 ), 303 $attr_str 304 ); 305 306 return '<a ' . $attr_str . '>' . $linktext . '</a>'; 307 } 308 309 /** 310 * Callback function for _replaceLinkURL (image) 311 */ 312 static function _replaceLinkURL_callback_img($matches) 313 { 314 list($imgtag, $imgtitle, $autofilled, $filename) = $matches; 315 316 if (!preg_match('/class="media(?:left|right|center)?[" ]/', $imgtag)) { 317 return $imgtag; 318 } 319 320 if ($autofilled) { 321 $imgtitle = $filename; 322 } 323 324 $filename = htmlspecialchars_decode($filename, ENT_QUOTES); 325 $pageid = cleanID($filename); 326 327 $imgtag = preg_replace( 328 array( 329 '/(src="[^"]*)' . preg_quote(rawurlencode($pageid)) . '((?:\?[^#]*)?(?:#[^"]*)?")/', 330 '/(alt|title)="[^"]*"/' 331 ), 332 array( 333 "\${1}" . rawurlencode($filename) . '\2', 334 '\1="' . $imgtitle . '"' 335 ), 336 $imgtag 337 ); 338 339 return $imgtag; 340 } 341 342 /** 343 * Exports configuration settings to $JSINFO 344 */ 345 function _exportToJSINFO(&$event) 346 { 347 global $JSINFO; 348 349 $JSINFO['plugin_preservefilenames'] = array( 350 'in_mediamanager' => true, 351 ); 352 } 353 354 /** 355 * Shows a list of media 356 */ 357 function _showMediaList(&$event) 358 { 359 global $NS; 360 global $AUTH; 361 global $JUMPTO; 362 363 if ($event->data['do'] !== 'filelist') { 364 return; 365 } 366 367 $event->preventDefault(); 368 369 ptln('<div id="media__content">'); 370 $this->_listMedia($NS, $AUTH, $JUMPTO); 371 ptln('</div>'); 372 } 373 374 /** 375 * Shows a list of media via ajax 376 */ 377 function _showMediaListAjax(&$event) 378 { 379 global $JUMPTO; 380 381 if ($event->data !== 'medialist_preservefilenames') { 382 return; 383 } 384 385 $event->preventDefault(); 386 387 require_once(DOKU_INC . 'inc/media.php'); 388 $ns = cleanID($_POST['ns']); 389 $auth = auth_quickaclcheck("$ns:*"); 390 391 $this->_listMedia($ns, $auth, $JUMPTO); 392 } 393 394 /** 395 * Outputs a list of files for mediamanager 396 * 397 * @see media_filelist() in inc/media.php 398 */ 399 function _listMedia($ns, $auth, $jumpto) 400 { 401 global $conf; 402 global $lang; 403 404 print '<h1 id="media__ns">:' . hsc($ns) . '</h1>' . NL; 405 406 if ($auth < AUTH_READ) { 407 print '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL; 408 } else { 409 media_uploadform($ns, $auth); 410 411 $dir = utf8_encodeFN(str_replace(':', '/', $ns)); 412 $data = array(); 413 search($data, $conf['mediadir'], 'search_media', 414 array('showmsg' => true, 'depth' => 1), $dir); 415 416 if (empty($data)) { 417 print '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL; 418 } else { 419 foreach ($data as $item) { 420 $filename = $this->_getOriginalFileName($item['id']); 421 422 if ($filename !== false) { 423 $item['file'] = utf8_encodeFN($filename); 424 } 425 426 media_printfile($item, $auth, $jumpto); 427 } 428 } 429 } 430 431 media_searchform($ns); 432 } 433 434 /** 435 * Returns original filename if exists 436 */ 437 function _getOriginalFileName($id) 438 { 439 $meta = unserialize(io_readFile(metaFN($id, '.filename'), false)); 440 return empty($meta['filename']) ? false : $this->common->_sanitizeFileName($meta['filename']); 441 } 442 443 /** 444 * Replaces the default snippet download handler 445 * 446 * NOTE: This method is needed to fix basename() bug in determining 447 * filename of the snippet. For more details please see 448 * common->_correctBasename(). 449 */ 450 function _replaceSnippetDownload(&$event) 451 { 452 global $ACT; 453 454 // $ACT is not clean, but in most cases this works fine 455 if ($ACT === 'export_code') $ACT = 'export_preservefilenames'; 456 } 457} 458