More adjusted API tests
[dokuwiki.git] / inc / media.php
blob1a6d168cf046786de18405cc170c62722d0a89a8
1 <?php
3 /**
4 * All output and handler function needed for the media management popup
6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author Andreas Gohr <andi@splitbrain.org>
8 */
10 use dokuwiki\Ui\MediaRevisions;
11 use dokuwiki\Cache\CacheImageMod;
12 use splitbrain\slika\Exception;
13 use dokuwiki\PassHash;
14 use dokuwiki\ChangeLog\MediaChangeLog;
15 use dokuwiki\Extension\Event;
16 use dokuwiki\Form\Form;
17 use dokuwiki\HTTP\DokuHTTPClient;
18 use dokuwiki\Logger;
19 use dokuwiki\Subscriptions\MediaSubscriptionSender;
20 use dokuwiki\Ui\Media\DisplayRow;
21 use dokuwiki\Ui\Media\DisplayTile;
22 use dokuwiki\Ui\MediaDiff;
23 use dokuwiki\Utf8\PhpString;
24 use dokuwiki\Utf8\Sort;
25 use splitbrain\slika\Slika;
27 /**
28 * Lists pages which currently use a media file selected for deletion
30 * References uses the same visual as search results and share
31 * their CSS tags except pagenames won't be links.
33 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
35 * @param array $data
36 * @param string $id
38 function media_filesinuse($data, $id)
40 global $lang;
41 echo '<h1>' . $lang['reference'] . ' <code>' . hsc(noNS($id)) . '</code></h1>';
42 echo '<p>' . hsc($lang['ref_inuse']) . '</p>';
44 $hidden = 0; //count of hits without read permission
45 foreach ($data as $row) {
46 if (auth_quickaclcheck($row) >= AUTH_READ && isVisiblePage($row)) {
47 echo '<div class="search_result">';
48 echo '<span class="mediaref_ref">' . hsc($row) . '</span>';
49 echo '</div>';
50 } else $hidden++;
52 if ($hidden) {
53 echo '<div class="mediaref_hidden">' . $lang['ref_hidden'] . '</div>';
57 /**
58 * Handles the saving of image meta data
60 * @author Andreas Gohr <andi@splitbrain.org>
61 * @author Kate Arzamastseva <pshns@ukr.net>
63 * @param string $id media id
64 * @param int $auth permission level
65 * @param array $data
66 * @return false|string
68 function media_metasave($id, $auth, $data)
70 if ($auth < AUTH_UPLOAD) return false;
71 if (!checkSecurityToken()) return false;
72 global $lang;
73 global $conf;
74 $src = mediaFN($id);
76 $meta = new JpegMeta($src);
77 $meta->_parseAll();
79 foreach ($data as $key => $val) {
80 $val = trim($val);
81 if (empty($val)) {
82 $meta->deleteField($key);
83 } else {
84 $meta->setField($key, $val);
88 $old = @filemtime($src);
89 if (!file_exists(mediaFN($id, $old)) && file_exists($src)) {
90 // add old revision to the attic
91 media_saveOldRevision($id);
93 $filesize_old = filesize($src);
94 if ($meta->save()) {
95 if ($conf['fperm']) chmod($src, $conf['fperm']);
96 @clearstatcache(true, $src);
97 $new = @filemtime($src);
98 $filesize_new = filesize($src);
99 $sizechange = $filesize_new - $filesize_old;
101 // add a log entry to the media changelog
102 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, $lang['media_meta_edited'], '', null, $sizechange);
104 msg($lang['metasaveok'], 1);
105 return $id;
106 } else {
107 msg($lang['metasaveerr'], -1);
108 return false;
113 * check if a media is external source
115 * @author Gerrit Uitslag <klapinklapin@gmail.com>
117 * @param string $id the media ID or URL
118 * @return bool
120 function media_isexternal($id)
122 if (preg_match('#^(?:https?|ftp)://#i', $id)) return true;
123 return false;
127 * Check if a media item is public (eg, external URL or readable by @ALL)
129 * @author Andreas Gohr <andi@splitbrain.org>
131 * @param string $id the media ID or URL
132 * @return bool
134 function media_ispublic($id)
136 if (media_isexternal($id)) return true;
137 $id = cleanID($id);
138 if (auth_aclcheck(getNS($id) . ':*', '', []) >= AUTH_READ) return true;
139 return false;
143 * Display the form to edit image meta data
145 * @author Andreas Gohr <andi@splitbrain.org>
146 * @author Kate Arzamastseva <pshns@ukr.net>
148 * @param string $id media id
149 * @param int $auth permission level
150 * @return bool
152 function media_metaform($id, $auth)
154 global $lang;
156 if ($auth < AUTH_UPLOAD) {
157 echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . DOKU_LF;
158 return false;
161 // load the field descriptions
162 static $fields = null;
163 if ($fields === null) {
164 $config_files = getConfigFiles('mediameta');
165 foreach ($config_files as $config_file) {
166 if (file_exists($config_file)) include($config_file);
170 $src = mediaFN($id);
172 // output
173 $form = new Form([
174 'action' => media_managerURL(['tab_details' => 'view'], '&'),
175 'class' => 'meta'
177 $form->addTagOpen('div')->addClass('no');
178 $form->setHiddenField('img', $id);
179 $form->setHiddenField('mediado', 'save');
180 foreach ($fields as $key => $field) {
181 // get current value
182 if (empty($field[0])) continue;
183 $tags = [$field[0]];
184 if (isset($field[3]) && is_array($field[3])) $tags = array_merge($tags, $field[3]);
185 $value = tpl_img_getTag($tags, '', $src);
186 $value = cleanText($value);
188 // prepare attributes
189 $p = [
190 'class' => 'edit',
191 'id' => 'meta__' . $key,
192 'name' => 'meta[' . $field[0] . ']'
195 $form->addTagOpen('div')->addClass('row');
196 if ($field[2] == 'text') {
197 $form->addTextInput(
198 $p['name'],
199 ($lang[$field[1]] ?: $field[1] . ':')
200 )->id($p['id'])->addClass($p['class'])->val($value);
201 } else {
202 $form->addTextarea($p['name'], $lang[$field[1]])->id($p['id'])
203 ->val(formText($value))
204 ->addClass($p['class'])
205 ->attr('rows', '6')->attr('cols', '50');
207 $form->addTagClose('div');
209 $form->addTagOpen('div')->addClass('buttons');
210 $form->addButton('mediado[save]', $lang['btn_save'])->attr('type', 'submit')
211 ->attrs(['accesskey' => 's']);
212 $form->addTagClose('div');
214 $form->addTagClose('div');
215 echo $form->toHTML();
216 return true;
220 * Convenience function to check if a media file is still in use
222 * @author Michael Klier <chi@chimeric.de>
224 * @param string $id media id
225 * @return array|bool
227 function media_inuse($id)
229 global $conf;
231 if ($conf['refcheck']) {
232 $mediareferences = ft_mediause($id, true);
233 if ($mediareferences === []) {
234 return false;
235 } else {
236 return $mediareferences;
238 } else {
239 return false;
244 * Handles media file deletions
246 * If configured, checks for media references before deletion
248 * @author Andreas Gohr <andi@splitbrain.org>
250 * @param string $id media id
251 * @param int $auth no longer used
252 * @return int One of: 0,
253 * DOKU_MEDIA_DELETED,
254 * DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS,
255 * DOKU_MEDIA_NOT_AUTH,
256 * DOKU_MEDIA_INUSE
258 function media_delete($id, $auth)
260 global $lang;
261 $auth = auth_quickaclcheck(ltrim(getNS($id) . ':*', ':'));
262 if ($auth < AUTH_DELETE) return DOKU_MEDIA_NOT_AUTH;
263 if (media_inuse($id)) return DOKU_MEDIA_INUSE;
265 $file = mediaFN($id);
267 // trigger an event - MEDIA_DELETE_FILE
268 $data = [];
269 $data['id'] = $id;
270 $data['name'] = PhpString::basename($file);
271 $data['path'] = $file;
272 $data['size'] = (file_exists($file)) ? filesize($file) : 0;
274 $data['unl'] = false;
275 $data['del'] = false;
276 $evt = new Event('MEDIA_DELETE_FILE', $data);
277 if ($evt->advise_before()) {
278 $old = @filemtime($file);
279 if (!file_exists(mediaFN($id, $old)) && file_exists($file)) {
280 // add old revision to the attic
281 media_saveOldRevision($id);
284 $data['unl'] = @unlink($file);
285 if ($data['unl']) {
286 $sizechange = 0 - $data['size'];
287 addMediaLogEntry(time(), $id, DOKU_CHANGE_TYPE_DELETE, $lang['deleted'], '', null, $sizechange);
289 $data['del'] = io_sweepNS($id, 'mediadir');
292 $evt->advise_after();
293 unset($evt);
295 if ($data['unl'] && $data['del']) {
296 return DOKU_MEDIA_DELETED | DOKU_MEDIA_EMPTY_NS;
299 return $data['unl'] ? DOKU_MEDIA_DELETED : 0;
303 * Handle file uploads via XMLHttpRequest
305 * @param string $ns target namespace
306 * @param int $auth current auth check result
307 * @return false|string false on error, id of the new file on success
309 function media_upload_xhr($ns, $auth)
311 if (!checkSecurityToken()) return false;
312 global $INPUT;
314 $id = $INPUT->get->str('qqfile');
315 [$ext, $mime] = mimetype($id);
316 $input = fopen("php://input", "r");
317 if (!($tmp = io_mktmpdir())) return false;
318 $path = $tmp . '/' . md5($id);
319 $target = fopen($path, "w");
320 $realSize = stream_copy_to_stream($input, $target);
321 fclose($target);
322 fclose($input);
323 if ($INPUT->server->has('CONTENT_LENGTH') && ($realSize != $INPUT->server->int('CONTENT_LENGTH'))) {
324 unlink($path);
325 return false;
328 $res = media_save(
329 ['name' => $path, 'mime' => $mime, 'ext' => $ext],
330 $ns . ':' . $id,
331 ($INPUT->get->str('ow') == 'true'),
332 $auth,
333 'copy'
335 unlink($path);
336 if ($tmp) io_rmdir($tmp, true);
337 if (is_array($res)) {
338 msg($res[0], $res[1]);
339 return false;
341 return $res;
345 * Handles media file uploads
347 * @author Andreas Gohr <andi@splitbrain.org>
348 * @author Michael Klier <chi@chimeric.de>
350 * @param string $ns target namespace
351 * @param int $auth current auth check result
352 * @param bool|array $file $_FILES member, $_FILES['upload'] if false
353 * @return false|string false on error, id of the new file on success
355 function media_upload($ns, $auth, $file = false)
357 if (!checkSecurityToken()) return false;
358 global $lang;
359 global $INPUT;
361 // get file and id
362 $id = $INPUT->post->str('mediaid');
363 if (!$file) $file = $_FILES['upload'];
364 if (empty($id)) $id = $file['name'];
366 // check for errors (messages are done in lib/exe/mediamanager.php)
367 if ($file['error']) return false;
369 // check extensions
370 [$fext, $fmime] = mimetype($file['name']);
371 [$iext, $imime] = mimetype($id);
372 if ($fext && !$iext) {
373 // no extension specified in id - read original one
374 $id .= '.' . $fext;
375 $imime = $fmime;
376 } elseif ($fext && $fext != $iext) {
377 // extension was changed, print warning
378 msg(sprintf($lang['mediaextchange'], $fext, $iext));
381 $res = media_save(
383 'name' => $file['tmp_name'],
384 'mime' => $imime,
385 'ext' => $iext
387 $ns . ':' . $id,
388 $INPUT->post->bool('ow'),
389 $auth,
390 'copy_uploaded_file'
392 if (is_array($res)) {
393 msg($res[0], $res[1]);
394 return false;
396 return $res;
400 * An alternative to move_uploaded_file that copies
402 * Using copy, makes sure any setgid bits on the media directory are honored
404 * @see move_uploaded_file()
406 * @param string $from
407 * @param string $to
408 * @return bool
410 function copy_uploaded_file($from, $to)
412 if (!is_uploaded_file($from)) return false;
413 $ok = copy($from, $to);
414 @unlink($from);
415 return $ok;
419 * This generates an action event and delegates to _media_upload_action().
420 * Action plugins are allowed to pre/postprocess the uploaded file.
421 * (The triggered event is preventable.)
423 * Event data:
424 * $data[0] fn_tmp: the temporary file name (read from $_FILES)
425 * $data[1] fn: the file name of the uploaded file
426 * $data[2] id: the future directory id of the uploaded file
427 * $data[3] imime: the mimetype of the uploaded file
428 * $data[4] overwrite: if an existing file is going to be overwritten
429 * $data[5] move: name of function that performs move/copy/..
431 * @triggers MEDIA_UPLOAD_FINISH
433 * @param array $file
434 * @param string $id media id
435 * @param bool $ow overwrite?
436 * @param int $auth permission level
437 * @param string $move name of functions that performs move/copy/..
438 * @return false|array|string
440 function media_save($file, $id, $ow, $auth, $move)
442 if ($auth < AUTH_UPLOAD) {
443 return ["You don't have permissions to upload files.", -1];
446 if (!isset($file['mime']) || !isset($file['ext'])) {
447 [$ext, $mime] = mimetype($id);
448 if (!isset($file['mime'])) {
449 $file['mime'] = $mime;
451 if (!isset($file['ext'])) {
452 $file['ext'] = $ext;
456 global $lang, $conf;
458 // get filename
459 $id = cleanID($id);
460 $fn = mediaFN($id);
462 // get filetype regexp
463 $types = array_keys(getMimeTypes());
464 $types = array_map(
465 static fn($q) => preg_quote($q, "/"),
466 $types
468 $regex = implode('|', $types);
470 // because a temp file was created already
471 if (!preg_match('/\.(' . $regex . ')$/i', $fn)) {
472 return [$lang['uploadwrong'], -1];
475 //check for overwrite
476 $overwrite = file_exists($fn);
477 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
478 if ($overwrite && (!$ow || $auth < $auth_ow)) {
479 return [$lang['uploadexist'], 0];
481 // check for valid content
482 $ok = media_contentcheck($file['name'], $file['mime']);
483 if ($ok == -1) {
484 return [sprintf($lang['uploadbadcontent'], '.' . $file['ext']), -1];
485 } elseif ($ok == -2) {
486 return [$lang['uploadspam'], -1];
487 } elseif ($ok == -3) {
488 return [$lang['uploadxss'], -1];
491 // prepare event data
492 $data = [];
493 $data[0] = $file['name'];
494 $data[1] = $fn;
495 $data[2] = $id;
496 $data[3] = $file['mime'];
497 $data[4] = $overwrite;
498 $data[5] = $move;
500 // trigger event
501 return Event::createAndTrigger('MEDIA_UPLOAD_FINISH', $data, '_media_upload_action', true);
505 * Callback adapter for media_upload_finish() triggered by MEDIA_UPLOAD_FINISH
507 * @author Michael Klier <chi@chimeric.de>
509 * @param array $data event data
510 * @return false|array|string
512 function _media_upload_action($data)
514 // fixme do further sanity tests of given data?
515 if (is_array($data) && count($data) === 6) {
516 return media_upload_finish($data[0], $data[1], $data[2], $data[3], $data[4], $data[5]);
517 } else {
518 return false; //callback error
523 * Saves an uploaded media file
525 * @author Andreas Gohr <andi@splitbrain.org>
526 * @author Michael Klier <chi@chimeric.de>
527 * @author Kate Arzamastseva <pshns@ukr.net>
529 * @param string $fn_tmp
530 * @param string $fn
531 * @param string $id media id
532 * @param string $imime mime type
533 * @param bool $overwrite overwrite existing?
534 * @param string $move function name
535 * @return array|string
537 function media_upload_finish($fn_tmp, $fn, $id, $imime, $overwrite, $move = 'move_uploaded_file')
539 global $conf;
540 global $lang;
541 global $REV;
543 $old = @filemtime($fn);
544 if (!file_exists(mediaFN($id, $old)) && file_exists($fn)) {
545 // add old revision to the attic if missing
546 media_saveOldRevision($id);
549 // prepare directory
550 io_createNamespace($id, 'media');
552 $filesize_old = file_exists($fn) ? filesize($fn) : 0;
554 if ($move($fn_tmp, $fn)) {
555 @clearstatcache(true, $fn);
556 $new = @filemtime($fn);
557 // Set the correct permission here.
558 // Always chmod media because they may be saved with different permissions than expected from the php umask.
559 // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
560 chmod($fn, $conf['fmode']);
561 msg($lang['uploadsucc'], 1);
562 media_notify($id, $fn, $imime, $old, $new);
563 // add a log entry to the media changelog
564 $filesize_new = filesize($fn);
565 $sizechange = $filesize_new - $filesize_old;
566 if ($REV) {
567 addMediaLogEntry(
568 $new,
569 $id,
570 DOKU_CHANGE_TYPE_REVERT,
571 sprintf($lang['restored'], dformat($REV)),
572 $REV,
573 null,
574 $sizechange
576 } elseif ($overwrite) {
577 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
578 } else {
579 addMediaLogEntry($new, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
581 return $id;
582 } else {
583 return [$lang['uploadfail'], -1];
588 * Moves the current version of media file to the media_attic
589 * directory
591 * @author Kate Arzamastseva <pshns@ukr.net>
593 * @param string $id
594 * @return int - revision date
596 function media_saveOldRevision($id)
598 global $conf, $lang;
600 $oldf = mediaFN($id);
601 if (!file_exists($oldf)) return '';
602 $date = filemtime($oldf);
603 if (!$conf['mediarevisions']) return $date;
605 $medialog = new MediaChangeLog($id);
606 if (!$medialog->getRevisionInfo($date)) {
607 // there was an external edit,
608 // there is no log entry for current version of file
609 $sizechange = filesize($oldf);
610 if (!file_exists(mediaMetaFN($id, '.changes'))) {
611 addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_CREATE, $lang['created'], '', null, $sizechange);
612 } else {
613 $oldRev = $medialog->getRevisions(-1, 1); // from changelog
614 $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]);
615 $filesize_old = filesize(mediaFN($id, $oldRev));
616 $sizechange -= $filesize_old;
618 addMediaLogEntry($date, $id, DOKU_CHANGE_TYPE_EDIT, '', '', null, $sizechange);
622 $newf = mediaFN($id, $date);
623 io_makeFileDir($newf);
624 if (copy($oldf, $newf)) {
625 // Set the correct permission here.
626 // Always chmod media because they may be saved with different permissions than expected from the php umask.
627 // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
628 chmod($newf, $conf['fmode']);
630 return $date;
634 * This function checks if the uploaded content is really what the
635 * mimetype says it is. We also do spam checking for text types here.
637 * We need to do this stuff because we can not rely on the browser
638 * to do this check correctly. Yes, IE is broken as usual.
640 * @author Andreas Gohr <andi@splitbrain.org>
641 * @link http://www.splitbrain.org/blog/2007-02/12-internet_explorer_facilitates_cross_site_scripting
642 * @fixme check all 26 magic IE filetypes here?
644 * @param string $file path to file
645 * @param string $mime mimetype
646 * @return int
648 function media_contentcheck($file, $mime)
650 global $conf;
651 if ($conf['iexssprotect']) {
652 $fh = @fopen($file, 'rb');
653 if ($fh) {
654 $bytes = fread($fh, 256);
655 fclose($fh);
656 if (preg_match('/<(script|a|img|html|body|iframe)[\s>]/i', $bytes)) {
657 return -3; //XSS: possibly malicious content
661 if (str_starts_with($mime, 'image/')) {
662 $info = @getimagesize($file);
663 if ($mime == 'image/gif' && $info[2] != 1) {
664 return -1; // uploaded content did not match the file extension
665 } elseif ($mime == 'image/jpeg' && $info[2] != 2) {
666 return -1;
667 } elseif ($mime == 'image/png' && $info[2] != 3) {
668 return -1;
670 # fixme maybe check other images types as well
671 } elseif (str_starts_with($mime, 'text/')) {
672 global $TEXT;
673 $TEXT = io_readFile($file);
674 if (checkwordblock()) {
675 return -2; //blocked by the spam blacklist
678 return 0;
682 * Send a notify mail on uploads
684 * @author Andreas Gohr <andi@splitbrain.org>
686 * @param string $id media id
687 * @param string $file path to file
688 * @param string $mime mime type
689 * @param bool|int $old_rev revision timestamp or false
691 function media_notify($id, $file, $mime, $old_rev = false, $current_rev = false)
693 global $conf;
694 if (empty($conf['notify'])) return; //notify enabled?
696 $subscription = new MediaSubscriptionSender();
697 $subscription->sendMediaDiff($conf['notify'], 'uploadmail', $id, $old_rev, $current_rev);
701 * List all files in a given Media namespace
703 * @param string $ns namespace
704 * @param null|int $auth permission level
705 * @param string $jump id
706 * @param bool $fullscreenview
707 * @param bool|string $sort sorting order, false skips sorting
709 function media_filelist($ns, $auth = null, $jump = '', $fullscreenview = false, $sort = false)
711 global $conf;
712 global $lang;
713 $ns = cleanID($ns);
715 // check auth our self if not given (needed for ajax calls)
716 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
718 if (!$fullscreenview) echo '<h1 id="media__ns">:' . hsc($ns) . '</h1>' . NL;
720 if ($auth < AUTH_READ) {
721 // FIXME: print permission warning here instead?
722 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
723 } else {
724 if (!$fullscreenview) {
725 media_uploadform($ns, $auth);
726 media_searchform($ns);
729 $dir = utf8_encodeFN(str_replace(':', '/', $ns));
730 $data = [];
731 search(
732 $data,
733 $conf['mediadir'],
734 'search_mediafiles',
735 ['showmsg' => true, 'depth' => 1],
736 $dir,
738 $sort
741 if (!count($data)) {
742 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
743 } else {
744 if ($fullscreenview) {
745 echo '<ul class="' . _media_get_list_type() . '">';
747 foreach ($data as $item) {
748 if (!$fullscreenview) {
749 //FIXME old call: media_printfile($item,$auth,$jump);
750 $display = new DisplayRow($item);
751 $display->scrollIntoView($jump == $item->getID());
752 $display->show();
753 } else {
754 //FIXME old call: media_printfile_thumbs($item,$auth,$jump);
755 echo '<li>';
756 $display = new DisplayTile($item);
757 $display->scrollIntoView($jump == $item->getID());
758 $display->show();
759 echo '</li>';
762 if ($fullscreenview) echo '</ul>' . NL;
768 * Prints tabs for files list actions
770 * @author Kate Arzamastseva <pshns@ukr.net>
771 * @author Adrian Lang <mail@adrianlang.de>
773 * @param string $selected_tab - opened tab
776 function media_tabs_files($selected_tab = '')
778 global $lang;
779 $tabs = [];
780 foreach (
782 'files' => 'mediaselect',
783 'upload' => 'media_uploadtab',
784 'search' => 'media_searchtab'
785 ] as $tab => $caption
787 $tabs[$tab] = [
788 'href' => media_managerURL(['tab_files' => $tab], '&'),
789 'caption' => $lang[$caption]
793 html_tabs($tabs, $selected_tab);
797 * Prints tabs for files details actions
799 * @author Kate Arzamastseva <pshns@ukr.net>
800 * @param string $image filename of the current image
801 * @param string $selected_tab opened tab
803 function media_tabs_details($image, $selected_tab = '')
805 global $lang, $conf;
807 $tabs = [];
808 $tabs['view'] = [
809 'href' => media_managerURL(['tab_details' => 'view'], '&'),
810 'caption' => $lang['media_viewtab']
813 [, $mime] = mimetype($image);
814 if ($mime == 'image/jpeg' && file_exists(mediaFN($image))) {
815 $tabs['edit'] = [
816 'href' => media_managerURL(['tab_details' => 'edit'], '&'),
817 'caption' => $lang['media_edittab']
820 if ($conf['mediarevisions']) {
821 $tabs['history'] = [
822 'href' => media_managerURL(['tab_details' => 'history'], '&'),
823 'caption' => $lang['media_historytab']
827 html_tabs($tabs, $selected_tab);
831 * Prints options for the tab that displays a list of all files
833 * @author Kate Arzamastseva <pshns@ukr.net>
835 function media_tab_files_options()
837 global $lang;
838 global $INPUT;
839 global $ID;
841 $form = new Form([
842 'method' => 'get',
843 'action' => wl($ID),
844 'class' => 'options'
846 $form->addTagOpen('div')->addClass('no');
847 $form->setHiddenField('sectok', null);
848 $media_manager_params = media_managerURL([], '', false, true);
849 foreach ($media_manager_params as $pKey => $pVal) {
850 $form->setHiddenField($pKey, $pVal);
852 if ($INPUT->has('q')) {
853 $form->setHiddenField('q', $INPUT->str('q'));
855 $form->addHTML('<ul>' . NL);
856 foreach (
858 'list' => ['listType', ['thumbs', 'rows']],
859 'sort' => ['sortBy', ['name', 'date']]
860 ] as $group => $content
862 $checked = "_media_get_{$group}_type";
863 $checked = $checked();
865 $form->addHTML('<li class="' . $content[0] . '">');
866 foreach ($content[1] as $option) {
867 $attrs = [];
868 if ($checked == $option) {
869 $attrs['checked'] = 'checked';
871 $radio = $form->addRadioButton(
872 $group . '_dwmedia',
873 $lang['media_' . $group . '_' . $option]
874 )->val($option)->id($content[0] . '__' . $option)->addClass($option);
875 $radio->attrs($attrs);
877 $form->addHTML('</li>' . NL);
879 $form->addHTML('<li>');
880 $form->addButton('', $lang['btn_apply'])->attr('type', 'submit');
881 $form->addHTML('</li>' . NL);
882 $form->addHTML('</ul>' . NL);
883 $form->addTagClose('div');
884 echo $form->toHTML();
888 * Returns type of sorting for the list of files in media manager
890 * @author Kate Arzamastseva <pshns@ukr.net>
892 * @return string - sort type
894 function _media_get_sort_type()
896 return _media_get_display_param('sort', ['default' => 'name', 'date']);
900 * Returns type of listing for the list of files in media manager
902 * @author Kate Arzamastseva <pshns@ukr.net>
904 * @return string - list type
906 function _media_get_list_type()
908 return _media_get_display_param('list', ['default' => 'thumbs', 'rows']);
912 * Get display parameters
914 * @param string $param name of parameter
915 * @param array $values allowed values, where default value has index key 'default'
916 * @return string the parameter value
918 function _media_get_display_param($param, $values)
920 global $INPUT;
921 if (in_array($INPUT->str($param), $values)) {
922 // FIXME: Set cookie
923 return $INPUT->str($param);
924 } else {
925 $val = get_doku_pref($param, $values['default']);
926 if (!in_array($val, $values)) {
927 $val = $values['default'];
929 return $val;
934 * Prints tab that displays a list of all files
936 * @author Kate Arzamastseva <pshns@ukr.net>
938 * @param string $ns
939 * @param null|int $auth permission level
940 * @param string $jump item id
942 function media_tab_files($ns, $auth = null, $jump = '')
944 global $lang;
945 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
947 if ($auth < AUTH_READ) {
948 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
949 } else {
950 media_filelist($ns, $auth, $jump, true, _media_get_sort_type());
955 * Prints tab that displays uploading form
957 * @author Kate Arzamastseva <pshns@ukr.net>
959 * @param string $ns
960 * @param null|int $auth permission level
961 * @param string $jump item id
963 function media_tab_upload($ns, $auth = null, $jump = '')
965 global $lang;
966 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
968 echo '<div class="upload">' . NL;
969 if ($auth >= AUTH_UPLOAD) {
970 echo '<p>' . $lang['mediaupload'] . '</p>';
972 media_uploadform($ns, $auth, true);
973 echo '</div>' . NL;
977 * Prints tab that displays search form
979 * @author Kate Arzamastseva <pshns@ukr.net>
981 * @param string $ns
982 * @param null|int $auth permission level
984 function media_tab_search($ns, $auth = null)
986 global $INPUT;
988 $do = $INPUT->str('mediado');
989 $query = $INPUT->str('q');
990 echo '<div class="search">' . NL;
992 media_searchform($ns, $query, true);
993 if ($do == 'searchlist' || $query) {
994 media_searchlist($query, $ns, $auth, true, _media_get_sort_type());
996 echo '</div>' . NL;
1000 * Prints tab that displays mediafile details
1002 * @author Kate Arzamastseva <pshns@ukr.net>
1004 * @param string $image media id
1005 * @param string $ns
1006 * @param null|int $auth permission level
1007 * @param string|int $rev revision timestamp or empty string
1009 function media_tab_view($image, $ns, $auth = null, $rev = '')
1011 global $lang;
1012 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1014 if ($image && $auth >= AUTH_READ) {
1015 $meta = new JpegMeta(mediaFN($image, $rev));
1016 media_preview($image, $auth, $rev, $meta);
1017 media_preview_buttons($image, $auth, $rev);
1018 media_details($image, $auth, $rev, $meta);
1019 } else {
1020 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
1025 * Prints tab that displays form for editing mediafile metadata
1027 * @author Kate Arzamastseva <pshns@ukr.net>
1029 * @param string $image media id
1030 * @param string $ns
1031 * @param null|int $auth permission level
1033 function media_tab_edit($image, $ns, $auth = null)
1035 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1037 if ($image) {
1038 [, $mime] = mimetype($image);
1039 if ($mime == 'image/jpeg') media_metaform($image, $auth);
1044 * Prints tab that displays mediafile revisions
1046 * @author Kate Arzamastseva <pshns@ukr.net>
1048 * @param string $image media id
1049 * @param string $ns
1050 * @param null|int $auth permission level
1052 function media_tab_history($image, $ns, $auth = null)
1054 global $lang;
1055 global $INPUT;
1057 if (is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
1058 $do = $INPUT->str('mediado');
1060 if ($auth >= AUTH_READ && $image) {
1061 if ($do == 'diff') {
1062 (new MediaDiff($image))->show(); //media_diff($image, $ns, $auth);
1063 } else {
1064 $first = $INPUT->int('first', -1);
1065 (new MediaRevisions($image))->show($first);
1067 } else {
1068 echo '<div class="nothing">' . $lang['media_perm_read'] . '</div>' . NL;
1073 * Prints mediafile details
1075 * @param string $image media id
1076 * @param int $auth permission level
1077 * @param int|string $rev revision timestamp or empty string
1078 * @param JpegMeta|bool $meta
1080 * @author Kate Arzamastseva <pshns@ukr.net>
1082 function media_preview($image, $auth, $rev = '', $meta = false)
1085 $size = media_image_preview_size($image, $rev, $meta);
1087 if ($size) {
1088 global $lang;
1089 echo '<div class="image">';
1091 $more = [];
1092 if ($rev) {
1093 $more['rev'] = $rev;
1094 } else {
1095 $t = @filemtime(mediaFN($image));
1096 $more['t'] = $t;
1099 $more['w'] = $size[0];
1100 $more['h'] = $size[1];
1101 $src = ml($image, $more);
1103 echo '<a href="' . $src . '" target="_blank" title="' . $lang['mediaview'] . '">';
1104 echo '<img src="' . $src . '" alt="" style="max-width: ' . $size[0] . 'px;" />';
1105 echo '</a>';
1107 echo '</div>';
1112 * Prints mediafile action buttons
1114 * @author Kate Arzamastseva <pshns@ukr.net>
1116 * @param string $image media id
1117 * @param int $auth permission level
1118 * @param int|string $rev revision timestamp, or empty string
1120 function media_preview_buttons($image, $auth, $rev = '')
1122 global $lang, $conf;
1124 echo '<ul class="actions">';
1126 if ($auth >= AUTH_DELETE && !$rev && file_exists(mediaFN($image))) {
1127 // delete button
1128 $form = new Form([
1129 'id' => 'mediamanager__btn_delete',
1130 'action' => media_managerURL(['delete' => $image], '&'),
1132 $form->addTagOpen('div')->addClass('no');
1133 $form->addButton('', $lang['btn_delete'])->attr('type', 'submit');
1134 $form->addTagClose('div');
1135 echo '<li>';
1136 echo $form->toHTML();
1137 echo '</li>';
1140 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
1141 if ($auth >= $auth_ow && !$rev) {
1142 // upload new version button
1143 $form = new Form([
1144 'id' => 'mediamanager__btn_update',
1145 'action' => media_managerURL(['image' => $image, 'mediado' => 'update'], '&'),
1147 $form->addTagOpen('div')->addClass('no');
1148 $form->addButton('', $lang['media_update'])->attr('type', 'submit');
1149 $form->addTagClose('div');
1150 echo '<li>';
1151 echo $form->toHTML();
1152 echo '</li>';
1155 if ($auth >= AUTH_UPLOAD && $rev && $conf['mediarevisions'] && file_exists(mediaFN($image, $rev))) {
1156 // restore button
1157 $form = new Form([
1158 'id' => 'mediamanager__btn_restore',
1159 'action' => media_managerURL(['image' => $image], '&'),
1161 $form->addTagOpen('div')->addClass('no');
1162 $form->setHiddenField('mediado', 'restore');
1163 $form->setHiddenField('rev', $rev);
1164 $form->addButton('', $lang['media_restore'])->attr('type', 'submit');
1165 $form->addTagClose('div');
1166 echo '<li>';
1167 echo $form->toHTML();
1168 echo '</li>';
1171 echo '</ul>';
1175 * Returns image width and height for mediamanager preview panel
1177 * @author Kate Arzamastseva <pshns@ukr.net>
1178 * @param string $image
1179 * @param int|string $rev
1180 * @param JpegMeta|bool $meta
1181 * @param int $size
1182 * @return array
1184 function media_image_preview_size($image, $rev, $meta = false, $size = 500)
1186 if (
1187 !preg_match("/\.(jpe?g|gif|png)$/", $image)
1188 || !file_exists($filename = mediaFN($image, $rev))
1189 ) return [];
1191 $info = getimagesize($filename);
1192 $w = $info[0];
1193 $h = $info[1];
1195 if ($meta && ($w > $size || $h > $size)) {
1196 $ratio = $meta->getResizeRatio($size, $size);
1197 $w = floor($w * $ratio);
1198 $h = floor($h * $ratio);
1200 return [$w, $h];
1204 * Returns the requested EXIF/IPTC tag from the image meta
1206 * @author Kate Arzamastseva <pshns@ukr.net>
1208 * @param array $tags array with tags, first existing is returned
1209 * @param JpegMeta $meta
1210 * @param string $alt alternative value
1211 * @return string
1213 function media_getTag($tags, $meta = false, $alt = '')
1215 if (!$meta) return $alt;
1216 $info = $meta->getField($tags);
1217 if (!$info) return $alt;
1218 return $info;
1222 * Returns mediafile tags
1224 * @author Kate Arzamastseva <pshns@ukr.net>
1226 * @param JpegMeta $meta
1227 * @return array list of tags of the mediafile
1229 function media_file_tags($meta)
1231 // load the field descriptions
1232 static $fields = null;
1233 if (is_null($fields)) {
1234 $config_files = getConfigFiles('mediameta');
1235 foreach ($config_files as $config_file) {
1236 if (file_exists($config_file)) include($config_file);
1240 $tags = [];
1242 foreach ($fields as $tag) {
1243 $t = [];
1244 if (!empty($tag[0])) $t = [$tag[0]];
1245 if (isset($tag[3]) && is_array($tag[3])) $t = array_merge($t, $tag[3]);
1246 $value = media_getTag($t, $meta);
1247 $tags[] = ['tag' => $tag, 'value' => $value];
1250 return $tags;
1254 * Prints mediafile tags
1256 * @author Kate Arzamastseva <pshns@ukr.net>
1258 * @param string $image image id
1259 * @param int $auth permission level
1260 * @param string|int $rev revision timestamp, or empty string
1261 * @param bool|JpegMeta $meta image object, or create one if false
1263 function media_details($image, $auth, $rev = '', $meta = false)
1265 global $lang;
1267 if (!$meta) $meta = new JpegMeta(mediaFN($image, $rev));
1268 $tags = media_file_tags($meta);
1270 echo '<dl>' . NL;
1271 foreach ($tags as $tag) {
1272 if ($tag['value']) {
1273 $value = cleanText($tag['value']);
1274 echo '<dt>' . $lang[$tag['tag'][1]] . '</dt><dd>';
1275 if ($tag['tag'][2] == 'date') echo dformat($value);
1276 else echo hsc($value);
1277 echo '</dd>' . NL;
1280 echo '</dl>' . NL;
1281 echo '<dl>' . NL;
1282 echo '<dt>' . $lang['reference'] . ':</dt>';
1283 $media_usage = ft_mediause($image, true);
1284 if ($media_usage !== []) {
1285 foreach ($media_usage as $path) {
1286 echo '<dd>' . html_wikilink($path) . '</dd>';
1288 } else {
1289 echo '<dd>' . $lang['nothingfound'] . '</dd>';
1291 echo '</dl>' . NL;
1295 * Shows difference between two revisions of file
1297 * @author Kate Arzamastseva <pshns@ukr.net>
1299 * @param string $image image id
1300 * @param string $ns
1301 * @param int $auth permission level
1302 * @param bool $fromajax
1304 * @deprecated 2020-12-31
1306 function media_diff($image, $ns, $auth, $fromajax = false)
1308 dbg_deprecated('see ' . MediaDiff::class . '::show()');
1312 * Callback for media file diff
1314 * @param array $data event data
1316 * @deprecated 2020-12-31
1318 function _media_file_diff($data)
1320 dbg_deprecated('see ' . MediaDiff::class . '::show()');
1324 * Shows difference between two revisions of image
1326 * @author Kate Arzamastseva <pshns@ukr.net>
1328 * @param string $image
1329 * @param string|int $l_rev revision timestamp, or empty string
1330 * @param string|int $r_rev revision timestamp, or empty string
1331 * @param string $ns
1332 * @param int $auth permission level
1333 * @param bool $fromajax
1334 * @deprecated 2020-12-31
1336 function media_file_diff($image, $l_rev, $r_rev, $ns, $auth, $fromajax)
1338 dbg_deprecated('see ' . MediaDiff::class . '::showFileDiff()');
1342 * Prints two images side by side
1343 * and slider
1345 * @author Kate Arzamastseva <pshns@ukr.net>
1347 * @param string $image image id
1348 * @param int $l_rev revision timestamp, or empty string
1349 * @param int $r_rev revision timestamp, or empty string
1350 * @param array $l_size array with width and height
1351 * @param array $r_size array with width and height
1352 * @param string $type
1353 * @deprecated 2020-12-31
1355 function media_image_diff($image, $l_rev, $r_rev, $l_size, $r_size, $type)
1357 dbg_deprecated('see ' . MediaDiff::class . '::showImageDiff()');
1361 * Restores an old revision of a media file
1363 * @param string $image media id
1364 * @param int $rev revision timestamp or empty string
1365 * @param int $auth
1366 * @return string - file's id
1368 * @author Kate Arzamastseva <pshns@ukr.net>
1370 function media_restore($image, $rev, $auth)
1372 global $conf;
1373 if ($auth < AUTH_UPLOAD || !$conf['mediarevisions']) return false;
1374 $removed = (!file_exists(mediaFN($image)) && file_exists(mediaMetaFN($image, '.changes')));
1375 if (!$image || (!file_exists(mediaFN($image)) && !$removed)) return false;
1376 if (!$rev || !file_exists(mediaFN($image, $rev))) return false;
1377 [, $imime, ] = mimetype($image);
1378 $res = media_upload_finish(
1379 mediaFN($image, $rev),
1380 mediaFN($image),
1381 $image,
1382 $imime,
1383 true,
1384 'copy'
1386 if (is_array($res)) {
1387 msg($res[0], $res[1]);
1388 return false;
1390 return $res;
1394 * List all files found by the search request
1396 * @author Tobias Sarnowski <sarnowski@cosmocode.de>
1397 * @author Andreas Gohr <gohr@cosmocode.de>
1398 * @author Kate Arzamastseva <pshns@ukr.net>
1399 * @triggers MEDIA_SEARCH
1401 * @param string $query
1402 * @param string $ns
1403 * @param null|int $auth
1404 * @param bool $fullscreen
1405 * @param string $sort
1407 function media_searchlist($query, $ns, $auth = null, $fullscreen = false, $sort = 'natural')
1409 global $conf;
1410 global $lang;
1412 $ns = cleanID($ns);
1413 $evdata = [
1414 'ns' => $ns,
1415 'data' => [],
1416 'query' => $query
1418 if (!blank($query)) {
1419 $evt = new Event('MEDIA_SEARCH', $evdata);
1420 if ($evt->advise_before()) {
1421 $dir = utf8_encodeFN(str_replace(':', '/', $evdata['ns']));
1422 $quoted = preg_quote($evdata['query'], '/');
1423 //apply globbing
1424 $quoted = str_replace(['\*', '\?'], ['.*', '.'], $quoted, $count);
1426 //if we use globbing file name must match entirely but may be preceded by arbitrary namespace
1427 if ($count > 0) $quoted = '^([^:]*:)*' . $quoted . '$';
1429 $pattern = '/' . $quoted . '/i';
1430 search(
1431 $evdata['data'],
1432 $conf['mediadir'],
1433 'search_mediafiles',
1434 ['showmsg' => false, 'pattern' => $pattern],
1435 $dir,
1437 $sort
1440 $evt->advise_after();
1441 unset($evt);
1444 if (!$fullscreen) {
1445 echo '<h1 id="media__ns">' . sprintf($lang['searchmedia_in'], hsc($ns) . ':*') . '</h1>' . NL;
1446 media_searchform($ns, $query);
1449 if (!count($evdata['data'])) {
1450 echo '<div class="nothing">' . $lang['nothingfound'] . '</div>' . NL;
1451 } else {
1452 if ($fullscreen) {
1453 echo '<ul class="' . _media_get_list_type() . '">';
1455 foreach ($evdata['data'] as $item) {
1456 if (!$fullscreen) {
1457 // FIXME old call: media_printfile($item,$item['perm'],'',true);
1458 $display = new DisplayRow($item);
1459 $display->relativeDisplay($ns);
1460 $display->show();
1461 } else {
1462 // FIXME old call: media_printfile_thumbs($item,$item['perm'],false,true);
1463 $display = new DisplayTile($item);
1464 $display->relativeDisplay($ns);
1465 echo '<li>';
1466 $display->show();
1467 echo '</li>';
1470 if ($fullscreen) echo '</ul>' . NL;
1475 * Display a media icon
1477 * @param string $filename media id
1478 * @param string $size the size subfolder, if not specified 16x16 is used
1479 * @return string html
1481 function media_printicon($filename, $size = '')
1483 [$ext] = mimetype(mediaFN($filename), false);
1485 if (file_exists(DOKU_INC . 'lib/images/fileicons/' . $size . '/' . $ext . '.png')) {
1486 $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/' . $ext . '.png';
1487 } else {
1488 $icon = DOKU_BASE . 'lib/images/fileicons/' . $size . '/file.png';
1491 return '<img src="' . $icon . '" alt="' . $filename . '" class="icon" />';
1495 * Build link based on the current, adding/rewriting parameters
1497 * @author Kate Arzamastseva <pshns@ukr.net>
1499 * @param array|bool $params
1500 * @param string $amp separator
1501 * @param bool $abs absolute url?
1502 * @param bool $params_array return the parmeters array?
1503 * @return string|array - link or link parameters
1505 function media_managerURL($params = false, $amp = '&amp;', $abs = false, $params_array = false)
1507 global $ID;
1508 global $INPUT;
1510 $gets = ['do' => 'media'];
1511 $media_manager_params = ['tab_files', 'tab_details', 'image', 'ns', 'list', 'sort'];
1512 foreach ($media_manager_params as $x) {
1513 if ($INPUT->has($x)) $gets[$x] = $INPUT->str($x);
1516 if ($params) {
1517 $gets = $params + $gets;
1519 unset($gets['id']);
1520 if (isset($gets['delete'])) {
1521 unset($gets['image']);
1522 unset($gets['tab_details']);
1525 if ($params_array) return $gets;
1527 return wl($ID, $gets, $abs, $amp);
1531 * Print the media upload form if permissions are correct
1533 * @author Andreas Gohr <andi@splitbrain.org>
1534 * @author Kate Arzamastseva <pshns@ukr.net>
1536 * @param string $ns
1537 * @param int $auth permission level
1538 * @param bool $fullscreen
1540 function media_uploadform($ns, $auth, $fullscreen = false)
1542 global $lang;
1543 global $conf;
1544 global $INPUT;
1546 if ($auth < AUTH_UPLOAD) {
1547 echo '<div class="nothing">' . $lang['media_perm_upload'] . '</div>' . NL;
1548 return;
1550 $auth_ow = (($conf['mediarevisions']) ? AUTH_UPLOAD : AUTH_DELETE);
1552 $update = false;
1553 $id = '';
1554 if ($auth >= $auth_ow && $fullscreen && $INPUT->str('mediado') == 'update') {
1555 $update = true;
1556 $id = cleanID($INPUT->str('image'));
1559 // The default HTML upload form
1560 $form = new Form([
1561 'id' => 'dw__upload',
1562 'enctype' => 'multipart/form-data',
1563 'action' => ($fullscreen)
1564 ? media_managerURL(['tab_files' => 'files', 'tab_details' => 'view'], '&')
1565 : DOKU_BASE . 'lib/exe/mediamanager.php',
1567 $form->addTagOpen('div')->addClass('no');
1568 $form->setHiddenField('ns', hsc($ns)); // FIXME hsc required?
1569 $form->addTagOpen('p');
1570 $form->addTextInput('upload', $lang['txt_upload'])->id('upload__file')
1571 ->attrs(['type' => 'file']);
1572 $form->addTagClose('p');
1573 $form->addTagOpen('p');
1574 $form->addTextInput('mediaid', $lang['txt_filename'])->id('upload__name')
1575 ->val(noNS($id));
1576 $form->addButton('', $lang['btn_upload'])->attr('type', 'submit');
1577 $form->addTagClose('p');
1578 if ($auth >= $auth_ow) {
1579 $form->addTagOpen('p');
1580 $attrs = [];
1581 if ($update) $attrs['checked'] = 'checked';
1582 $form->addCheckbox('ow', $lang['txt_overwrt'])->id('dw__ow')->val('1')
1583 ->addClass('check')->attrs($attrs);
1584 $form->addTagClose('p');
1586 $form->addTagClose('div');
1588 if (!$fullscreen) {
1589 echo '<div class="upload">' . $lang['mediaupload'] . '</div>' . DOKU_LF;
1590 } else {
1591 echo DOKU_LF;
1594 echo '<div id="mediamanager__uploader">' . DOKU_LF;
1595 echo $form->toHTML('Upload');
1596 echo '</div>' . DOKU_LF;
1598 echo '<p class="maxsize">';
1599 printf($lang['maxuploadsize'], filesize_h(media_getuploadsize()));
1600 echo ' <a class="allowedmime" href="#">' . $lang['allowedmime'] . '</a>';
1601 echo ' <span>' . implode(', ', array_keys(getMimeTypes())) . '</span>';
1602 echo '</p>' . DOKU_LF;
1606 * Returns the size uploaded files may have
1608 * This uses a conservative approach using the lowest number found
1609 * in any of the limiting ini settings
1611 * @returns int size in bytes
1613 function media_getuploadsize()
1615 $okay = 0;
1617 $post = php_to_byte(@ini_get('post_max_size'));
1618 $suho = php_to_byte(@ini_get('suhosin.post.max_value_length'));
1619 $upld = php_to_byte(@ini_get('upload_max_filesize'));
1621 if ($post && ($post < $okay || $okay === 0)) $okay = $post;
1622 if ($suho && ($suho < $okay || $okay == 0)) $okay = $suho;
1623 if ($upld && ($upld < $okay || $okay == 0)) $okay = $upld;
1625 return $okay;
1629 * Print the search field form
1631 * @author Tobias Sarnowski <sarnowski@cosmocode.de>
1632 * @author Kate Arzamastseva <pshns@ukr.net>
1634 * @param string $ns
1635 * @param string $query
1636 * @param bool $fullscreen
1638 function media_searchform($ns, $query = '', $fullscreen = false)
1640 global $lang;
1642 // The default HTML search form
1643 $form = new Form([
1644 'id' => 'dw__mediasearch',
1645 'action' => ($fullscreen)
1646 ? media_managerURL([], '&')
1647 : DOKU_BASE . 'lib/exe/mediamanager.php',
1649 $form->addTagOpen('div')->addClass('no');
1650 $form->setHiddenField('ns', $ns);
1651 $form->setHiddenField($fullscreen ? 'mediado' : 'do', 'searchlist');
1653 $form->addTagOpen('p');
1654 $form->addTextInput('q', $lang['searchmedia'])
1655 ->attr('title', sprintf($lang['searchmedia_in'], hsc($ns) . ':*'))
1656 ->val($query);
1657 $form->addHTML(' ');
1658 $form->addButton('', $lang['btn_search'])->attr('type', 'submit');
1659 $form->addTagClose('p');
1660 $form->addTagClose('div');
1661 echo $form->toHTML('SearchMedia');
1665 * Build a tree outline of available media namespaces
1667 * @author Andreas Gohr <andi@splitbrain.org>
1669 * @param string $ns
1671 function media_nstree($ns)
1673 global $conf;
1674 global $lang;
1676 // currently selected namespace
1677 $ns = cleanID($ns);
1678 if (empty($ns)) {
1679 global $ID;
1680 $ns = (string)getNS($ID);
1683 $ns_dir = utf8_encodeFN(str_replace(':', '/', $ns));
1685 $data = [];
1686 search($data, $conf['mediadir'], 'search_index', ['ns' => $ns_dir, 'nofiles' => true]);
1688 // wrap a list with the root level around the other namespaces
1689 array_unshift($data, ['level' => 0, 'id' => '', 'open' => 'true', 'label' => '[' . $lang['mediaroot'] . ']']);
1691 // insert the current ns into the hierarchy if it isn't already part of it
1692 $ns_parts = explode(':', $ns);
1693 $tmp_ns = '';
1694 $pos = 0;
1695 foreach ($ns_parts as $level => $part) {
1696 if ($tmp_ns) $tmp_ns .= ':' . $part;
1697 else $tmp_ns = $part;
1699 // find the namespace parts or insert them
1700 while ($data[$pos]['id'] != $tmp_ns) {
1701 if (
1702 $pos >= count($data) ||
1703 ($data[$pos]['level'] <= $level + 1 && Sort::strcmp($data[$pos]['id'], $tmp_ns) > 0)
1705 array_splice($data, $pos, 0, [['level' => $level + 1, 'id' => $tmp_ns, 'open' => 'true']]);
1706 break;
1708 ++$pos;
1712 echo html_buildlist($data, 'idx', 'media_nstree_item', 'media_nstree_li');
1716 * Userfunction for html_buildlist
1718 * Prints a media namespace tree item
1720 * @author Andreas Gohr <andi@splitbrain.org>
1722 * @param array $item
1723 * @return string html
1725 function media_nstree_item($item)
1727 global $INPUT;
1728 $pos = strrpos($item['id'], ':');
1729 $label = substr($item['id'], $pos > 0 ? $pos + 1 : 0);
1730 if (empty($item['label'])) $item['label'] = $label;
1732 $ret = '';
1733 if ($INPUT->str('do') != 'media')
1734 $ret .= '<a href="' . DOKU_BASE . 'lib/exe/mediamanager.php?ns=' . idfilter($item['id']) . '" class="idx_dir">';
1735 else $ret .= '<a href="' . media_managerURL(['ns' => idfilter($item['id'], false), 'tab_files' => 'files'])
1736 . '" class="idx_dir">';
1737 $ret .= $item['label'];
1738 $ret .= '</a>';
1739 return $ret;
1743 * Userfunction for html_buildlist
1745 * Prints a media namespace tree item opener
1747 * @author Andreas Gohr <andi@splitbrain.org>
1749 * @param array $item
1750 * @return string html
1752 function media_nstree_li($item)
1754 $class = 'media level' . $item['level'];
1755 if ($item['open']) {
1756 $class .= ' open';
1757 $img = DOKU_BASE . 'lib/images/minus.gif';
1758 $alt = '−';
1759 } else {
1760 $class .= ' closed';
1761 $img = DOKU_BASE . 'lib/images/plus.gif';
1762 $alt = '+';
1764 // TODO: only deliver an image if it actually has a subtree...
1765 return '<li class="' . $class . '">' .
1766 '<img src="' . $img . '" alt="' . $alt . '" />';
1770 * Resizes or crop the given image to the given size
1772 * @author Andreas Gohr <andi@splitbrain.org>
1774 * @param string $file filename, path to file
1775 * @param string $ext extension
1776 * @param int $w desired width
1777 * @param int $h desired height
1778 * @param bool $crop should a center crop be used?
1779 * @return string path to resized or original size if failed
1781 function media_mod_image($file, $ext, $w, $h = 0, $crop = false)
1783 global $conf;
1784 if (!$h) $h = 0;
1785 // we wont scale up to infinity
1786 if ($w > 2000 || $h > 2000) return $file;
1788 $operation = $crop ? 'crop' : 'resize';
1790 $options = [
1791 'quality' => $conf['jpg_quality'],
1792 'imconvert' => $conf['im_convert'],
1795 $cache = new CacheImageMod($file, $w, $h, $ext, $crop);
1796 if (!$cache->useCache()) {
1797 try {
1798 Slika::run($file, $options)
1799 ->autorotate()
1800 ->$operation($w, $h)
1801 ->save($cache->cache, $ext);
1802 if ($conf['fperm']) @chmod($cache->cache, $conf['fperm']);
1803 } catch (Exception $e) {
1804 Logger::debug($e->getMessage());
1805 return $file;
1809 return $cache->cache;
1813 * Resizes the given image to the given size
1815 * @author Andreas Gohr <andi@splitbrain.org>
1817 * @param string $file filename, path to file
1818 * @param string $ext extension
1819 * @param int $w desired width
1820 * @param int $h desired height
1821 * @return string path to resized or original size if failed
1823 function media_resize_image($file, $ext, $w, $h = 0)
1825 return media_mod_image($file, $ext, $w, $h, false);
1829 * Center crops the given image to the wanted size
1831 * @author Andreas Gohr <andi@splitbrain.org>
1833 * @param string $file filename, path to file
1834 * @param string $ext extension
1835 * @param int $w desired width
1836 * @param int $h desired height
1837 * @return string path to resized or original size if failed
1839 function media_crop_image($file, $ext, $w, $h = 0)
1841 return media_mod_image($file, $ext, $w, $h, true);
1845 * Calculate a token to be used to verify fetch requests for resized or
1846 * cropped images have been internally generated - and prevent external
1847 * DDOS attacks via fetch
1849 * @author Christopher Smith <chris@jalakai.co.uk>
1851 * @param string $id id of the image
1852 * @param int $w resize/crop width
1853 * @param int $h resize/crop height
1854 * @return string token or empty string if no token required
1856 function media_get_token($id, $w, $h)
1858 // token is only required for modified images
1859 if ($w || $h || media_isexternal($id)) {
1860 $token = $id;
1861 if ($w) $token .= '.' . $w;
1862 if ($h) $token .= '.' . $h;
1864 return substr(PassHash::hmac('md5', $token, auth_cookiesalt()), 0, 6);
1867 return '';
1871 * Download a remote file and return local filename
1873 * returns false if download fails. Uses cached file if available and
1874 * wanted
1876 * @author Andreas Gohr <andi@splitbrain.org>
1877 * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
1879 * @param string $url
1880 * @param string $ext extension
1881 * @param int $cache cachetime in seconds
1882 * @return false|string path to cached file
1884 function media_get_from_URL($url, $ext, $cache)
1886 global $conf;
1888 // if no cache or fetchsize just redirect
1889 if ($cache == 0) return false;
1890 if (!$conf['fetchsize']) return false;
1892 $local = getCacheName(strtolower($url), ".media.$ext");
1893 $mtime = @filemtime($local); // 0 if not exists
1895 //decide if download needed:
1896 if (
1897 ($mtime == 0) || // cache does not exist
1898 ($cache != -1 && $mtime < time() - $cache) // 'recache' and cache has expired
1900 if (media_image_download($url, $local)) {
1901 return $local;
1902 } else {
1903 return false;
1907 //if cache exists use it else
1908 if ($mtime) return $local;
1910 //else return false
1911 return false;
1915 * Download image files
1917 * @author Andreas Gohr <andi@splitbrain.org>
1919 * @param string $url
1920 * @param string $file path to file in which to put the downloaded content
1921 * @return bool
1923 function media_image_download($url, $file)
1925 global $conf;
1926 $http = new DokuHTTPClient();
1927 $http->keep_alive = false; // we do single ops here, no need for keep-alive
1929 $http->max_bodysize = $conf['fetchsize'];
1930 $http->timeout = 25; //max. 25 sec
1931 $http->header_regexp = '!\r\nContent-Type: image/(jpe?g|gif|png)!i';
1933 $data = $http->get($url);
1934 if (!$data) return false;
1936 $fileexists = file_exists($file);
1937 $fp = @fopen($file, "w");
1938 if (!$fp) return false;
1939 fwrite($fp, $data);
1940 fclose($fp);
1941 if (!$fileexists && $conf['fperm']) chmod($file, $conf['fperm']);
1943 // check if it is really an image
1944 $info = @getimagesize($file);
1945 if (!$info) {
1946 @unlink($file);
1947 return false;
1950 return true;
1954 * resize images using external ImageMagick convert program
1956 * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
1957 * @author Andreas Gohr <andi@splitbrain.org>
1959 * @param string $ext extension
1960 * @param string $from filename path to file
1961 * @param int $from_w original width
1962 * @param int $from_h original height
1963 * @param string $to path to resized file
1964 * @param int $to_w desired width
1965 * @param int $to_h desired height
1966 * @return bool
1968 function media_resize_imageIM($ext, $from, $from_w, $from_h, $to, $to_w, $to_h)
1970 global $conf;
1972 // check if convert is configured
1973 if (!$conf['im_convert']) return false;
1975 // prepare command
1976 $cmd = $conf['im_convert'];
1977 $cmd .= ' -resize ' . $to_w . 'x' . $to_h . '!';
1978 if ($ext == 'jpg' || $ext == 'jpeg') {
1979 $cmd .= ' -quality ' . $conf['jpg_quality'];
1981 $cmd .= " $from $to";
1983 @exec($cmd, $out, $retval);
1984 if ($retval == 0) return true;
1985 return false;
1989 * crop images using external ImageMagick convert program
1991 * @author Andreas Gohr <andi@splitbrain.org>
1993 * @param string $ext extension
1994 * @param string $from filename path to file
1995 * @param int $from_w original width
1996 * @param int $from_h original height
1997 * @param string $to path to resized file
1998 * @param int $to_w desired width
1999 * @param int $to_h desired height
2000 * @param int $ofs_x offset of crop centre
2001 * @param int $ofs_y offset of crop centre
2002 * @return bool
2003 * @deprecated 2020-09-01
2005 function media_crop_imageIM($ext, $from, $from_w, $from_h, $to, $to_w, $to_h, $ofs_x, $ofs_y)
2007 global $conf;
2008 dbg_deprecated('splitbrain\\Slika');
2010 // check if convert is configured
2011 if (!$conf['im_convert']) return false;
2013 // prepare command
2014 $cmd = $conf['im_convert'];
2015 $cmd .= ' -crop ' . $to_w . 'x' . $to_h . '+' . $ofs_x . '+' . $ofs_y;
2016 if ($ext == 'jpg' || $ext == 'jpeg') {
2017 $cmd .= ' -quality ' . $conf['jpg_quality'];
2019 $cmd .= " $from $to";
2021 @exec($cmd, $out, $retval);
2022 if ($retval == 0) return true;
2023 return false;
2027 * resize or crop images using PHP's libGD support
2029 * @author Andreas Gohr <andi@splitbrain.org>
2030 * @author Sebastian Wienecke <s_wienecke@web.de>
2032 * @param string $ext extension
2033 * @param string $from filename path to file
2034 * @param int $from_w original width
2035 * @param int $from_h original height
2036 * @param string $to path to resized file
2037 * @param int $to_w desired width
2038 * @param int $to_h desired height
2039 * @param int $ofs_x offset of crop centre
2040 * @param int $ofs_y offset of crop centre
2041 * @return bool
2042 * @deprecated 2020-09-01
2044 function media_resize_imageGD($ext, $from, $from_w, $from_h, $to, $to_w, $to_h, $ofs_x = 0, $ofs_y = 0)
2046 global $conf;
2047 dbg_deprecated('splitbrain\\Slika');
2049 if ($conf['gdlib'] < 1) return false; //no GDlib available or wanted
2051 // check available memory
2052 if (!is_mem_available(($from_w * $from_h * 4) + ($to_w * $to_h * 4))) {
2053 return false;
2056 // create an image of the given filetype
2057 $image = false;
2058 if ($ext == 'jpg' || $ext == 'jpeg') {
2059 if (!function_exists("imagecreatefromjpeg")) return false;
2060 $image = @imagecreatefromjpeg($from);
2061 } elseif ($ext == 'png') {
2062 if (!function_exists("imagecreatefrompng")) return false;
2063 $image = @imagecreatefrompng($from);
2064 } elseif ($ext == 'gif') {
2065 if (!function_exists("imagecreatefromgif")) return false;
2066 $image = @imagecreatefromgif($from);
2068 if (!$image) return false;
2070 $newimg = false;
2071 if (($conf['gdlib'] > 1) && function_exists("imagecreatetruecolor") && $ext != 'gif') {
2072 $newimg = @imagecreatetruecolor($to_w, $to_h);
2074 if (!$newimg) $newimg = @imagecreate($to_w, $to_h);
2075 if (!$newimg) {
2076 imagedestroy($image);
2077 return false;
2080 //keep png alpha channel if possible
2081 if ($ext == 'png' && $conf['gdlib'] > 1 && function_exists('imagesavealpha')) {
2082 imagealphablending($newimg, false);
2083 imagesavealpha($newimg, true);
2086 //keep gif transparent color if possible
2087 if ($ext == 'gif' && function_exists('imagefill') && function_exists('imagecolorallocate')) {
2088 if (function_exists('imagecolorsforindex') && function_exists('imagecolortransparent')) {
2089 $transcolorindex = @imagecolortransparent($image);
2090 if ($transcolorindex >= 0) { //transparent color exists
2091 $transcolor = @imagecolorsforindex($image, $transcolorindex);
2092 $transcolorindex = @imagecolorallocate(
2093 $newimg,
2094 $transcolor['red'],
2095 $transcolor['green'],
2096 $transcolor['blue']
2098 @imagefill($newimg, 0, 0, $transcolorindex);
2099 @imagecolortransparent($newimg, $transcolorindex);
2100 } else { //filling with white
2101 $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
2102 @imagefill($newimg, 0, 0, $whitecolorindex);
2104 } else { //filling with white
2105 $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
2106 @imagefill($newimg, 0, 0, $whitecolorindex);
2110 //try resampling first
2111 if (function_exists("imagecopyresampled")) {
2112 if (!@imagecopyresampled($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h)) {
2113 imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
2115 } else {
2116 imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
2119 $okay = false;
2120 if ($ext == 'jpg' || $ext == 'jpeg') {
2121 if (!function_exists('imagejpeg')) {
2122 $okay = false;
2123 } else {
2124 $okay = imagejpeg($newimg, $to, $conf['jpg_quality']);
2126 } elseif ($ext == 'png') {
2127 if (!function_exists('imagepng')) {
2128 $okay = false;
2129 } else {
2130 $okay = imagepng($newimg, $to);
2132 } elseif ($ext == 'gif') {
2133 if (!function_exists('imagegif')) {
2134 $okay = false;
2135 } else {
2136 $okay = imagegif($newimg, $to);
2140 // destroy GD image resources
2141 imagedestroy($image);
2142 imagedestroy($newimg);
2144 return $okay;
2148 * Return other media files with the same base name
2149 * but different extensions.
2151 * @param string $src - ID of media file
2152 * @param string[] $exts - alternative extensions to find other files for
2153 * @return array - array(mime type => file ID)
2155 * @author Anika Henke <anika@selfthinker.org>
2157 function media_alternativefiles($src, $exts)
2160 $files = [];
2161 [$srcExt, /* srcMime */] = mimetype($src);
2162 $filebase = substr($src, 0, -1 * (strlen($srcExt) + 1));
2164 foreach ($exts as $ext) {
2165 $fileid = $filebase . '.' . $ext;
2166 $file = mediaFN($fileid);
2167 if (file_exists($file)) {
2168 [/* fileExt */, $fileMime] = mimetype($file);
2169 $files[$fileMime] = $fileid;
2172 return $files;
2176 * Check if video/audio is supported to be embedded.
2178 * @param string $mime - mimetype of media file
2179 * @param string $type - type of media files to check ('video', 'audio', or null for all)
2180 * @return boolean
2182 * @author Anika Henke <anika@selfthinker.org>
2184 function media_supportedav($mime, $type = null)
2186 $supportedAudio = [
2187 'ogg' => 'audio/ogg',
2188 'mp3' => 'audio/mpeg',
2189 'wav' => 'audio/wav'
2191 $supportedVideo = [
2192 'webm' => 'video/webm',
2193 'ogv' => 'video/ogg',
2194 'mp4' => 'video/mp4'
2196 if ($type == 'audio') {
2197 $supportedAv = $supportedAudio;
2198 } elseif ($type == 'video') {
2199 $supportedAv = $supportedVideo;
2200 } else {
2201 $supportedAv = array_merge($supportedAudio, $supportedVideo);
2203 return in_array($mime, $supportedAv);
2207 * Return track media files with the same base name
2208 * but extensions that indicate kind and lang.
2209 * ie for foo.webm search foo.sub.lang.vtt, foo.cap.lang.vtt...
2211 * @param string $src - ID of media file
2212 * @return array - array(mediaID => array( kind, srclang ))
2214 * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
2216 function media_trackfiles($src)
2218 $kinds = [
2219 'sub' => 'subtitles',
2220 'cap' => 'captions',
2221 'des' => 'descriptions',
2222 'cha' => 'chapters',
2223 'met' => 'metadata'
2226 $files = [];
2227 $re = '/\\.(sub|cap|des|cha|met)\\.([^.]+)\\.vtt$/';
2228 $baseid = pathinfo($src, PATHINFO_FILENAME);
2229 $pattern = mediaFN($baseid) . '.*.*.vtt';
2230 $list = glob($pattern);
2231 foreach ($list as $track) {
2232 if (preg_match($re, $track, $matches)) {
2233 $files[$baseid . '.' . $matches[1] . '.' . $matches[2] . '.vtt'] = [$kinds[$matches[1]], $matches[2]];
2236 return $files;
2239 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */