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