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