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