xref: /dokuwiki/inc/media.php (revision 1462e3ae97d9af23cc143bfaf7a48143673b3d40)
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
9if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/');
10if(!defined('NL')) define('NL',"\n");
11
12require_once(DOKU_INC.'inc/html.php');
13require_once(DOKU_INC.'inc/search.php');
14require_once(DOKU_INC.'inc/JpegMeta.php');
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 */
24function media_filesinuse($data,$id){
25    global $lang;
26    echo '<h1>'.$lang['reference'].' <code>'.hsc(noNS($id)).'</code></h1>';
27    echo '<p>'.hsc($lang['ref_inuse']).'</p>';
28
29    $hidden=0; //count of hits without read permission
30    usort($data,'sort_search_fulltext');
31    foreach($data as $row){
32        if(auth_quickaclcheck($row['id']) >= AUTH_READ){
33            echo '<div class="search_result">';
34            echo '<span class="mediaref_ref">'.$row['id'].'</span>';
35            echo ': <span class="search_cnt">'.$row['count'].' '.$lang['hits'].'</span><br />';
36            echo '<div class="search_snippet">'.$row['snippet'].'</div>';
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 */
51function media_metasave($id,$auth,$data){
52    if($auth < AUTH_UPLOAD) return false;
53    if(!checkSecurityToken()) return false;
54    global $lang;
55    global $conf;
56    $src = mediaFN($id);
57
58    $meta = new JpegMeta($src);
59    $meta->_parseAll();
60
61    foreach($data as $key => $val){
62        $val=trim($val);
63        if(empty($val)){
64            $meta->deleteField($key);
65        }else{
66            $meta->setField($key,$val);
67        }
68    }
69
70    if($meta->save()){
71        if($conf['fperm']) chmod($src, $conf['fperm']);
72        msg($lang['metasaveok'],1);
73        return $id;
74    }else{
75        msg($lang['metasaveerr'],-1);
76        return false;
77    }
78}
79
80/**
81 * Display the form to edit image meta data
82 *
83 * @author Andreas Gohr <andi@splitbrain.org>
84 */
85function media_metaform($id,$auth){
86    if($auth < AUTH_UPLOAD) return false;
87    global $lang;
88
89    // load the field descriptions
90    static $fields = null;
91    if(is_null($fields)){
92        include(DOKU_CONF.'mediameta.php');
93        if(@file_exists(DOKU_CONF.'mediameta.local.php')){
94            include(DOKU_CONF.'mediameta.local.php');
95        }
96    }
97
98    $src = mediaFN($id);
99
100    // output
101    echo '<h1>'.hsc(noNS($id)).'</h1>'.NL;
102    echo '<form action="'.DOKU_BASE.'lib/exe/mediamanager.php" accept-charset="utf-8" method="post" class="meta">'.NL;
103    formSecurityToken();
104    foreach($fields as $key => $field){
105        // get current value
106        $tags = array($field[0]);
107        if(is_array($field[3])) $tags = array_merge($tags,$field[3]);
108        $value = tpl_img_getTag($tags,'',$src);
109
110        // prepare attributes
111        $p = array();
112        $p['class'] = 'edit';
113        $p['id']    = 'meta__'.$key;
114        $p['name']  = 'meta['.$field[0].']';
115
116        // put label
117        echo '<div class="metafield">';
118        echo '<label for="meta__'.$key.'">';
119        echo ($lang[$field[1]]) ? $lang[$field[1]] : $field[1];
120        echo ':</label>';
121
122        // put input field
123        if($field[2] == 'text'){
124            $p['value'] = $value;
125            $p['type']  = 'text';
126            $att = buildAttributes($p);
127            echo "<input $att/>".NL;
128        }else{
129            $att = buildAttributes($p);
130            echo "<textarea $att rows=\"6\" cols=\"50\">".formText($value).'</textarea>'.NL;
131        }
132        echo '</div>'.NL;
133    }
134    echo '<div class="buttons">'.NL;
135    echo '<input type="hidden" name="img" value="'.hsc($id).'" />'.NL;
136    echo '<input name="do[save]" type="submit" value="'.$lang['btn_save'].
137         '" title="ALT+S" accesskey="s" class="button" />'.NL;
138    echo '<input name="do[cancel]" type="submit" value="'.$lang['btn_cancel'].
139         '" title="ALT+C" accesskey="c" class="button" />'.NL;
140    echo '</div>'.NL;
141    echo '</form>'.NL;
142}
143
144/**
145 * Handles media file deletions
146 *
147 * If configured, checks for media references before deletion
148 *
149 * @author Andreas Gohr <andi@splitbrain.org>
150 * @return mixed false on error, true on delete or array with refs
151 */
152function media_delete($id,$auth){
153    if($auth < AUTH_DELETE) return false;
154    if(!checkSecurityToken()) return false;
155    global $conf;
156    global $lang;
157
158    $mediareferences = array();
159    if($conf['refcheck']){
160        search($mediareferences,$conf['datadir'],'search_reference',array('query' => $id));
161    }
162
163    if(!count($mediareferences)){
164        $file = mediaFN($id);
165        if(@unlink($file)){
166            msg(str_replace('%s',noNS($id),$lang['deletesucc']),1);
167            io_sweepNS($id,'mediadir');
168            return true;
169        }
170        //something went wrong
171        msg(str_replace('%s',$file,$lang['deletefail']),-1);
172        return false;
173    }elseif(!$conf['refshow']){
174        msg(str_replace('%s',noNS($id),$lang['mediainuse']),0);
175        return false;
176    }
177
178    return $mediareferences;
179}
180
181/**
182 * Handles media file uploads
183 *
184 * This generates an action event and delegates to _media_upload_action().
185 * Action plugins are allowed to pre/postprocess the uploaded file.
186 * (The triggered event is preventable.)
187 *
188 * Event data:
189 * $data[0]     fn_tmp: the temporary file name (read from $_FILES)
190 * $data[1]     fn: the file name of the uploaded file
191 * $data[2]     id: the future directory id of the uploaded file
192 * $data[3]     imime: the mimetype of the uploaded file
193 *
194 * @triggers MEDIA_UPLOAD_FINISH
195 * @author Andreas Gohr <andi@splitbrain.org>
196 * @author Michael Klier <chi@chimeric.de>
197 * @return mixed false on error, id of the new file on success
198 */
199function media_upload($ns,$auth){
200    if($auth < AUTH_UPLOAD) return false;
201    if(!checkSecurityToken()) return false;
202    require_once(DOKU_INC.'inc/confutils.php');
203    global $lang;
204    global $conf;
205
206    // get file and id
207    $id   = $_POST['id'];
208    $file = $_FILES['upload'];
209    if(empty($id)) $id = $file['name'];
210
211    // check extensions
212    list($fext,$fmime) = mimetype($file['name']);
213    list($iext,$imime) = mimetype($id);
214    if($fext && !$iext){
215        // no extension specified in id - read original one
216        $id   .= '.'.$fext;
217        $imime = $fmime;
218    }elseif($fext && $fext != $iext){
219        // extension was changed, print warning
220        msg(sprintf($lang['mediaextchange'],$fext,$iext));
221    }
222
223    // get filename
224    $id   = cleanID($ns.':'.$id);
225    $fn   = mediaFN($id);
226
227    // get filetype regexp
228    $types = array_keys(getMimeTypes());
229    $types = array_map(create_function('$q','return preg_quote($q,"/");'),$types);
230    $regex = join('|',$types);
231
232    // because a temp file was created already
233    if(preg_match('/\.('.$regex.')$/i',$fn)){
234        //check for overwrite
235        if(@file_exists($fn) && (!$_POST['ow'] || $auth < AUTH_DELETE)){
236            msg($lang['uploadexist'],0);
237            return false;
238        }
239        // check for valid content
240        $ok = media_contentcheck($file['tmp_name'],$imime);
241        if($ok == -1){
242            msg(sprintf($lang['uploadbadcontent'],".$iext"),-1);
243            return false;
244        }elseif($ok == -2){
245            msg($lang['uploadspam'],-1);
246            return false;
247        }elseif($ok == -3){
248            msg($lang['uploadxss'],-1);
249            return false;
250        }
251
252        // prepare event data
253        $data[0] = $file['tmp_name'];
254        $data[1] = $fn;
255        $data[2] = $id;
256        $data[3] = $imime;
257
258        // trigger event
259        return trigger_event('MEDIA_UPLOAD_FINISH', $data, '_media_upload_action', true);
260
261    }else{
262        msg($lang['uploadwrong'],-1);
263    }
264    return false;
265}
266
267/**
268 * Callback adapter for media_upload_finish()
269 * @author Michael Klier <chi@chimeric.de>
270 */
271function _media_upload_action($data) {
272    // fixme do further sanity tests of given data?
273    if(is_array($data) && count($data)===4) {
274        return media_upload_finish($data[0], $data[1], $data[2], $data[3]);
275    } else {
276        return false; //callback error
277    }
278}
279
280/**
281 * Saves an uploaded media file
282 *
283 * @author Andreas Gohr <andi@splitbrain.org>
284 * @author Michael Klier <chi@chimeric.de>
285 */
286function media_upload_finish($fn_tmp, $fn, $id, $imime) {
287    global $conf;
288    global $lang;
289
290    // prepare directory
291    io_createNamespace($id, 'media');
292
293    if(move_uploaded_file($fn_tmp, $fn)) {
294        // Set the correct permission here.
295        // Always chmod media because they may be saved with different permissions than expected from the php umask.
296        // (Should normally chmod to $conf['fperm'] only if $conf['fperm'] is set.)
297        chmod($fn, $conf['fmode']);
298        msg($lang['uploadsucc'],1);
299        media_notify($id,$fn,$imime);
300        return $id;
301    }else{
302        msg($lang['uploadfail'],-1);
303    }
304}
305
306/**
307 * This function checks if the uploaded content is really what the
308 * mimetype says it is. We also do spam checking for text types here.
309 *
310 * We need to do this stuff because we can not rely on the browser
311 * to do this check correctly. Yes, IE is broken as usual.
312 *
313 * @author Andreas Gohr <andi@splitbrain.org>
314 * @link   http://www.splitbrain.org/blog/2007-02/12-internet_explorer_facilitates_cross_site_scripting
315 * @fixme  check all 26 magic IE filetypes here?
316 */
317function media_contentcheck($file,$mime){
318    global $conf;
319    if($conf['iexssprotect']){
320        $fh = @fopen($file, 'rb');
321        if($fh){
322            $bytes = fread($fh, 256);
323            fclose($fh);
324            if(preg_match('/<(script|a|img|html|body|iframe)[\s>]/i',$bytes)){
325                return -3;
326            }
327        }
328    }
329    if(substr($mime,0,6) == 'image/'){
330        $info = @getimagesize($file);
331        if($mime == 'image/gif' && $info[2] != 1){
332            return -1;
333        }elseif($mime == 'image/jpeg' && $info[2] != 2){
334            return -1;
335        }elseif($mime == 'image/png' && $info[2] != 3){
336            return -1;
337        }
338        # fixme maybe check other images types as well
339    }elseif(substr($mime,0,5) == 'text/'){
340        global $TEXT;
341        $TEXT = io_readFile($file);
342        if(checkwordblock()){
343            return -2;
344        }
345    }
346    return 0;
347}
348
349/**
350 * Send a notify mail on uploads
351 *
352 * @author Andreas Gohr <andi@splitbrain.org>
353 */
354function media_notify($id,$file,$mime){
355    global $lang;
356    global $conf;
357    if(empty($conf['notify'])) return; //notify enabled?
358
359    $text = rawLocale('uploadmail');
360    $text = str_replace('@DATE@',date($conf['dformat']),$text);
361    $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
362    $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
363    $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
364    $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
365    $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
366    $text = str_replace('@MIME@',$mime,$text);
367    $text = str_replace('@MEDIA@',ml($id,'',true,'&',true),$text);
368    $text = str_replace('@SIZE@',filesize_h(filesize($file)),$text);
369
370    $from = $conf['mailfrom'];
371    $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
372    $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
373    $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
374
375    $subject = '['.$conf['title'].'] '.$lang['mail_upload'].' '.$id;
376
377    mail_send($conf['notify'],$subject,$text,$from);
378}
379
380/**
381 * List all files in a given Media namespace
382 */
383function media_filelist($ns,$auth=null,$jump=''){
384    global $conf;
385    global $lang;
386    $ns = cleanID($ns);
387
388    // check auth our self if not given (needed for ajax calls)
389    if(is_null($auth)) $auth = auth_quickaclcheck("$ns:*");
390
391    echo '<h1 id="media__ns">:'.hsc($ns).'</h1>'.NL;
392
393    if($auth < AUTH_READ){
394        // FIXME: print permission warning here instead?
395        echo '<div class="nothing">'.$lang['nothingfound'].'</div>'.NL;
396        return;
397    }
398
399    media_uploadform($ns, $auth);
400
401    $dir = utf8_encodeFN(str_replace(':','/',$ns));
402    $data = array();
403    search($data,$conf['mediadir'],'search_media',array(),$dir);
404
405    if(!count($data)){
406        echo '<div class="nothing">'.$lang['nothingfound'].'</div>'.NL;
407        return;
408    }
409
410    foreach($data as $item){
411        media_printfile($item,$auth,$jump);
412    }
413}
414
415/**
416 * Print action links for a file depending on filetype
417 * and available permissions
418 *
419 * @todo contains inline javascript
420 */
421function media_fileactions($item,$auth){
422    global $lang;
423
424    // view button
425    $link = ml($item['id'],'',true);
426    echo ' <a href="'.$link.'" target="_blank"><img src="'.DOKU_BASE.'lib/images/magnifier.png" '.
427         'alt="'.$lang['mediaview'].'" title="'.$lang['mediaview'].'" class="btn" /></a>';
428
429
430    // no further actions if not writable
431    if(!$item['writable']) return;
432
433    // delete button
434    if($auth >= AUTH_DELETE){
435        $ask  = addslashes($lang['del_confirm']).'\\n';
436        $ask .= addslashes($item['id']);
437
438        echo ' <a href="'.DOKU_BASE.'lib/exe/mediamanager.php?delete='.rawurlencode($item['id']).
439             '&amp;sectok='.getSecurityToken().'" '.
440             'onclick="return confirm(\''.$ask.'\')" onkeypress="return confirm(\''.$ask.'\')">'.
441             '<img src="'.DOKU_BASE.'lib/images/trash.png" alt="'.$lang['btn_delete'].'" '.
442             'title="'.$lang['btn_delete'].'" class="btn" /></a>';
443    }
444
445    // edit button
446    if($auth >= AUTH_UPLOAD && $item['isimg'] && $item['meta']->getField('File.Mime') == 'image/jpeg'){
447        echo ' <a href="'.DOKU_BASE.'lib/exe/mediamanager.php?edit='.rawurlencode($item['id']).'">'.
448             '<img src="'.DOKU_BASE.'lib/images/pencil.png" alt="'.$lang['metaedit'].'" '.
449             'title="'.$lang['metaedit'].'" class="btn" /></a>';
450    }
451
452}
453
454/**
455 * Formats and prints one file in the list
456 */
457function media_printfile($item,$auth,$jump){
458    global $lang;
459    global $conf;
460
461    // Prepare zebra coloring
462    // I always wanted to use this variable name :-D
463    static $twibble = 1;
464    $twibble *= -1;
465    $zebra = ($twibble == -1) ? 'odd' : 'even';
466
467    // Automatically jump to recent action
468    if($jump == $item['id']) {
469        $jump = ' id="scroll__here" ';
470    }else{
471        $jump = '';
472    }
473
474    // Prepare fileicons
475    list($ext,$mime) = mimetype($item['file']);
476    $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
477    $class = 'select mediafile mf_'.$class;
478
479    // Prepare filename
480    $file = utf8_decodeFN($item['file']);
481
482    // Prepare info
483    $info = '';
484    if($item['isimg']){
485        $info .= (int) $item['meta']->getField('File.Width');
486        $info .= '&#215;';
487        $info .= (int) $item['meta']->getField('File.Height');
488        $info .= ' ';
489    }
490    $info .= '<i>'.date($conf['dformat'],$item['mtime']).'</i>';
491    $info .= ' ';
492    $info .= filesize_h($item['size']);
493
494    // ouput
495    echo '<div class="'.$zebra.'"'.$jump.'>'.NL;
496    echo '<a name="h_'.$item['id'].'" class="'.$class.'">'.$file.'</a> ';
497    echo '<span class="info">('.$info.')</span>'.NL;
498    media_fileactions($item,$auth);
499    echo '<div class="example" id="ex_'.str_replace(':','_',$item['id']).'">';
500    echo $lang['mediausage'].' <code>{{:'.$item['id'].'}}</code>';
501    echo '</div>';
502    if($item['isimg']) media_printimgdetail($item);
503    echo '<div class="clearer"></div>'.NL;
504    echo '</div>'.NL;
505}
506
507/**
508 * Prints a thumbnail and metainfos
509 */
510function media_printimgdetail($item){
511    // prepare thumbnail
512    $w = (int) $item['meta']->getField('File.Width');
513    $h = (int) $item['meta']->getField('File.Height');
514    if($w>120 || $h>120){
515        $ratio = $item['meta']->getResizeRatio(120);
516        $w = floor($w * $ratio);
517        $h = floor($h * $ratio);
518    }
519    $src = ml($item['id'],array('w'=>$w,'h'=>$h));
520    $p = array();
521    $p['width']  = $w;
522    $p['height'] = $h;
523    $p['alt']    = $item['id'];
524    $p['class']  = 'thumb';
525    $att = buildAttributes($p);
526
527    // output
528    echo '<div class="detail">';
529    echo '<div class="thumb">';
530    echo '<a name="d_'.$item['id'].'" class="select">';
531    echo '<img src="'.$src.'" '.$att.' />';
532    echo '</a>';
533    echo '</div>';
534
535    // read EXIF/IPTC data
536    $t = $item['meta']->getField('IPTC.Headline');
537    $d = $item['meta']->getField(array('IPTC.Caption','EXIF.UserComment',
538                                       'EXIF.TIFFImageDescription',
539                                       'EXIF.TIFFUserComment'));
540    if(utf8_strlen($d) > 250) $d = utf8_substr($d,0,250).'...';
541    $k = $item['meta']->getField(array('IPTC.Keywords','IPTC.Category'));
542
543    // print EXIF/IPTC data
544    if($t || $d || $k ){
545        echo '<p>';
546        if($t) echo '<strong>'.htmlspecialchars($t).'</strong><br />';
547        if($d) echo htmlspecialchars($d).'<br />';
548        if($t) echo '<em>'.htmlspecialchars($k).'</em>';
549        echo '</p>';
550    }
551    echo '</div>';
552}
553
554/**
555 * Print the media upload form if permissions are correct
556 *
557 * @author Andreas Gohr <andi@splitbrain.org>
558 */
559function media_uploadform($ns, $auth){
560    global $lang;
561
562    if($auth < AUTH_UPLOAD) return; //fixme print info on missing permissions?
563
564    ?>
565    <div class="upload"><?php echo $lang['mediaupload']?></div>
566    <form action="<?php echo DOKU_BASE?>lib/exe/mediamanager.php"
567          method="post" enctype="multipart/form-data" class="upload">
568      <fieldset>
569        <legend class="hidden"><?php echo $lang['btn_upload']?></legend>
570        <input type="hidden" name="ns" value="<?php echo hsc($ns)?>" />
571        <?php formSecurityToken();?>
572        <p>
573          <label for="upload__file"><?php echo $lang['txt_upload']?>:</label>
574          <input type="file" name="upload" class="edit" id="upload__file" />
575        </p>
576
577        <p>
578          <label for="upload__name"><?php echo $lang['txt_filename']?>:</label>
579          <span class="nowrap">
580          <input type="text" name="id" class="edit" id="upload__name" /><input
581                 type="submit" class="button" value="<?php echo $lang['btn_upload']?>"
582                 accesskey="s" />
583          </span>
584        </p>
585
586        <?php if($auth >= AUTH_DELETE){?>
587            <p>
588              <input type="checkbox" name="ow" value="1" id="dw__ow" class="check" />
589              <label for="dw__ow" class="check"><?php echo $lang['txt_overwrt']?></label>
590            </p>
591        <?php }?>
592      </fieldset>
593    </form>
594    <?php
595}
596
597
598
599/**
600 * Build a tree outline of available media namespaces
601 *
602 * @author Andreas Gohr <andi@splitbrain.org>
603 */
604function media_nstree($ns){
605    global $conf;
606    global $lang;
607
608    // currently selected namespace
609    $ns  = cleanID($ns);
610    if(empty($ns)){
611        $ns = dirname(str_replace(':','/',$ID));
612        if($ns == '.') $ns ='';
613    }
614    $ns  = utf8_encodeFN(str_replace(':','/',$ns));
615
616    $data = array();
617    search($data,$conf['mediadir'],'search_index',array('ns' => $ns, 'nofiles' => true));
618
619    // wrap a list with the root level around the other namespaces
620    $item = array( 'level' => 0, 'id' => '',
621                   'open' =>'true', 'label' => '['.$lang['mediaroot'].']');
622
623    echo '<ul class="idx">';
624    echo media_nstree_li($item);
625    echo media_nstree_item($item);
626    echo html_buildlist($data,'idx','media_nstree_item','media_nstree_li');
627    echo '</li>';
628    echo '</ul>';
629}
630
631/**
632 * Userfunction for html_buildlist
633 *
634 * Prints a media namespace tree item
635 *
636 * @author Andreas Gohr <andi@splitbrain.org>
637 */
638function media_nstree_item($item){
639    $pos   = strrpos($item['id'], ':');
640    $label = substr($item['id'], $pos > 0 ? $pos + 1 : 0);
641    if(!$item['label']) $item['label'] = $label;
642
643    $ret  = '';
644    $ret .= '<a href="'.DOKU_BASE.'lib/exe/mediamanager.php?ns='.idfilter($item['id']).'" class="idx_dir">';
645    $ret .= $item['label'];
646    $ret .= '</a>';
647    return $ret;
648}
649
650/**
651 * Userfunction for html_buildlist
652 *
653 * Prints a media namespace tree item opener
654 *
655 * @author Andreas Gohr <andi@splitbrain.org>
656 */
657function media_nstree_li($item){
658    $class='media level'.$item['level'];
659    if($item['open']){
660        $class .= ' open';
661        $img   = DOKU_BASE.'lib/images/minus.gif';
662        $alt   = '&minus;';
663    }else{
664        $class .= ' closed';
665        $img   = DOKU_BASE.'lib/images/plus.gif';
666        $alt   = '+';
667    }
668    return '<li class="'.$class.'">'.
669           '<img src="'.$img.'" alt="'.$alt.'" />';
670}
671