1<?php 2 3/** 4 * All output and handler function needed for the media management popup 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Andreas Gohr <andi@splitbrain.org> 8 */ 9 10use dokuwiki\Ui\MediaRevisions; 11use dokuwiki\Cache\CacheImageMod; 12use splitbrain\slika\Exception; 13use dokuwiki\PassHash; 14use dokuwiki\ChangeLog\MediaChangeLog; 15use dokuwiki\Extension\Event; 16use dokuwiki\File\MediaFile; 17use dokuwiki\Form\Form; 18use dokuwiki\HTTP\DokuHTTPClient; 19use dokuwiki\Logger; 20use dokuwiki\Subscriptions\MediaSubscriptionSender; 21use dokuwiki\Ui\Media\Display; 22use dokuwiki\Ui\Media\DisplayRow; 23use dokuwiki\Ui\Media\DisplayTile; 24use dokuwiki\Search\MetadataSearch; 25use dokuwiki\Ui\MediaDiff; 26use dokuwiki\Utf8\PhpString; 27use dokuwiki\Utf8\Sort; 28use splitbrain\slika\Slika; 29 30/** 31 * Lists pages which currently use a media file selected for deletion 32 * 33 * References uses the same visual as search results and share 34 * their CSS tags except pagenames won't be links. 35 * 36 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> 37 * 38 * @param array $data 39 * @param string $id 40 */ 41function media_filesinuse($data, $id) 42{ 43 global $lang; 44 echo '<h1>' . $lang['reference'] . ' <code>' . hsc(noNS($id)) . '</code></h1>'; 45 echo '<p>' . hsc($lang['ref_inuse']) . '</p>'; 46 47 $hidden = 0; //count of hits without read permission 48 foreach ($data as $row) { 49 if (auth_quickaclcheck($row) >= AUTH_READ && isVisiblePage($row)) { 50 echo '<div class="search_result">'; 51 echo '<span class="mediaref_ref">' . hsc($row) . '</span>'; 52 echo '</div>'; 53 } else $hidden++; 54 } 55 if ($hidden) { 56 echo '<div class="mediaref_hidden">' . $lang['ref_hidden'] . '</div>'; 57 } 58} 59 60/** 61 * Handles the saving of image meta data 62 * 63 * @author Andreas Gohr <andi@splitbrain.org> 64 * @author Kate Arzamastseva <pshns@ukr.net> 65 * 66 * @param string $id media id 67 * @param array $data 68 * @return false|string 69 */ 70function media_metasave($id, $data) 71{ 72 if (auth_quickaclcheck(mediaAclPath($id)) < AUTH_UPLOAD) return false; 73 if (!checkSecurityToken()) return false; 74 global $lang; 75 global $conf; 76 $src = mediaFN($id); 77 78 $meta = new JpegMeta($src); 79 $meta->_parseAll(); 80 81 foreach ($data as $key => $val) { 82 $val = trim($val); 83 if (empty($val)) { 84 $meta->deleteField($key); 85 } else { 86 $meta->setField($key, $val); 87 } 88 } 89 90 $old = @filemtime($src); 91 if (!file_exists(mediaFN($id, $old)) && file_exists($src)) { 92 // add old revision to the attic 93 media_saveOldRevision($id); 94 } 95 $filesize_old = filesize($src); 96 if ($meta->save()) { 97 if ($conf['fperm']) chmod($src, $conf['fperm']); 98 @clearstatcache(true, $src); 99 $new = @filemtime($src); 100 $filesize_new = filesize($src); 101 $sizechange = $filesize_new - $filesize_old; 102 103 // add a log entry to the media changelog 104 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, $lang['media_meta_edited'], '', null, $sizechange); 105 106 msg($lang['metasaveok'], 1); 107 return $id; 108 } else { 109 msg($lang['metasaveerr'], -1); 110 return false; 111 } 112} 113 114/** 115 * check if a media is external source 116 * 117 * @author Gerrit Uitslag <klapinklapin@gmail.com> 118 * 119 * @param string $id the media ID or URL 120 * @return bool 121 */ 122function media_isexternal($id) 123{ 124 if (preg_match('#^(?:https?|ftp)://#i', $id)) return true; 125 return false; 126} 127 128/** 129 * Check if a media item is public (eg, external URL or readable by @ALL) 130 * 131 * @author Andreas Gohr <andi@splitbrain.org> 132 * 133 * @param string $id the media ID or URL 134 * @return bool 135 */ 136function media_ispublic($id) 137{ 138 if (media_isexternal($id)) return true; 139 $id = cleanID($id); 140 if (auth_aclcheck(mediaAclPath($id), '', []) >= AUTH_READ) return true; 141 return false; 142} 143 144/** 145 * Display the form to edit image meta data 146 * 147 * @author Andreas Gohr <andi@splitbrain.org> 148 * @author Kate Arzamastseva <pshns@ukr.net> 149 * 150 * @param string $id media id 151 * @param int $auth permission level 152 * @return bool 153 */ 154function media_metaform($id, $auth) 155{ 156 global $lang; 157 158 if ($auth < AUTH_UPLOAD) { 159 echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . DOKU_LF; 160 return false; 161 } 162 163 // load the field descriptions 164 static $fields = null; 165 if ($fields === null) { 166 $config_files = getConfigFiles('mediameta'); 167 foreach ($config_files as $config_file) { 168 if (file_exists($config_file)) include($config_file); 169 } 170 } 171 172 $src = mediaFN($id); 173 174 // output 175 $form = new Form([ 176 'action' => media_managerURL(['tab_details' => 'view'], '&'), 177 'class' => 'meta' 178 ]); 179 $form->addTagOpen('div')->addClass('no'); 180 $form->setHiddenField('img', $id); 181 $form->setHiddenField('mediado', 'save'); 182 foreach ($fields as $key => $field) { 183 // get current value 184 if (empty($field[0])) continue; 185 $tags = [$field[0]]; 186 if (isset($field[3]) && is_array($field[3])) $tags = array_merge($tags, $field[3]); 187 $value = tpl_img_getTag($tags, '', $src); 188 $value = cleanText($value); 189 190 // prepare attributes 191 $p = [ 192 'class' => 'edit', 193 'id' => 'meta__' . $key, 194 'name' => 'meta[' . $field[0] . ']' 195 ]; 196 197 $form->addTagOpen('div')->addClass('row'); 198 if ($field[2] == 'text') { 199 $form->addTextInput( 200 $p['name'], 201 ($lang[$field[1]] ?: $field[1] . ':') 202 )->id($p['id'])->addClass($p['class'])->val($value); 203 } else { 204 $form->addTextarea($p['name'], $lang[$field[1]])->id($p['id']) 205 ->val(formText($value)) 206 ->addClass($p['class']) 207 ->attr('rows', '6')->attr('cols', '50'); 208 } 209 $form->addTagClose('div'); 210 } 211 $form->addTagOpen('div')->addClass('buttons'); 212 $form->addButton('mediado[save]', $lang['btn_save'])->attr('type', 'submit') 213 ->attrs(['accesskey' => 's']); 214 $form->addTagClose('div'); 215 216 $form->addTagClose('div'); 217 echo $form->toHTML(); 218 return true; 219} 220 221/** 222 * Convenience function to check if a media file is still in use 223 * 224 * @author Michael Klier <chi@chimeric.de> 225 * 226 * @param string $id media id 227 * @return array|bool 228 */ 229function media_inuse($id) 230{ 231 global $conf; 232 233 if ($conf['refcheck']) { 234 $mediareferences = (new MetadataSearch())->mediause($id, true); 235 if ($mediareferences === []) { 236 return false; 237 } else { 238 return $mediareferences; 239 } 240 } else { 241 return false; 242 } 243} 244 245/** 246 * Handles media file deletions 247 * 248 * If configured, checks for media references before deletion 249 * 250 * @author Andreas Gohr <andi@splitbrain.org> 251 * 252 * @param string $id media id 253 * @param int $auth no longer used 254 * @return int One of: 0, 255 * DOKU_MEDIA_DELETED, 256 * DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS, 257 * DOKU_MEDIA_NOT_AUTH, 258 * DOKU_MEDIA_INUSE 259 */ 260function media_delete($id, $auth) 261{ 262 global $lang; 263 $auth = auth_quickaclcheck(mediaAclPath($id)); 264 if ($auth < AUTH_DELETE) return DOKU_MEDIA_NOT_AUTH; 265 if (media_inuse($id)) return DOKU_MEDIA_INUSE; 266 267 $file = mediaFN($id); 268 269 // trigger an event - MEDIA_DELETE_FILE 270 $data = []; 271 $data['id'] = $id; 272 $data['name'] = PhpString::basename($file); 273 $data['path'] = $file; 274 $data['size'] = (file_exists($file)) ? filesize($file) : 0; 275 276 $data['unl'] = false; 277 $data['del'] = false; 278 $evt = new Event('MEDIA_DELETE_FILE', $data); 279 if ($evt->advise_before()) { 280 $old = @filemtime($file); 281 if (!file_exists(mediaFN($id, $old)) && file_exists($file)) { 282 // add old revision to the attic 283 media_saveOldRevision($id); 284 } 285 286 $data['unl'] = @unlink($file); 287 if ($data['unl']) { 288 $sizechange = 0 - $data['size']; 289 addMediaLogEntry(time(), $id, DOKU_CHANGE_TYPE_DELETE, $lang['deleted'], '', null, $sizechange); 290 291 $data['del'] = io_sweepNS($id, 'mediadir'); 292 } 293 } 294 $evt->advise_after(); 295 unset($evt); 296 297 if ($data['unl'] && $data['del']) { 298 return DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS; 299 } 300 301 return $data['unl'] ? DOKU_MEDIA_DELETED : 0; 302} 303 304/** 305 * Handle file uploads via XMLHttpRequest 306 * 307 * @param string $ns target namespace 308 * @param null|int $auth deprecated and ignored, the ACL is re-checked against 309 * the resolved target id 310 * @return false|string false on error, id of the new file on success 311 */ 312function media_upload_xhr($ns, $auth = null) 313{ 314 if (!checkSecurityToken()) return false; 315 global $INPUT; 316 317 $id = $INPUT->get->str('qqfile'); 318 [$ext, $mime] = mimetype($id); 319 $input = fopen("php://input", "r"); 320 if (!($tmp = io_mktmpdir())) return false; 321 $path = $tmp . '/' . md5($id); 322 $target = fopen($path, "w"); 323 $realSize = stream_copy_to_stream($input, $target); 324 fclose($target); 325 fclose($input); 326 if ($INPUT->server->has('CONTENT_LENGTH') && ($realSize != $INPUT->server->int('CONTENT_LENGTH'))) { 327 unlink($path); 328 return false; 329 } 330 331 $target = cleanID($ns . ':' . $id); 332 $res = media_save( 333 ['name' => $path, 'mime' => $mime, 'ext' => $ext], 334 $target, 335 ($INPUT->get->str('ow') == 'true'), 336 auth_quickaclcheck(mediaAclPath($target)), 337 'copy' 338 ); 339 unlink($path); 340 if ($tmp) io_rmdir($tmp, true); 341 if (is_array($res)) { 342 msg($res[0], $res[1]); 343 return false; 344 } 345 return $res; 346} 347 348/** 349 * Handles media file uploads 350 * 351 * @author Andreas Gohr <andi@splitbrain.org> 352 * @author Michael Klier <chi@chimeric.de> 353 * 354 * @param string $ns target namespace 355 * @param null|int $auth deprecated and ignored, the ACL is re-checked 356 * against the resolved target id 357 * @param bool|array $file $_FILES member, $_FILES['upload'] if false 358 * @return false|string false on error, id of the new file on success 359 */ 360function media_upload($ns, $auth = null, $file = false) 361{ 362 if (!checkSecurityToken()) return false; 363 global $lang; 364 global $INPUT; 365 366 // get file and id 367 $id = $INPUT->post->str('mediaid'); 368 if (!$file) $file = $_FILES['upload']; 369 if (empty($id)) $id = $file['name']; 370 371 // check for errors (messages are done in lib/exe/mediamanager.php) 372 if ($file['error']) return false; 373 374 // check extensions 375 [$fext, $fmime] = mimetype($file['name']); 376 [$iext, $imime] = mimetype($id); 377 if ($fext && !$iext) { 378 // no extension specified in id - read original one 379 $id .= '.' . $fext; 380 $imime = $fmime; 381 } elseif ($fext && $fext != $iext) { 382 // extension was changed, print warning 383 msg(sprintf($lang['mediaextchange'], $fext, $iext)); 384 } 385 386 $target = cleanID($ns . ':' . $id); 387 $res = media_save( 388 [ 389 'name' => $file['tmp_name'], 390 'mime' => $imime, 391 'ext' => $iext 392 ], 393 $target, 394 $INPUT->post->bool('ow'), 395 auth_quickaclcheck(mediaAclPath($target)), 396 'copy_uploaded_file' 397 ); 398 if (is_array($res)) { 399 msg($res[0], $res[1]); 400 return false; 401 } 402 return $res; 403} 404 405/** 406 * An alternative to move_uploaded_file that copies 407 * 408 * Using copy, makes sure any setgid bits on the media directory are honored 409 * 410 * @see move_uploaded_file() 411 * 412 * @param string $from 413 * @param string $to 414 * @return bool 415 */ 416function copy_uploaded_file($from, $to) 417{ 418 if (!is_uploaded_file($from)) return false; 419 $ok = copy($from, $to); 420 @unlink($from); 421 return $ok; 422} 423 424/** 425 * This generates an action event and delegates to _media_upload_action(). 426 * Action plugins are allowed to pre/postprocess the uploaded file. 427 * (The triggered event is preventable.) 428 * 429 * Event data: 430 * $data[0] fn_tmp: the temporary file name (read from $_FILES) 431 * $data[1] fn: the file name of the uploaded file 432 * $data[2] id: the future directory id of the uploaded file 433 * $data[3] imime: the mimetype of the uploaded file 434 * $data[4] overwrite: if an existing file is going to be overwritten 435 * $data[5] move: name of function that performs move/copy/.. 436 * 437 * @triggers MEDIA_UPLOAD_FINISH 438 * 439 * @param array $file 440 * @param string $id media id 441 * @param bool $ow overwrite? 442 * @param int $auth permission level 443 * @param string $move name of functions that performs move/copy/.. 444 * @return false|array|string 445 */ 446function media_save($file, $id, $ow, $auth, $move) 447{ 448 if ($auth < AUTH_UPLOAD) { 449 return ["You don't have permissions to upload files.", -1]; 450 } 451 452 if (!isset($file['mime']) || !isset($file['ext'])) { 453 [$ext, $mime] = mimetype($id); 454 if (!isset($file['mime'])) { 455 $file['mime'] = $mime; 456 } 457 if (!isset($file['ext'])) { 458 $file['ext'] = $ext; 459 } 460 } 461 462 global $lang, $conf; 463 464 // get filename 465 $id = cleanID($id); 466 $fn = mediaFN($id); 467 468 // get filetype regexp 469 $types = array_keys(getMimeTypes()); 470 $types = array_map( 471 static fn($q) => preg_quote($q, "/"), 472 $types 473 ); 474 $regex = implode('|', $types); 475 476 // because a temp file was created already 477 if (!preg_match('/\.(' . $regex . ')$/i', $fn)) { 478 return [$lang['uploadwrong'], -1]; 479 } 480 481 //check for overwrite 482 $overwrite = file_exists($fn); 483 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE); 484 if ($overwrite && (!$ow || $auth < $auth_ow)) { 485 return [$lang['uploadexist'], 0]; 486 } 487 // check for valid content 488 $ok = media_contentcheck($file['name'], $file['mime']); 489 if ($ok == -1) { 490 return [sprintf($lang['uploadbadcontent'], '.' . $file['ext']), -1]; 491 } elseif ($ok == -2) { 492 return [$lang['uploadspam'], -1]; 493 } elseif ($ok == -3) { 494 return [$lang['uploadxss'], -1]; 495 } 496 497 // prepare event data 498 $data = []; 499 $data[0] = $file['name']; 500 $data[1] = $fn; 501 $data[2] = $id; 502 $data[3] = $file['mime']; 503 $data[4] = $overwrite; 504 $data[5] = $move; 505 506 // trigger event 507 return Event::createAndTrigger('MEDIA_UPLOAD_FINISH', $data, '_media_upload_action', true); 508} 509 510/** 511 * Callback adapter for media_upload_finish() triggered by MEDIA_UPLOAD_FINISH 512 * 513 * @author Michael Klier <chi@chimeric.de> 514 * 515 * @param array $data event data 516 * @return false|array|string 517 */ 518function _media_upload_action($data) 519{ 520 // fixme do further sanity tests of given data? 521 if (is_array($data) && count($data) === 6) { 522 return media_upload_finish($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]); 523 } else { 524 return false; //callback error 525 } 526} 527 528/** 529 * Saves an uploaded media file 530 * 531 * @author Andreas Gohr <andi@splitbrain.org> 532 * @author Michael Klier <chi@chimeric.de> 533 * @author Kate Arzamastseva <pshns@ukr.net> 534 * 535 * @param string $fn_tmp 536 * @param string $fn 537 * @param string $id media id 538 * @param string $imime mime type 539 * @param bool $overwrite overwrite existing? 540 * @param string $move function name 541 * @return array|string 542 */ 543function media_upload_finish($fn_tmp, $fn, $id, $imime, $overwrite, $move = 'move_uploaded_file') 544{ 545 global $conf; 546 global $lang; 547 global $REV; 548 549 $old = @filemtime($fn); 550 if (!file_exists(mediaFN($id, $old)) && file_exists($fn)) { 551 // add old revision to the attic if missing 552 media_saveOldRevision($id); 553 } 554 555 // prepare directory 556 io_createNamespace($id, 'media'); 557 558 $filesize_old = file_exists($fn) ? filesize($fn) : 0; 559 560 if ($move($fn_tmp, $fn)) { 561 @clearstatcache(true, $fn); 562 $new = @filemtime($fn); 563 // Set the correct permission here. 564 // Always chmod media because they may be saved with different permissions than expected from the php umask. 565 // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.) 566 chmod($fn, $conf['fmode']); 567 msg($lang['uploadsucc'], 1); 568 media_notify($id, $fn, $imime, $old, $new); 569 // add a log entry to the media changelog 570 $filesize_new = filesize($fn); 571 $sizechange = $filesize_new - $filesize_old; 572 if ($REV) { 573 addMediaLogEntry( 574 $new, 575 $id, 576 DOKU_CHANGE_TYPE_REVERT, 577 sprintf($lang['restored'], dformat($REV)), 578 $REV, 579 null, 580 $sizechange 581 ); 582 } elseif ($overwrite) { 583 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange); 584 } else { 585 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange); 586 } 587 return $id; 588 } else { 589 return [$lang['uploadfail'], -1]; 590 } 591} 592 593/** 594 * Moves the current version of media file to the media_attic 595 * directory 596 * 597 * @author Kate Arzamastseva <pshns@ukr.net> 598 * 599 * @param string $id 600 * @return int - revision date 601 */ 602function media_saveOldRevision($id) 603{ 604 global $conf, $lang; 605 606 $oldf = mediaFN($id); 607 if (!file_exists($oldf)) return ''; 608 $date = filemtime($oldf); 609 if (!$conf['mediarevisions']) return $date; 610 611 $medialog = new MediaChangeLog($id); 612 if (!$medialog->getRevisionInfo($date)) { 613 // there was an external edit, 614 // there is no log entry for current version of file 615 $sizechange = filesize($oldf); 616 if (!file_exists(mediaMetaFN($id, '.changes'))) { 617 addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange); 618 } else { 619 $oldRev = $medialog->getRevisions(-1, 1); // from changelog 620 $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]); 621 $filesize_old = filesize(mediaFN($id, $oldRev)); 622 $sizechange -= $filesize_old; 623 624 addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange); 625 } 626 } 627 628 $newf = mediaFN($id, $date); 629 io_makeFileDir($newf); 630 if (copy($oldf, $newf)) { 631 // Set the correct permission here. 632 // Always chmod media because they may be saved with different permissions than expected from the php umask. 633 // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.) 634 chmod($newf, $conf['fmode']); 635 } 636 return $date; 637} 638 639/** 640 * This function checks if the uploaded content is really what the 641 * mimetype says it is. We also do spam checking for text types here. 642 * 643 * We need to do this stuff because we can not rely on the browser 644 * to do this check correctly. Yes, IE is broken as usual. 645 * 646 * @author Andreas Gohr <andi@splitbrain.org> 647 * @link http://www.splitbrain.org/blog/2007-02/12-internet_explorer_facilitates_cross_site_scripting 648 * @fixme check all 26 magic IE filetypes here? 649 * 650 * @param string $file path to file 651 * @param string $mime mimetype 652 * @return int 653 */ 654function media_contentcheck($file, $mime) 655{ 656 global $conf; 657 if ($conf['iexssprotect']) { 658 $fh = @fopen($file, 'rb'); 659 if ($fh) { 660 $bytes = fread($fh, 256); 661 fclose($fh); 662 if (preg_match('/<(script|a|img|html|body|iframe)[\s>]/i', $bytes)) { 663 return -3; //XSS: possibly malicious content 664 } 665 } 666 } 667 if (str_starts_with($mime, 'image/')) { 668 $info = @getimagesize($file); 669 if ($mime == 'image/gif' && $info[2] != 1) { 670 return -1; // uploaded content did not match the file extension 671 } elseif ($mime == 'image/jpeg' && $info[2] != 2) { 672 return -1; 673 } elseif ($mime == 'image/png' && $info[2] != 3) { 674 return -1; 675 } 676 # fixme maybe check other images types as well 677 } elseif (str_starts_with($mime, 'text/')) { 678 global $TEXT; 679 $TEXT = io_readFile($file); 680 if (checkwordblock()) { 681 return -2; //blocked by the spam blacklist 682 } 683 } 684 return 0; 685} 686 687/** 688 * Send a notify mail on uploads 689 * 690 * @author Andreas Gohr <andi@splitbrain.org> 691 * 692 * @param string $id media id 693 * @param string $file path to file 694 * @param string $mime mime type 695 * @param bool|int $old_rev revision timestamp or false 696 */ 697function media_notify($id, $file, $mime, $old_rev = false, $current_rev = false) 698{ 699 global $conf; 700 if (empty($conf['notify'])) return; //notify enabled? 701 702 $subscription = new MediaSubscriptionSender(); 703 $subscription->sendMediaDiff($conf['notify'], 'uploadmail', $id, $old_rev, $current_rev); 704} 705 706/** 707 * List all files in a given Media namespace 708 * 709 * @param string $ns namespace 710 * @param null|int $auth permission level 711 * @param string $jump id 712 * @param bool $fullscreenview 713 * @param bool|string $sort sorting order, false skips sorting 714 */ 715function media_filelist($ns, $auth = null, $jump = '', $fullscreenview = false, $sort = false) 716{ 717 global $conf; 718 global $lang; 719 $ns = cleanID($ns); 720 721 // check auth our self if not given (needed for ajax calls) 722 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 723 724 if (!$fullscreenview) echo '<h1 id="media__ns">:' . hsc($ns) . '</h1>' . NL; 725 726 if ($auth < AUTH_READ) { 727 // FIXME: print permission warning here instead? 728 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL; 729 } else { 730 if (!$fullscreenview) { 731 media_uploadform($ns, $auth); 732 media_searchform($ns); 733 } 734 735 $dir = utf8_encodeFN(str_replace(':', '/', $ns)); 736 $data = []; 737 search( 738 $data, 739 $conf['mediadir'], 740 'search_mediafiles', 741 ['showmsg' => true, 'depth' => 1], 742 $dir, 743 1, 744 $sort 745 ); 746 747 if ($data === []) { 748 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL; 749 } else { 750 if ($fullscreenview) { 751 echo '<ul class="' . _media_get_list_type() . '">'; 752 } 753 foreach ($data as $item) { 754 if (!$fullscreenview) { 755 //FIXME old call: media_printfile($item,$auth,$jump); 756 $display = new DisplayRow($item); 757 $display->scrollIntoView($jump == $item->getID()); 758 $display->show(); 759 } else { 760 //FIXME old call: media_printfile_thumbs($item,$auth,$jump); 761 echo '<li>'; 762 $display = new DisplayTile($item); 763 $display->scrollIntoView($jump == $item->getID()); 764 $display->show(); 765 echo '</li>'; 766 } 767 } 768 if ($fullscreenview) echo '</ul>' . NL; 769 } 770 } 771} 772 773/** 774 * Prints tabs for files list actions 775 * 776 * @author Kate Arzamastseva <pshns@ukr.net> 777 * @author Adrian Lang <mail@adrianlang.de> 778 * 779 * @param string $selected_tab - opened tab 780 */ 781 782function media_tabs_files($selected_tab = '') 783{ 784 global $lang; 785 $tabs = []; 786 foreach ( 787 [ 788 'files' => 'mediaselect', 789 'upload' => 'media_uploadtab', 790 'search' => 'media_searchtab' 791 ] as $tab => $caption 792 ) { 793 $tabs[$tab] = [ 794 'href' => media_managerURL(['tab_files' => $tab], '&'), 795 'caption' => $lang[$caption] 796 ]; 797 } 798 799 html_tabs($tabs, $selected_tab); 800} 801 802/** 803 * Prints tabs for files details actions 804 * 805 * @author Kate Arzamastseva <pshns@ukr.net> 806 * @param string $image filename of the current image 807 * @param string $selected_tab opened tab 808 */ 809function media_tabs_details($image, $selected_tab = '') 810{ 811 global $lang, $conf; 812 813 $tabs = []; 814 $tabs['view'] = [ 815 'href' => media_managerURL(['tab_details' => 'view'], '&'), 816 'caption' => $lang['media_viewtab'] 817 ]; 818 819 [, $mime] = mimetype($image); 820 if ($mime == 'image/jpeg' && file_exists(mediaFN($image))) { 821 $tabs['edit'] = [ 822 'href' => media_managerURL(['tab_details' => 'edit'], '&'), 823 'caption' => $lang['media_edittab'] 824 ]; 825 } 826 if ($conf['mediarevisions']) { 827 $tabs['history'] = [ 828 'href' => media_managerURL(['tab_details' => 'history'], '&'), 829 'caption' => $lang['media_historytab'] 830 ]; 831 } 832 833 html_tabs($tabs, $selected_tab); 834} 835 836/** 837 * Prints options for the tab that displays a list of all files 838 * 839 * @author Kate Arzamastseva <pshns@ukr.net> 840 */ 841function media_tab_files_options() 842{ 843 global $lang; 844 global $INPUT; 845 global $ID; 846 847 $form = new Form([ 848 'method' => 'get', 849 'action' => wl($ID), 850 'class' => 'options' 851 ]); 852 $form->addTagOpen('div')->addClass('no'); 853 $form->setHiddenField('sectok', null); 854 $media_manager_params = media_managerURL([], '', false, true); 855 foreach ($media_manager_params as $pKey => $pVal) { 856 $form->setHiddenField($pKey, $pVal); 857 } 858 if ($INPUT->has('q')) { 859 $form->setHiddenField('q', $INPUT->str('q')); 860 } 861 $form->addHTML('<ul>' . NL); 862 foreach ( 863 [ 864 'list' => ['listType', ['thumbs', 'rows']], 865 'sort' => ['sortBy', ['name', 'date']] 866 ] as $group => $content 867 ) { 868 $checked = "_media_get_{$group}_type"; 869 $checked = $checked(); 870 871 $form->addHTML('<li class="' . $content[0] . '">'); 872 foreach ($content[1] as $option) { 873 $attrs = []; 874 if ($checked == $option) { 875 $attrs['checked'] = 'checked'; 876 } 877 $radio = $form->addRadioButton( 878 $group . '_dwmedia', 879 $lang['media_' . $group . '_' . $option] 880 )->val($option)->id($content[0] . '__' . $option)->addClass($option); 881 $radio->attrs($attrs); 882 } 883 $form->addHTML('</li>' . NL); 884 } 885 $form->addHTML('<li>'); 886 $form->addButton('', $lang['btn_apply'])->attr('type', 'submit'); 887 $form->addHTML('</li>' . NL); 888 $form->addHTML('</ul>' . NL); 889 $form->addTagClose('div'); 890 echo $form->toHTML(); 891} 892 893/** 894 * Returns type of sorting for the list of files in media manager 895 * 896 * @author Kate Arzamastseva <pshns@ukr.net> 897 * 898 * @return string - sort type 899 */ 900function _media_get_sort_type() 901{ 902 return _media_get_display_param('sort', ['default' => 'name', 'date']); 903} 904 905/** 906 * Returns type of listing for the list of files in media manager 907 * 908 * @author Kate Arzamastseva <pshns@ukr.net> 909 * 910 * @return string - list type 911 */ 912function _media_get_list_type() 913{ 914 return _media_get_display_param('list', ['default' => 'thumbs', 'rows']); 915} 916 917/** 918 * Get display parameters 919 * 920 * @param string $param name of parameter 921 * @param array $values allowed values, where default value has index key 'default' 922 * @return string the parameter value 923 */ 924function _media_get_display_param($param, $values) 925{ 926 global $INPUT; 927 if (in_array($INPUT->str($param), $values)) { 928 // FIXME: Set cookie 929 return $INPUT->str($param); 930 } else { 931 $val = get_doku_pref($param, $values['default']); 932 if (!in_array($val, $values)) { 933 $val = $values['default']; 934 } 935 return $val; 936 } 937} 938 939/** 940 * Prints tab that displays a list of all files 941 * 942 * @author Kate Arzamastseva <pshns@ukr.net> 943 * 944 * @param string $ns 945 * @param null|int $auth permission level 946 * @param string $jump item id 947 */ 948function media_tab_files($ns, $auth = null, $jump = '') 949{ 950 global $lang; 951 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 952 953 if ($auth < AUTH_READ) { 954 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL; 955 } else { 956 media_filelist($ns, $auth, $jump, true, _media_get_sort_type()); 957 } 958} 959 960/** 961 * Prints tab that displays uploading form 962 * 963 * @author Kate Arzamastseva <pshns@ukr.net> 964 * 965 * @param string $ns 966 * @param null|int $auth permission level 967 * @param string $jump item id 968 */ 969function media_tab_upload($ns, $auth = null, $jump = '') 970{ 971 global $lang; 972 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 973 974 echo '<div class="upload">' . NL; 975 if ($auth >= AUTH_UPLOAD) { 976 echo '<p>' . $lang['mediaupload'] . '</p>'; 977 } 978 media_uploadform($ns, $auth, true); 979 echo '</div>' . NL; 980} 981 982/** 983 * Prints tab that displays search form 984 * 985 * @author Kate Arzamastseva <pshns@ukr.net> 986 * 987 * @param string $ns 988 * @param null|int $auth permission level 989 */ 990function media_tab_search($ns, $auth = null) 991{ 992 global $INPUT; 993 994 $do = $INPUT->str('mediado'); 995 $query = $INPUT->str('q'); 996 echo '<div class="search">' . NL; 997 998 media_searchform($ns, $query, true); 999 if ($do == 'searchlist' || $query) { 1000 media_searchlist($query, $ns, $auth, true, _media_get_sort_type()); 1001 } 1002 echo '</div>' . NL; 1003} 1004 1005/** 1006 * Prints tab that displays mediafile details 1007 * 1008 * @author Kate Arzamastseva <pshns@ukr.net> 1009 * 1010 * @param string $image media id 1011 * @param string $ns 1012 * @param null|int $auth permission level 1013 * @param string|int $rev revision timestamp or empty string 1014 */ 1015function media_tab_view($image, $ns, $auth = null, $rev = '') 1016{ 1017 global $lang; 1018 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 1019 1020 if ($image && $auth >= AUTH_READ) { 1021 $mf = new MediaFile($image, $rev); 1022 echo (new Display($mf))->getDetailHtml(); 1023 media_preview_buttons($image, $auth, $rev); 1024 media_details($image, $auth, $rev, $mf->getMeta()); 1025 } else { 1026 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL; 1027 } 1028} 1029 1030/** 1031 * Prints tab that displays form for editing mediafile metadata 1032 * 1033 * @author Kate Arzamastseva <pshns@ukr.net> 1034 * 1035 * @param string $image media id 1036 * @param string $ns 1037 * @param null|int $auth permission level 1038 */ 1039function media_tab_edit($image, $ns, $auth = null) 1040{ 1041 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 1042 1043 if ($image) { 1044 [, $mime] = mimetype($image); 1045 if ($mime == 'image/jpeg') media_metaform($image, $auth); 1046 } 1047} 1048 1049/** 1050 * Prints tab that displays mediafile revisions 1051 * 1052 * @author Kate Arzamastseva <pshns@ukr.net> 1053 * 1054 * @param string $image media id 1055 * @param string $ns 1056 * @param null|int $auth permission level 1057 */ 1058function media_tab_history($image, $ns, $auth = null) 1059{ 1060 global $lang; 1061 global $INPUT; 1062 1063 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*"); 1064 $do = $INPUT->str('mediado'); 1065 1066 if ($auth >= AUTH_READ && $image) { 1067 if ($do == 'diff') { 1068 (new MediaDiff($image))->show(); //media_diff($image, $ns, $auth); 1069 } else { 1070 $first = $INPUT->int('first', -1); 1071 (new MediaRevisions($image))->show($first); 1072 } 1073 } else { 1074 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL; 1075 } 1076} 1077 1078/** 1079 * Prints mediafile action buttons 1080 * 1081 * @author Kate Arzamastseva <pshns@ukr.net> 1082 * 1083 * @param string $image media id 1084 * @param int $auth permission level 1085 * @param int|string $rev revision timestamp, or empty string 1086 */ 1087function media_preview_buttons($image, $auth, $rev = '') 1088{ 1089 global $lang, $conf; 1090 1091 echo '<ul class="actions">'; 1092 1093 if ($auth >= AUTH_DELETE && !$rev && file_exists(mediaFN($image))) { 1094 // delete button 1095 $form = new Form([ 1096 'id' => 'mediamanager__btn_delete', 1097 'action' => media_managerURL(['delete' => $image], '&'), 1098 ]); 1099 $form->addTagOpen('div')->addClass('no'); 1100 $form->addButton('', $lang['btn_delete'])->attr('type', 'submit'); 1101 $form->addTagClose('div'); 1102 echo '<li>'; 1103 echo $form->toHTML(); 1104 echo '</li>'; 1105 } 1106 1107 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE); 1108 if ($auth >= $auth_ow && !$rev) { 1109 // upload new version button 1110 $form = new Form([ 1111 'id' => 'mediamanager__btn_update', 1112 'action' => media_managerURL(['image' => $image, 'mediado' => 'update'], '&'), 1113 ]); 1114 $form->addTagOpen('div')->addClass('no'); 1115 $form->addButton('', $lang['media_update'])->attr('type', 'submit'); 1116 $form->addTagClose('div'); 1117 echo '<li>'; 1118 echo $form->toHTML(); 1119 echo '</li>'; 1120 } 1121 1122 if ($auth >= AUTH_UPLOAD && $rev && $conf['mediarevisions'] && file_exists(mediaFN($image, $rev))) { 1123 // restore button 1124 $form = new Form([ 1125 'id' => 'mediamanager__btn_restore', 1126 'action' => media_managerURL(['image' => $image], '&'), 1127 ]); 1128 $form->addTagOpen('div')->addClass('no'); 1129 $form->setHiddenField('mediado', 'restore'); 1130 $form->setHiddenField('rev', $rev); 1131 $form->addButton('', $lang['media_restore'])->attr('type', 'submit'); 1132 $form->addTagClose('div'); 1133 echo '<li>'; 1134 echo $form->toHTML(); 1135 echo '</li>'; 1136 } 1137 1138 echo '</ul>'; 1139} 1140 1141/** 1142 * Returns the requested EXIF/IPTC tag from the image meta 1143 * 1144 * @author Kate Arzamastseva <pshns@ukr.net> 1145 * 1146 * @param array $tags array with tags, first existing is returned 1147 * @param JpegMeta $meta 1148 * @param string $alt alternative value 1149 * @return string 1150 */ 1151function media_getTag($tags, $meta = false, $alt = '') 1152{ 1153 if (!$meta) return $alt; 1154 $info = $meta->getField($tags); 1155 if (!$info) return $alt; 1156 return $info; 1157} 1158 1159/** 1160 * Returns mediafile tags 1161 * 1162 * @author Kate Arzamastseva <pshns@ukr.net> 1163 * 1164 * @param JpegMeta $meta 1165 * @return array list of tags of the mediafile 1166 */ 1167function media_file_tags($meta) 1168{ 1169 // load the field descriptions 1170 static $fields = null; 1171 if (is_null($fields)) { 1172 $config_files = getConfigFiles('mediameta'); 1173 foreach ($config_files as $config_file) { 1174 if (file_exists($config_file)) include($config_file); 1175 } 1176 } 1177 1178 $tags = []; 1179 1180 foreach ($fields as $tag) { 1181 $t = []; 1182 if (!empty($tag[0])) $t = [$tag[0]]; 1183 if (isset($tag[3]) && is_array($tag[3])) $t = array_merge($t, $tag[3]); 1184 $value = media_getTag($t, $meta); 1185 $tags[] = ['tag' => $tag, 'value' => $value]; 1186 } 1187 1188 return $tags; 1189} 1190 1191/** 1192 * Prints mediafile tags 1193 * 1194 * @author Kate Arzamastseva <pshns@ukr.net> 1195 * 1196 * @param string $image image id 1197 * @param int $auth permission level 1198 * @param string|int $rev revision timestamp, or empty string 1199 * @param bool|JpegMeta $meta image object, or create one if false 1200 */ 1201function media_details($image, $auth, $rev = '', $meta = false) 1202{ 1203 global $lang; 1204 1205 if (!$meta) $meta = new JpegMeta(mediaFN($image, $rev)); 1206 $tags = media_file_tags($meta); 1207 1208 echo '<dl>' . NL; 1209 foreach ($tags as $tag) { 1210 if ($tag['value']) { 1211 $value = cleanText($tag['value']); 1212 echo '<dt>' . $lang[$tag['tag'][1]] . '</dt><dd>'; 1213 if ($tag['tag'][2] == 'date') { 1214 echo dformat($value); 1215 } else { 1216 echo hsc($value); 1217 } 1218 echo '</dd>' . NL; 1219 } 1220 } 1221 echo '</dl>' . NL; 1222 echo '<dl>' . NL; 1223 echo '<dt>' . $lang['reference'] . ':</dt>'; 1224 $media_usage = (new MetadataSearch())->mediause($image, true); 1225 if ($media_usage !== []) { 1226 foreach ($media_usage as $path) { 1227 echo '<dd>' . html_wikilink($path) . '</dd>'; 1228 } 1229 } else { 1230 echo '<dd>' . $lang['nothingfound'] . '</dd>'; 1231 } 1232 echo '</dl>' . NL; 1233} 1234 1235/** 1236 * Shows difference between two revisions of file 1237 * 1238 * @author Kate Arzamastseva <pshns@ukr.net> 1239 * 1240 * @param string $image image id 1241 * @param string $ns 1242 * @param int $auth permission level 1243 * @param bool $fromajax 1244 * 1245 * @deprecated 2020-12-31 1246 */ 1247function media_diff($image, $ns, $auth, $fromajax = false) 1248{ 1249 dbg_deprecated('see ' . MediaDiff::class . '::show()'); 1250} 1251 1252/** 1253 * Callback for media file diff 1254 * 1255 * @param array $data event data 1256 * 1257 * @deprecated 2020-12-31 1258 */ 1259function _media_file_diff($data) 1260{ 1261 dbg_deprecated('see ' . MediaDiff::class . '::show()'); 1262} 1263 1264/** 1265 * Shows difference between two revisions of image 1266 * 1267 * @author Kate Arzamastseva <pshns@ukr.net> 1268 * 1269 * @param string $image 1270 * @param string|int $l_rev revision timestamp, or empty string 1271 * @param string|int $r_rev revision timestamp, or empty string 1272 * @param string $ns 1273 * @param int $auth permission level 1274 * @param bool $fromajax 1275 * @deprecated 2020-12-31 1276 */ 1277function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax) 1278{ 1279 dbg_deprecated('see ' . MediaDiff::class . '::showFileDiff()'); 1280} 1281 1282/** 1283 * Prints two images side by side 1284 * and slider 1285 * 1286 * @author Kate Arzamastseva <pshns@ukr.net> 1287 * 1288 * @param string $image image id 1289 * @param int $l_rev revision timestamp, or empty string 1290 * @param int $r_rev revision timestamp, or empty string 1291 * @param array $l_size array with width and height 1292 * @param array $r_size array with width and height 1293 * @param string $type 1294 * @deprecated 2020-12-31 1295 */ 1296function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type) 1297{ 1298 dbg_deprecated('see ' . MediaDiff::class . '::showImageDiff()'); 1299} 1300 1301/** 1302 * Restores an old revision of a media file 1303 * 1304 * @param string $image media id 1305 * @param int $rev revision timestamp or empty string 1306 * @return false|string the file's id, or false on error 1307 * 1308 * @author Kate Arzamastseva <pshns@ukr.net> 1309 */ 1310function media_restore($image, $rev) 1311{ 1312 global $conf; 1313 if (!$conf['mediarevisions']) return false; 1314 $image = cleanID($image); 1315 if (auth_quickaclcheck(mediaAclPath($image)) < AUTH_UPLOAD) return false; 1316 $removed = (!file_exists(mediaFN($image)) && file_exists(mediaMetaFN($image, '.changes'))); 1317 if (!$image || (!file_exists(mediaFN($image)) && !$removed)) return false; 1318 if (!$rev || !file_exists(mediaFN($image, $rev))) return false; 1319 [, $imime, ] = mimetype($image); 1320 $res = media_upload_finish( 1321 mediaFN($image, $rev), 1322 mediaFN($image), 1323 $image, 1324 $imime, 1325 true, 1326 'copy' 1327 ); 1328 if (is_array($res)) { 1329 msg($res[0], $res[1]); 1330 return false; 1331 } 1332 return $res; 1333} 1334 1335/** 1336 * List all files found by the search request 1337 * 1338 * @author Tobias Sarnowski <sarnowski@cosmocode.de> 1339 * @author Andreas Gohr <gohr@cosmocode.de> 1340 * @author Kate Arzamastseva <pshns@ukr.net> 1341 * @triggers MEDIA_SEARCH 1342 * 1343 * @param string $query 1344 * @param string $ns 1345 * @param null|int $auth 1346 * @param bool $fullscreen 1347 * @param string $sort 1348 */ 1349function media_searchlist($query, $ns, $auth = null, $fullscreen = false, $sort = 'natural') 1350{ 1351 global $conf; 1352 global $lang; 1353 1354 $ns = cleanID($ns); 1355 $evdata = [ 1356 'ns' => $ns, 1357 'data' => [], 1358 'query' => $query 1359 ]; 1360 if (!blank($query)) { 1361 $evt = new Event('MEDIA_SEARCH', $evdata); 1362 if ($evt->advise_before()) { 1363 $dir = utf8_encodeFN(str_replace(':', '/', $evdata['ns'])); 1364 $quoted = preg_quote($evdata['query'], '/'); 1365 //apply globbing 1366 $quoted = str_replace(['\*', '\?'], ['.*', '.'], $quoted, $count); 1367 1368 //if we use globbing file name must match entirely but may be preceded by arbitrary namespace 1369 if ($count > 0) $quoted = '^([^:]*:)*' . $quoted . '$'; 1370 1371 $pattern = '/' . $quoted . '/i'; 1372 search( 1373 $evdata['data'], 1374 $conf['mediadir'], 1375 'search_mediafiles', 1376 ['showmsg' => false, 'pattern' => $pattern], 1377 $dir, 1378 1, 1379 $sort 1380 ); 1381 } 1382 $evt->advise_after(); 1383 unset($evt); 1384 } 1385 1386 if (!$fullscreen) { 1387 echo '<h1 id="media__ns">' . sprintf($lang['searchmedia_in'], hsc($ns) . ':*') . '</h1>' . NL; 1388 media_searchform($ns, $query); 1389 } 1390 1391 if (!count($evdata['data'])) { 1392 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL; 1393 } else { 1394 if ($fullscreen) { 1395 echo '<ul class="' . _media_get_list_type() . '">'; 1396 } 1397 foreach ($evdata['data'] as $item) { 1398 if (!$fullscreen) { 1399 // FIXME old call: media_printfile($item,$item['perm'],'',true); 1400 $display = new DisplayRow($item); 1401 $display->relativeDisplay($ns); 1402 $display->show(); 1403 } else { 1404 // FIXME old call: media_printfile_thumbs($item,$item['perm'],false,true); 1405 $display = new DisplayTile($item); 1406 $display->relativeDisplay($ns); 1407 echo '<li>'; 1408 $display->show(); 1409 echo '</li>'; 1410 } 1411 } 1412 if ($fullscreen) echo '</ul>' . NL; 1413 } 1414} 1415 1416/** 1417 * Display a media icon 1418 * 1419 * @param string $filename media id 1420 * @param string $size the size subfolder, if not specified 16x16 is used 1421 * @return string html 1422 */ 1423function media_printicon($filename, $size = '') 1424{ 1425 [$ext] = mimetype(mediaFN($filename), false); 1426 1427 if (file_exists(DOKU_INC . 'lib/images/fileicons/' . $size . '/' . $ext . '.png')) { 1428 $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/' . $ext . '.png'; 1429 } else { 1430 $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/file.png'; 1431 } 1432 1433 return '<img src="' . $icon . '" alt="' . $filename . '" class="icon" />'; 1434} 1435 1436/** 1437 * Build link based on the current, adding/rewriting parameters 1438 * 1439 * @author Kate Arzamastseva <pshns@ukr.net> 1440 * 1441 * @param array|bool $params 1442 * @param string $amp separator 1443 * @param bool $abs absolute url? 1444 * @param bool $params_array return the parmeters array? 1445 * @return string|array - link or link parameters 1446 */ 1447function media_managerURL($params = false, $amp = '&', $abs = false, $params_array = false) 1448{ 1449 global $ID; 1450 global $INPUT; 1451 1452 $gets = ['do' => 'media']; 1453 $media_manager_params = ['tab_files', 'tab_details', 'image', 'ns', 'list', 'sort']; 1454 foreach ($media_manager_params as $x) { 1455 if ($INPUT->has($x)) $gets[$x] = $INPUT->str($x); 1456 } 1457 1458 if ($params) { 1459 $gets = $params + $gets; 1460 } 1461 unset($gets['id']); 1462 if (isset($gets['delete'])) { 1463 unset($gets['image']); 1464 unset($gets['tab_details']); 1465 } 1466 1467 if ($params_array) return $gets; 1468 1469 return wl($ID, $gets, $abs, $amp); 1470} 1471 1472/** 1473 * Print the media upload form if permissions are correct 1474 * 1475 * @author Andreas Gohr <andi@splitbrain.org> 1476 * @author Kate Arzamastseva <pshns@ukr.net> 1477 * 1478 * @param string $ns 1479 * @param int $auth permission level 1480 * @param bool $fullscreen 1481 */ 1482function media_uploadform($ns, $auth, $fullscreen = false) 1483{ 1484 global $lang; 1485 global $conf; 1486 global $INPUT; 1487 1488 if ($auth < AUTH_UPLOAD) { 1489 echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . NL; 1490 return; 1491 } 1492 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE); 1493 1494 $update = false; 1495 $id = ''; 1496 if ($auth >= $auth_ow && $fullscreen && $INPUT->str('mediado') == 'update') { 1497 $update = true; 1498 $id = cleanID($INPUT->str('image')); 1499 } 1500 1501 // The default HTML upload form 1502 $form = new Form([ 1503 'id' => 'dw__upload', 1504 'enctype' => 'multipart/form-data', 1505 'action' => ($fullscreen) 1506 ? media_managerURL(['tab_files' => 'files', 'tab_details' => 'view'], '&') 1507 : DOKU_BASE . 'lib/exe/mediamanager.php', 1508 ]); 1509 $form->addTagOpen('div')->addClass('no'); 1510 $form->setHiddenField('ns', hsc($ns)); // FIXME hsc required? 1511 $form->addTagOpen('p'); 1512 $form->addTextInput('upload', $lang['txt_upload'])->id('upload__file') 1513 ->attrs(['type' => 'file']); 1514 $form->addTagClose('p'); 1515 $form->addTagOpen('p'); 1516 $form->addTextInput('mediaid', $lang['txt_filename'])->id('upload__name') 1517 ->val(noNS($id)); 1518 $form->addButton('', $lang['btn_upload'])->attr('type', 'submit'); 1519 $form->addTagClose('p'); 1520 if ($auth >= $auth_ow) { 1521 $form->addTagOpen('p'); 1522 $attrs = []; 1523 if ($update) $attrs['checked'] = 'checked'; 1524 $form->addCheckbox('ow', $lang['txt_overwrt'])->id('dw__ow')->val('1') 1525 ->addClass('check')->attrs($attrs); 1526 $form->addTagClose('p'); 1527 } 1528 $form->addTagClose('div'); 1529 1530 if (!$fullscreen) { 1531 echo '<div class="upload">' . $lang['mediaupload'] . '</div>' . DOKU_LF; 1532 } else { 1533 echo DOKU_LF; 1534 } 1535 1536 echo '<div id="mediamanager__uploader">' . DOKU_LF; 1537 echo $form->toHTML('Upload'); 1538 echo '</div>' . DOKU_LF; 1539 1540 echo '<p class="maxsize">'; 1541 printf($lang['maxuploadsize'], filesize_h(media_getuploadsize())); 1542 echo ' <a class="allowedmime" href="#">' . $lang['allowedmime'] . '</a>'; 1543 echo ' <span>' . implode(', ', array_keys(getMimeTypes())) . '</span>'; 1544 echo '</p>' . DOKU_LF; 1545} 1546 1547/** 1548 * Returns the size uploaded files may have 1549 * 1550 * This uses a conservative approach using the lowest number found 1551 * in any of the limiting ini settings 1552 * 1553 * @returns int size in bytes 1554 */ 1555function media_getuploadsize() 1556{ 1557 $okay = 0; 1558 1559 $post = php_to_byte(@ini_get('post_max_size')); 1560 $suho = php_to_byte(@ini_get('suhosin.post.max_value_length')); 1561 $upld = php_to_byte(@ini_get('upload_max_filesize')); 1562 1563 if ($post && ($post < $okay || $okay === 0)) $okay = $post; 1564 if ($suho && ($suho < $okay || $okay == 0)) $okay = $suho; 1565 if ($upld && ($upld < $okay || $okay == 0)) $okay = $upld; 1566 1567 return $okay; 1568} 1569 1570/** 1571 * Print the search field form 1572 * 1573 * @author Tobias Sarnowski <sarnowski@cosmocode.de> 1574 * @author Kate Arzamastseva <pshns@ukr.net> 1575 * 1576 * @param string $ns 1577 * @param string $query 1578 * @param bool $fullscreen 1579 */ 1580function media_searchform($ns, $query = '', $fullscreen = false) 1581{ 1582 global $lang; 1583 global $INPUT; 1584 1585 // In popup mode the search reloads the whole page, so the parameters the 1586 // opening editor or plugin relies on (edid, onselect - see media.js) have to 1587 // be carried over in the action URL or they would be lost after a search. 1588 $action = DOKU_BASE . 'lib/exe/mediamanager.php'; 1589 $keep = array_filter(['edid' => $INPUT->str('edid'), 'onselect' => $INPUT->str('onselect')]); 1590 if ($keep) { 1591 $action .= '?' . buildURLparams($keep, '&'); 1592 } 1593 1594 // The default HTML search form 1595 $form = new Form([ 1596 'id' => 'dw__mediasearch', 1597 'action' => ($fullscreen) 1598 ? media_managerURL([], '&') 1599 : $action, 1600 ]); 1601 $form->addTagOpen('div')->addClass('no'); 1602 $form->setHiddenField('ns', $ns); 1603 $form->setHiddenField($fullscreen ? 'mediado' : 'do', 'searchlist'); 1604 1605 $form->addTagOpen('p'); 1606 $form->addTextInput('q', $lang['searchmedia']) 1607 ->attr('title', sprintf($lang['searchmedia_in'], hsc($ns) . ':*')) 1608 ->val($query); 1609 $form->addHTML(' '); 1610 $form->addButton('', $lang['btn_search'])->attr('type', 'submit'); 1611 $form->addTagClose('p'); 1612 $form->addTagClose('div'); 1613 echo $form->toHTML('SearchMedia'); 1614} 1615 1616/** 1617 * Build a tree outline of available media namespaces 1618 * 1619 * @author Andreas Gohr <andi@splitbrain.org> 1620 * 1621 * @param string $ns 1622 */ 1623function media_nstree($ns) 1624{ 1625 global $conf; 1626 global $lang; 1627 1628 // currently selected namespace 1629 $ns = cleanID($ns); 1630 if (empty($ns)) { 1631 global $ID; 1632 $ns = (string)getNS($ID); 1633 } 1634 1635 $ns_dir = utf8_encodeFN(str_replace(':', '/', $ns)); 1636 1637 $data = []; 1638 search($data, $conf['mediadir'], 'search_index', ['ns' => $ns_dir, 'nofiles' => true]); 1639 1640 // wrap a list with the root level around the other namespaces 1641 array_unshift($data, ['level' => 0, 'id' => '', 'open' => 'true', 'label' => '[' . $lang['mediaroot'] . ']']); 1642 1643 // insert the current ns into the hierarchy if it isn't already part of it 1644 $ns_parts = explode(':', $ns); 1645 $tmp_ns = ''; 1646 $pos = 0; 1647 $insert = false; 1648 foreach ($ns_parts as $level => $part) { 1649 if ($tmp_ns) { 1650 $tmp_ns .= ':' . $part; 1651 } else { 1652 $tmp_ns = $part; 1653 } 1654 1655 // find the namespace parts 1656 while (array_key_exists($pos, $data) && $data[$pos]['id'] != $tmp_ns) { 1657 if ( 1658 $pos >= count($data) || 1659 ($data[$pos]['level'] <= $level + 1 && Sort::strcmp($data[$pos]['id'], $tmp_ns) > 0) 1660 ) { 1661 $insert = true; 1662 break; 1663 } 1664 ++$pos; 1665 } 1666 // insert namespace in hierarchy; if not found in above loop, append it to the end 1667 if ($insert || $pos === count($data)) { 1668 array_splice($data, $pos, 0, [['level' => $level + 1, 'id' => $tmp_ns, 'open' => 'true']]); 1669 } 1670 } 1671 1672 echo html_buildlist($data, 'idx', 'media_nstree_item', 'media_nstree_li'); 1673} 1674 1675/** 1676 * Userfunction for html_buildlist 1677 * 1678 * Prints a media namespace tree item 1679 * 1680 * @author Andreas Gohr <andi@splitbrain.org> 1681 * 1682 * @param array $item 1683 * @return string html 1684 */ 1685function media_nstree_item($item) 1686{ 1687 global $INPUT; 1688 $pos = strrpos($item['id'], ':'); 1689 $label = substr($item['id'], $pos > 0 ? $pos + 1 : 0); 1690 if (empty($item['label'])) $item['label'] = $label; 1691 1692 $ret = ''; 1693 if ($INPUT->str('do') != 'media') 1694 $ret .= '<a href="' . DOKU_BASE . 'lib/exe/mediamanager.php?ns=' . idfilter($item['id']) . '" class="idx_dir">'; 1695 else $ret .= '<a href="' . media_managerURL(['ns' => idfilter($item['id'], false), 'tab_files' => 'files']) 1696 . '" class="idx_dir">'; 1697 $ret .= $item['label']; 1698 $ret .= '</a>'; 1699 return $ret; 1700} 1701 1702/** 1703 * Userfunction for html_buildlist 1704 * 1705 * Prints a media namespace tree item opener 1706 * 1707 * @author Andreas Gohr <andi@splitbrain.org> 1708 * 1709 * @param array $item 1710 * @return string html 1711 */ 1712function media_nstree_li($item) 1713{ 1714 $class = 'media level' . $item['level']; 1715 if ($item['open']) { 1716 $class .= ' open'; 1717 $img = DOKU_BASE . 'lib/images/minus.gif'; 1718 $alt = '−'; 1719 } else { 1720 $class .= ' closed'; 1721 $img = DOKU_BASE . 'lib/images/plus.gif'; 1722 $alt = '+'; 1723 } 1724 // TODO: only deliver an image if it actually has a subtree... 1725 return '<li class="' . $class . '">' . 1726 '<img src="' . $img . '" alt="' . $alt . '" />'; 1727} 1728 1729/** 1730 * Resizes or crop the given image to the given size 1731 * 1732 * @author Andreas Gohr <andi@splitbrain.org> 1733 * 1734 * @param string $file filename, path to file 1735 * @param string $ext extension 1736 * @param int $w desired width 1737 * @param int $h desired height 1738 * @param bool $crop should a center crop be used? 1739 * @param bool $upscale when false, a smaller image is kept at its original size 1740 * @return string path to resized or original size if failed 1741 */ 1742function media_mod_image($file, $ext, $w, $h = 0, $crop = false, $upscale = true) 1743{ 1744 global $conf; 1745 if (!$h) $h = 0; 1746 // we wont scale up to infinity 1747 if ($w > 2000 || $h > 2000) return $file; 1748 1749 $operation = $crop ? 'crop' : 'resize'; 1750 1751 $options = [ 1752 'quality' => $conf['jpg_quality'], 1753 'imconvert' => $conf['im_convert'], 1754 ]; 1755 1756 $cache = new CacheImageMod($file, $w, $h, $ext, $crop, $upscale); 1757 if (!$cache->useCache()) { 1758 try { 1759 Slika::run($file, $options) 1760 ->autorotate() 1761 ->$operation($w, $h, $upscale) 1762 ->save($cache->cache, $ext); 1763 if ($conf['fperm']) @chmod($cache->cache, $conf['fperm']); 1764 } catch (Exception $e) { 1765 Logger::debug($e->getMessage()); 1766 return $file; 1767 } 1768 } 1769 1770 return $cache->cache; 1771} 1772 1773/** 1774 * Resizes the given image to the given size 1775 * 1776 * @author Andreas Gohr <andi@splitbrain.org> 1777 * 1778 * @param string $file filename, path to file 1779 * @param string $ext extension 1780 * @param int $w desired width 1781 * @param int $h desired height 1782 * @param bool $upscale when false, a smaller image is kept at its original size 1783 * @return string path to resized or original size if failed 1784 */ 1785function media_resize_image($file, $ext, $w, $h = 0, $upscale = true) 1786{ 1787 return media_mod_image($file, $ext, $w, $h, false, $upscale); 1788} 1789 1790/** 1791 * Center crops the given image to the wanted size 1792 * 1793 * @author Andreas Gohr <andi@splitbrain.org> 1794 * 1795 * @param string $file filename, path to file 1796 * @param string $ext extension 1797 * @param int $w desired width 1798 * @param int $h desired height 1799 * @return string path to resized or original size if failed 1800 */ 1801function media_crop_image($file, $ext, $w, $h = 0) 1802{ 1803 return media_mod_image($file, $ext, $w, $h, true); 1804} 1805 1806/** 1807 * Calculate a token to be used to verify fetch requests for resized or 1808 * cropped images have been internally generated - and prevent external 1809 * DDOS attacks via fetch 1810 * 1811 * @author Christopher Smith <chris@jalakai.co.uk> 1812 * 1813 * @param string $id id of the image 1814 * @param int $w resize/crop width 1815 * @param int $h resize/crop height 1816 * @return string token or empty string if no token required 1817 */ 1818function media_get_token($id, $w, $h) 1819{ 1820 // token is only required for modified images 1821 if ($w || $h || media_isexternal($id)) { 1822 $token = $id; 1823 if ($w) $token .= '.' . $w; 1824 if ($h) $token .= '.' . $h; 1825 1826 return substr(PassHash::hmac('md5', $token, auth_cookiesalt()), 0, 6); 1827 } 1828 1829 return ''; 1830} 1831 1832/** 1833 * Download a remote file and return local filename 1834 * 1835 * returns false if download fails. Uses cached file if available and 1836 * wanted 1837 * 1838 * @author Andreas Gohr <andi@splitbrain.org> 1839 * @author Pavel Vitis <Pavel.Vitis@seznam.cz> 1840 * 1841 * @param string $url 1842 * @param string $ext extension 1843 * @param int $cache cachetime in seconds 1844 * @return false|string path to cached file 1845 */ 1846function media_get_from_URL($url, $ext, $cache) 1847{ 1848 global $conf; 1849 1850 // if no cache or fetchsize just redirect 1851 if ($cache == 0) return false; 1852 if (!$conf['fetchsize']) return false; 1853 1854 $local = getCacheName(strtolower($url), ".media.$ext"); 1855 $mtime = @filemtime($local); // 0 if not exists 1856 1857 //decide if download needed: 1858 if ( 1859 ($mtime == 0) || // cache does not exist 1860 ($cache != -1 && $mtime < time() - $cache) // 'recache' and cache has expired 1861 ) { 1862 if (media_image_download($url, $local)) { 1863 return $local; 1864 } else { 1865 return false; 1866 } 1867 } 1868 1869 //if cache exists use it else 1870 if ($mtime) return $local; 1871 1872 //else return false 1873 return false; 1874} 1875 1876/** 1877 * Download image files 1878 * 1879 * @author Andreas Gohr <andi@splitbrain.org> 1880 * 1881 * @param string $url 1882 * @param string $file path to file in which to put the downloaded content 1883 * @return bool 1884 */ 1885function media_image_download($url, $file) 1886{ 1887 global $conf; 1888 $http = new DokuHTTPClient(); 1889 $http->keep_alive = false; // we do single ops here, no need for keep-alive 1890 1891 $http->max_bodysize = $conf['fetchsize']; 1892 $http->timeout = 25; //max. 25 sec 1893 $http->header_regexp = '!\r\nContent-Type: image/(jpe?g|gif|png)!i'; 1894 1895 $data = $http->get($url); 1896 if (!$data) return false; 1897 1898 $fileexists = file_exists($file); 1899 $fp = @fopen($file, "w"); 1900 if (!$fp) return false; 1901 fwrite($fp, $data); 1902 fclose($fp); 1903 if (!$fileexists && $conf['fperm']) chmod($file, $conf['fperm']); 1904 1905 // check if it is really an image 1906 $info = @getimagesize($file); 1907 if (!$info) { 1908 @unlink($file); 1909 return false; 1910 } 1911 1912 return true; 1913} 1914 1915/** 1916 * Return other media files with the same base name 1917 * but different extensions. 1918 * 1919 * @param string $src - ID of media file 1920 * @param string[] $exts - alternative extensions to find other files for 1921 * @return array - array(mime type => file ID) 1922 * 1923 * @author Anika Henke <anika@selfthinker.org> 1924 */ 1925function media_alternativefiles($src, $exts) 1926{ 1927 1928 $files = []; 1929 [$srcExt, /* srcMime */] = mimetype($src); 1930 $filebase = substr($src, 0, -1 * (strlen($srcExt) + 1)); 1931 1932 foreach ($exts as $ext) { 1933 $fileid = $filebase . '.' . $ext; 1934 $file = mediaFN($fileid); 1935 if (file_exists($file)) { 1936 [/* fileExt */, $fileMime] = mimetype($file); 1937 $files[$fileMime] = $fileid; 1938 } 1939 } 1940 return $files; 1941} 1942 1943/** 1944 * Check if video/audio is supported to be embedded. 1945 * 1946 * @param string $mime - mimetype of media file 1947 * @param string $type - type of media files to check ('video', 'audio', or null for all) 1948 * @return boolean 1949 * 1950 * @author Anika Henke <anika@selfthinker.org> 1951 */ 1952function media_supportedav($mime, $type = null) 1953{ 1954 $supportedAudio = [ 1955 'ogg' => 'audio/ogg', 1956 'mp3' => 'audio/mpeg', 1957 'wav' => 'audio/wav' 1958 ]; 1959 $supportedVideo = [ 1960 'webm' => 'video/webm', 1961 'ogv' => 'video/ogg', 1962 'mp4' => 'video/mp4' 1963 ]; 1964 if ($type == 'audio') { 1965 $supportedAv = $supportedAudio; 1966 } elseif ($type == 'video') { 1967 $supportedAv = $supportedVideo; 1968 } else { 1969 $supportedAv = array_merge($supportedAudio, $supportedVideo); 1970 } 1971 return in_array($mime, $supportedAv); 1972} 1973 1974/** 1975 * Return track media files with the same base name 1976 * but extensions that indicate kind and lang. 1977 * ie for foo.webm search foo.sub.lang.vtt, foo.cap.lang.vtt... 1978 * 1979 * @param string $src - ID of media file 1980 * @return array - array(mediaID => array( kind, srclang )) 1981 * 1982 * @author Schplurtz le Déboulonné <Schplurtz@laposte.net> 1983 */ 1984function media_trackfiles($src) 1985{ 1986 $kinds = [ 1987 'sub' => 'subtitles', 1988 'cap' => 'captions', 1989 'des' => 'descriptions', 1990 'cha' => 'chapters', 1991 'met' => 'metadata' 1992 ]; 1993 1994 $files = []; 1995 $re = '/\\.(sub|cap|des|cha|met)\\.([^.]+)\\.vtt$/'; 1996 $baseid = pathinfo($src, PATHINFO_FILENAME); 1997 $pattern = mediaFN($baseid) . '.*.*.vtt'; 1998 $list = glob($pattern); 1999 foreach ($list as $track) { 2000 if (preg_match($re, $track, $matches)) { 2001 $files[$baseid . '.' . $matches[1] . '.' . $matches[2] . '.vtt'] = [$kinds[$matches[1]], $matches[2]]; 2002 } 2003 } 2004 return $files; 2005} 2006 2007/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 2008