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