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