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