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