xref: /dokuwiki/lib/exe/fetch.php (revision 640f0ac18e2bb5e5b2c8d04c7502aa5642134b4e)
1<?php
2/**
3 * DokuWiki media passthrough file
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9  if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../');
10  define('DOKU_DISABLE_GZIP_OUTPUT', 1);
11  require_once(DOKU_INC.'inc/init.php');
12  require_once(DOKU_INC.'inc/common.php');
13  require_once(DOKU_INC.'inc/pageutils.php');
14  require_once(DOKU_INC.'inc/confutils.php');
15  require_once(DOKU_INC.'inc/auth.php');
16  //close sesseion
17  session_write_close();
18  if(!defined('CHUNK_SIZE')) define('CHUNK_SIZE',16*1024);
19
20  $mimetypes = getMimeTypes();
21
22  //get input
23  $MEDIA  = stripctl(getID('media',false)); // no cleaning except control chars - maybe external
24  $CACHE  = calc_cache($_REQUEST['cache']);
25  $WIDTH  = (int) $_REQUEST['w'];
26  $HEIGHT = (int) $_REQUEST['h'];
27  list($EXT,$MIME) = mimetype($MEDIA);
28  if($EXT === false){
29    $EXT  = 'unknown';
30    $MIME = 'application/octet-stream';
31  }
32
33  //media to local file
34  if(preg_match('#^(https?)://#i',$MEDIA)){
35    //handle external images
36    if(strncmp($MIME,'image/',6) == 0) $FILE = get_from_URL($MEDIA,$EXT,$CACHE);
37    if(!$FILE){
38      //download failed - redirect to original URL
39      header('Location: '.$MEDIA);
40      exit;
41    }
42  }else{
43    $MEDIA = cleanID($MEDIA);
44    if(empty($MEDIA)){
45      header("HTTP/1.0 400 Bad Request");
46      print 'Bad request';
47      exit;
48    }
49
50    //check permissions (namespace only)
51    if(auth_quickaclcheck(getNS($MEDIA).':X') < AUTH_READ){
52      header("HTTP/1.0 401 Unauthorized");
53      //fixme add some image for imagefiles
54      print 'Unauthorized';
55      exit;
56    }
57    $FILE  = mediaFN($MEDIA);
58  }
59
60  //check file existance
61  if(!@file_exists($FILE)){
62    header("HTTP/1.0 404 Not Found");
63    //FIXME add some default broken image
64    print 'Not Found';
65    exit;
66  }
67
68  //handle image resizing/cropping
69  if((substr($MIME,0,5) == 'image') && $WIDTH){
70    if($HEIGHT){
71        $FILE = get_cropped($FILE,$EXT,$WIDTH,$HEIGHT);
72    }else{
73        $FILE = get_resized($FILE,$EXT,$WIDTH,$HEIGHT);
74    }
75  }
76
77  // finally send the file to the client
78  sendFile($FILE,$MIME,$CACHE);
79
80/* ------------------------------------------------------------------------ */
81
82/**
83 * Set headers and send the file to the client
84 *
85 * @author Andreas Gohr <andi@splitbrain.org>
86 * @author Ben Coburn <btcoburn@silicodon.net>
87 */
88function sendFile($file,$mime,$cache){
89  global $conf;
90  $fmtime = filemtime($file);
91  // send headers
92  header("Content-Type: $mime");
93  // smart http caching headers
94  if ($cache==-1) {
95    // cache
96    // cachetime or one hour
97    header('Expires: '.gmdate("D, d M Y H:i:s", time()+max($conf['cachetime'], 3600)).' GMT');
98    header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.max($conf['cachetime'], 3600));
99    header('Pragma: public');
100  } else if ($cache>0) {
101    // recache
102    // remaining cachetime + 10 seconds so the newly recached media is used
103    header('Expires: '.gmdate("D, d M Y H:i:s", $fmtime+$conf['cachetime']+10).' GMT');
104    header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.max($fmtime-time()+$conf['cachetime']+10, 0));
105    header('Pragma: public');
106  } else if ($cache==0) {
107    // nocache
108    header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0');
109    header('Pragma: public');
110  }
111  //send important headers first, script stops here if '304 Not Modified' response
112  http_conditionalRequest($fmtime);
113
114
115  //application mime type is downloadable
116  if(substr($mime,0,11) == 'application'){
117    header('Content-Disposition: attachment; filename="'.basename($file).'";');
118  }
119
120  //use x-sendfile header to pass the delivery to compatible webservers
121  if($conf['xsendfile'] == 1){
122    header("X-LIGHTTPD-send-file: $file");
123    exit;
124  }elseif($conf['xsendfile'] == 2){
125    header("X-Sendfile: $file");
126    exit;
127  }elseif($conf['xsendfile'] == 3){
128    header("X-Accel-Redirect: $file");
129    exit;
130  }
131
132  //support download continueing
133  header('Accept-Ranges: bytes');
134  list($start,$len) = http_rangeRequest(filesize($file));
135
136  // send file contents
137  $fp = @fopen($file,"rb");
138  if($fp){
139    fseek($fp,$start); //seek to start of range
140
141    $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len;
142    while (!feof($fp) && $chunk > 0) {
143      @set_time_limit(30); // large files can take a lot of time
144      print fread($fp, $chunk);
145      flush();
146      $len -= $chunk;
147      $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len;
148    }
149    fclose($fp);
150  }else{
151    header("HTTP/1.0 500 Internal Server Error");
152    print "Could not read $file - bad permissions?";
153  }
154}
155
156/**
157 * Checks and sets headers to handle range requets
158 *
159 * @author  Andreas Gohr <andi@splitbrain.org>
160 * @returns array The start byte and the amount of bytes to send
161 */
162function http_rangeRequest($size){
163  if(!isset($_SERVER['HTTP_RANGE'])){
164    // no range requested - send the whole file
165    header("Content-Length: $size");
166    return array(0,$size);
167  }
168
169  $t = explode('=', $_SERVER['HTTP_RANGE']);
170  if (!$t[0]=='bytes') {
171    // we only understand byte ranges - send the whole file
172    header("Content-Length: $size");
173    return array(0,$size);
174  }
175
176  $r = explode('-', $t[1]);
177  $start = (int)$r[0];
178  $end = (int)$r[1];
179  if (!$end) $end = $size - 1;
180  if ($start > $end || $start > $size || $end > $size){
181    header('HTTP/1.1 416 Requested Range Not Satisfiable');
182    print 'Bad Range Request!';
183    exit;
184  }
185
186  $tot = $end - $start + 1;
187  header('HTTP/1.1 206 Partial Content');
188  header("Content-Range: bytes {$start}-{$end}/{$size}");
189  header("Content-Length: $tot");
190
191  return array($start,$tot);
192}
193
194/**
195 * Resizes the given image to the given size
196 *
197 * @author  Andreas Gohr <andi@splitbrain.org>
198 */
199function get_resized($file, $ext, $w, $h=0){
200  global $conf;
201
202  $info  = getimagesize($file);
203  if(!$h) $h = round(($w * $info[1]) / $info[0]);
204
205  // we wont scale up to infinity
206  if($w > 2000 || $h > 2000) return $file;
207
208  //cache
209  $local = getCacheName($file,'.media.'.$w.'x'.$h.'.'.$ext);
210  $mtime = @filemtime($local); // 0 if not exists
211
212  if( $mtime > filemtime($file) ||
213      resize_imageIM($ext,$file,$info[0],$info[1],$local,$w,$h) ||
214      resize_imageGD($ext,$file,$info[0],$info[1],$local,$w,$h) ){
215    if($conf['fperm']) chmod($local, $conf['fperm']);
216    return $local;
217  }
218  //still here? resizing failed
219  return $file;
220}
221
222/**
223 * Crops the given image to the wanted ratio, then calls get_resized to scale it
224 * to the wanted size
225 *
226 * Crops are centered horizontally but prefer the upper third of an vertical
227 * image because most pics are more interesting in that area (rule of thirds)
228 *
229 * @author  Andreas Gohr <andi@splitbrain.org>
230 */
231function get_cropped($file, $ext, $w, $h=0){
232  global $conf;
233
234  if(!$h) $h = $w;
235  $info = getimagesize($file); //get original size
236
237  // calculate crop size
238  $fr = $info[0]/$info[1];
239  $tr = $w/$h;
240  if($tr >= 1){
241    if($tr > $fr){
242        $cw = $info[0];
243        $ch = (int) $info[0]/$tr;
244    }else{
245        $cw = (int) $info[1]*$tr;
246        $ch = $info[1];
247    }
248  }else{
249    if($tr < $fr){
250        $cw = (int) $info[1]*$tr;
251        $ch = $info[1];
252    }else{
253        $cw = $info[0];
254        $ch = (int) $info[0]/$tr;
255    }
256  }
257  // calculate crop offset
258  $cx = (int) ($info[0]-$cw)/2;
259  $cy = (int) ($info[1]-$ch)/3;
260
261  //cache
262  $local = getCacheName($file,'.media.'.$cw.'x'.$ch.'.crop.'.$ext);
263  $mtime = @filemtime($local); // 0 if not exists
264
265  if( $mtime > filemtime($file) ||
266      crop_imageIM($ext,$file,$info[0],$info[1],$local,$cw,$ch,$cx,$cy) ||
267      resize_imageGD($ext,$file,$cw,$ch,$local,$cw,$ch,$cx,$cy) ){
268    if($conf['fperm']) chmod($local, $conf['fperm']);
269    return get_resized($local,$ext, $w, $h);
270  }
271
272  //still here? cropping failed
273  return get_resized($file,$ext, $w, $h);
274}
275
276
277/**
278 * Returns the wanted cachetime in seconds
279 *
280 * Resolves named constants
281 *
282 * @author  Andreas Gohr <andi@splitbrain.org>
283 */
284function calc_cache($cache){
285  global $conf;
286
287  if(strtolower($cache) == 'nocache') return 0; //never cache
288  if(strtolower($cache) == 'recache') return $conf['cachetime']; //use standard cache
289  return -1; //cache endless
290}
291
292/**
293 * Download a remote file and return local filename
294 *
295 * returns false if download fails. Uses cached file if available and
296 * wanted
297 *
298 * @author  Andreas Gohr <andi@splitbrain.org>
299 * @author  Pavel Vitis <Pavel.Vitis@seznam.cz>
300 */
301function get_from_URL($url,$ext,$cache){
302  global $conf;
303
304  // if no cache or fetchsize just redirect
305  if ($cache==0)           return false;
306  if (!$conf['fetchsize']) return false;
307
308  $local = getCacheName(strtolower($url),".media.$ext");
309  $mtime = @filemtime($local); // 0 if not exists
310
311  //decide if download needed:
312  if( ($mtime == 0) ||                           // cache does not exist
313      ($cache != -1 && $mtime < time()-$cache)   // 'recache' and cache has expired
314    ){
315      if(image_download($url,$local)){
316        return $local;
317      }else{
318        return false;
319      }
320  }
321
322  //if cache exists use it else
323  if($mtime) return $local;
324
325  //else return false
326  return false;
327}
328
329/**
330 * Download image files
331 *
332 * @author Andreas Gohr <andi@splitbrain.org>
333 */
334function image_download($url,$file){
335  global $conf;
336  $http = new DokuHTTPClient();
337  $http->max_bodysize = $conf['fetchsize'];
338  $http->timeout = 25; //max. 25 sec
339  $http->header_regexp = '!\r\nContent-Type: image/(jpe?g|gif|png)!i';
340
341  $data = $http->get($url);
342  if(!$data) return false;
343
344  $fileexists = @file_exists($file);
345  $fp = @fopen($file,"w");
346  if(!$fp) return false;
347  fwrite($fp,$data);
348  fclose($fp);
349  if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
350
351  // check if it is really an image
352  $info = @getimagesize($file);
353  if(!$info){
354    @unlink($file);
355    return false;
356  }
357
358  return true;
359}
360
361/**
362 * resize images using external ImageMagick convert program
363 *
364 * @author Pavel Vitis <Pavel.Vitis@seznam.cz>
365 * @author Andreas Gohr <andi@splitbrain.org>
366 */
367function resize_imageIM($ext,$from,$from_w,$from_h,$to,$to_w,$to_h){
368  global $conf;
369
370  // check if convert is configured
371  if(!$conf['im_convert']) return false;
372
373  // prepare command
374  $cmd  = $conf['im_convert'];
375  $cmd .= ' -resize '.$to_w.'x'.$to_h.'!';
376  if ($ext == 'jpg' || $ext == 'jpeg') {
377      $cmd .= ' -quality '.$conf['jpg_quality'];
378  }
379  $cmd .= " $from $to";
380
381  @exec($cmd,$out,$retval);
382  if ($retval == 0) return true;
383  return false;
384}
385
386/**
387 * crop images using external ImageMagick convert program
388 *
389 * @author Andreas Gohr <andi@splitbrain.org>
390 */
391function crop_imageIM($ext,$from,$from_w,$from_h,$to,$to_w,$to_h,$ofs_x,$ofs_y){
392  global $conf;
393
394  // check if convert is configured
395  if(!$conf['im_convert']) return false;
396
397  // prepare command
398  $cmd  = $conf['im_convert'];
399  $cmd .= ' -crop '.$to_w.'x'.$to_h.'+'.$ofs_x.'+'.$ofs_y;
400  if ($ext == 'jpg' || $ext == 'jpeg') {
401      $cmd .= ' -quality '.$conf['jpg_quality'];
402  }
403  $cmd .= " $from $to";
404
405  @exec($cmd,$out,$retval);
406  if ($retval == 0) return true;
407  return false;
408}
409
410/**
411 * resize or crop images using PHP's libGD support
412 *
413 * @author Andreas Gohr <andi@splitbrain.org>
414 * @author Sebastian Wienecke <s_wienecke@web.de>
415 */
416function resize_imageGD($ext,$from,$from_w,$from_h,$to,$to_w,$to_h,$ofs_x=0,$ofs_y=0){
417  global $conf;
418
419  if($conf['gdlib'] < 1) return false; //no GDlib available or wanted
420
421  // check available memory
422  if(!is_mem_available(($from_w * $from_h * 4) + ($to_w * $to_h * 4))){
423    return false;
424  }
425
426  // create an image of the given filetype
427  if ($ext == 'jpg' || $ext == 'jpeg'){
428    if(!function_exists("imagecreatefromjpeg")) return false;
429    $image = @imagecreatefromjpeg($from);
430  }elseif($ext == 'png') {
431    if(!function_exists("imagecreatefrompng")) return false;
432    $image = @imagecreatefrompng($from);
433
434  }elseif($ext == 'gif') {
435    if(!function_exists("imagecreatefromgif")) return false;
436    $image = @imagecreatefromgif($from);
437  }
438  if(!$image) return false;
439
440  if(($conf['gdlib']>1) && function_exists("imagecreatetruecolor") && $ext != 'gif'){
441    $newimg = @imagecreatetruecolor ($to_w, $to_h);
442  }
443  if(!$newimg) $newimg = @imagecreate($to_w, $to_h);
444  if(!$newimg){
445    imagedestroy($image);
446    return false;
447  }
448
449  //keep png alpha channel if possible
450  if($ext == 'png' && $conf['gdlib']>1 && function_exists('imagesavealpha')){
451    imagealphablending($newimg, false);
452    imagesavealpha($newimg,true);
453  }
454
455  //keep gif transparent color if possible
456  if($ext == 'gif' && function_exists('imagefill') && function_exists('imagecolorallocate')) {
457    if(function_exists('imagecolorsforindex') && function_exists('imagecolortransparent')) {
458      $transcolorindex = @imagecolortransparent($image);
459      if($transcolorindex >= 0 ) { //transparent color exists
460        $transcolor = @imagecolorsforindex($image, $transcolorindex);
461        $transcolorindex = @imagecolorallocate($newimg, $transcolor['red'], $transcolor['green'], $transcolor['blue']);
462        @imagefill($newimg, 0, 0, $transcolorindex);
463        @imagecolortransparent($newimg, $transcolorindex);
464      }else{ //filling with white
465        $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
466        @imagefill($newimg, 0, 0, $whitecolorindex);
467      }
468    }else{ //filling with white
469      $whitecolorindex = @imagecolorallocate($newimg, 255, 255, 255);
470      @imagefill($newimg, 0, 0, $whitecolorindex);
471    }
472  }
473
474  //try resampling first
475  if(function_exists("imagecopyresampled")){
476    if(!@imagecopyresampled($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h)) {
477      imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
478    }
479  }else{
480    imagecopyresized($newimg, $image, 0, 0, $ofs_x, $ofs_y, $to_w, $to_h, $from_w, $from_h);
481  }
482
483  $okay = false;
484  if ($ext == 'jpg' || $ext == 'jpeg'){
485    if(!function_exists('imagejpeg')){
486      $okay = false;
487    }else{
488      $okay = imagejpeg($newimg, $to, $conf['jpg_quality']);
489    }
490  }elseif($ext == 'png') {
491    if(!function_exists('imagepng')){
492      $okay = false;
493    }else{
494      $okay =  imagepng($newimg, $to);
495    }
496  }elseif($ext == 'gif') {
497    if(!function_exists('imagegif')){
498      $okay = false;
499    }else{
500      $okay = imagegif($newimg, $to);
501    }
502  }
503
504  // destroy GD image ressources
505  if($image) imagedestroy($image);
506  if($newimg) imagedestroy($newimg);
507
508  return $okay;
509}
510
511/**
512 * Checks if the given amount of memory is available
513 *
514 * If the memory_get_usage() function is not available the
515 * function just assumes $bytes of already allocated memory
516 *
517 * @param  int $mem  Size of memory you want to allocate in bytes
518 * @param  int $used already allocated memory (see above)
519 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
520 * @author Andreas Gohr <andi@splitbrain.org>
521 */
522function is_mem_available($mem,$bytes=1048576){
523  $limit = trim(ini_get('memory_limit'));
524  if(empty($limit)) return true; // no limit set!
525
526  // parse limit to bytes
527  $limit = php_to_byte($limit);
528
529  // get used memory if possible
530  if(function_exists('memory_get_usage')){
531    $used = memory_get_usage();
532  }
533
534  if($used+$mem > $limit){
535    return false;
536  }
537
538  return true;
539}
540
541//Setup VIM: ex: et ts=2 enc=utf-8 :
542?>
543