Issues (4868)

api/thumbnail.php (1 issue)

1
<?php
2
/**
3
* eGroupWare - eTemplates
4
*
5
* @link http://www.egroupware.org
6
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
7
* @author Nathan Gray
8
* @author Andreas Stöckel
9
* @package etemplate
10
* @version $Id$
11
*/
12
13
use EGroupware\Api;
14
use EGroupware\Api\Vfs;
15
16
//Set all necessary info and fire up egroupware
17
$GLOBALS['egw_info']['flags'] = array(
18
	'currentapp'	=>	get_app(),
19
	'noheader'	=>	true,
20
	'nonavbar'	=>	true
21
);
22
include ('../header.inc.php');
23
24
// strip slashes from _GET parameters, if someone still has magic_quotes_gpc on
25
if (get_magic_quotes_gpc() && $_GET)
26
{
27
	$_GET = array_stripslashes($_GET);
28
}
29
30
// no need to keep the session open (it stops other parallel calls)
31
$GLOBALS['egw']->session->commit_session();
32
33
if (!read_thumbnail(get_srcfile()))
34
{
35
	header('404 Not found');
36
}
37
38
/**
39
 * Reads the source file from the path parameters
40
 */
41
function get_srcfile()
42
{
43
	if (isset($_GET['path']))
44
	{
45
		$g_srcfile = $_GET['path'];
46
	}
47
	else
48
	{
49
		$g_srcfile = Api\Link::vfs_path($_GET['app'], $_GET['id'], $_GET['file'], true);
50
	}
51
52
	return Vfs::PREFIX.$g_srcfile;
53
}
54
55
/**
56
 * Returns the currently active app.
57
 */
58
function get_app()
59
{
60
	if (isset($_GET['app']))
61
	{
62
		$app = $_GET['app'];
63
	}
64
	elseif (isset($_GET['path']))
65
	{
66
		@list(, $apps, $app) = explode('/', $_GET['path']);
67
		if ($apps !== 'apps')
68
		{
69
			$app = 'filemanager';
70
		}
71
	}
72
73
	if (!preg_match('/^[a-z0-9_-]+$/i',$app))
74
	{
75
		die('Stop');	// just to prevent someone doing nasty things
76
	}
77
78
	return $app;
79
}
80
81
/**
82
 * Returns the maximum width/height of a thumbnail
83
 */
84
function get_maxsize()
85
{
86
	$preset = !($GLOBALS['egw_info']['server']['link_list_thumbnail'] > 0) ? 64 :
87
		$GLOBALS['egw_info']['server']['link_list_thumbnail'];
88
89
	// Another maximum size may be passed if thumbnails are turned on
90
	if ($preset != 0 && isset($_GET['thsize']) && is_numeric($_GET['thsize']))
91
	{
92
		$preset = (int)$_GET['thsize'];
93
	}
94
95
	return $preset;
96
}
97
98
/**
99
 * Either loads the thumbnail for the given file form cache or generates a new
100
 * one
101
 *
102
 * @param string $src is the file of which a thumbnail should be created
103
 * @returns false if the file doesn't exist or any other error occured.
104
 */
105
function read_thumbnail($src)
106
{
107
	//Check whether the source file is readable and exists
108
	if (!file_exists($src) || !Vfs::is_readable($src))
109
	{
110
		return false;
111
	}
112
113
	// Get the maxsize of an thumbnail. If thumbnailing is turned off, the value
114
	// will be 0
115
	$maxsize = get_maxsize();
116
	if (isset($_GET['thheight']) && (int)$_GET['thheight'] > 0)
117
	{
118
		$height = (int)$_GET['thheight'];
119
		$maxh = $minh = $height;
120
	}
121
	elseif (isset($_GET['thwidth']) && (int)$_GET['thwidth'] > 0)
122
	{
123
		$width = (int)$_GET['thwidth'];
124
		$maxw = $minw = $width;
125
	}
126
	elseif (isset($_GET['thminsize']) && (int)$_GET['thminsize'] > 0)
127
	{
128
		$minsize = (int)$_GET['thminsize'];
129
		$minw = $minh = $minsize;
130
	}
131
	else
132
	{
133
		$maxw = $maxh = $maxsize;
134
	}
135
	//error_log(__METHOD__."() maxsize=$maxsize, width=$width, height=$height, minsize=$minsize --> maxw=$maxw, maxh=$maxh, minw=$minw, minh=$minh");
136
137
	// Generate the destination filename and check whether the destination directory
138
	// had been successfully created (the cache class used in gen_dstfile does that).
139
	$stat = Vfs::stat(Vfs::parse_url($src, PHP_URL_PATH));
140
141
	// if pdf-thumbnail-creation is not available, generate a single scaled-down pdf-icon
142
	if ($stat && Vfs::mime_content_type($src) == 'application/pdf' && !pdf_thumbnails_available())
143
	{
144
		list($app, $icon) = explode('/', Vfs::mime_icon('application/pdf'), 2);
145
		list(, $path) = explode($GLOBALS['egw_info']['server']['webserver_url'],
146
			Api\Image::find($app, $icon), 2);
147
		$src = EGW_SERVER_ROOT.$path;
148
		$stat = false;
149
		$maxsize = $height = $width = $minsize = $maxh = $minh = $maxw = $minw = 16;
150
	}
151
	$dst = gen_dstfile($stat && !empty($stat['url']) ? $stat['url'] : $src, $maxsize, $height, $width, $minsize);
152
	$dst_dir = dirname($dst);
153
	if(file_exists($dst_dir))
154
	{
155
		// Check whether the destination file already exists and is newer than
156
		// the source file. Assume the file doesn't exist if thumbnailing is turned off.
157
		$exists = file_exists($dst) && filemtime($dst) >= filemtime($src);
158
		// Only generate the thumbnail if the destination file does not match the
159
		// conditions mentioned above. Abort if $maxsize is 0.
160
		$gen_thumb = !$exists;
161
		if ($gen_thumb && ($thumb = gd_image_thumbnail($src, $maxw, $maxh, $minw, $minh)))
162
		{
163
			// Save the file to disk...
164
			imagepng($thumb, $dst);
165
166
			// Previous versions generated a new copy of the png to output it -
167
			// as encoding pngs is quite cpu-intensive I think it might
168
			// be better to just read it from the temp dir again - as it is probably
169
			// still in the fs-cache
170
			$exists = true;
171
172
			imagedestroy($thumb);
173
		}
174
175
		$output_mime = 'image/png';
176
177
		// If some error occured during thumbnail generation or thumbnailing is turned off,
178
		// simply output the mime type icon
179
		if (!$exists)
180
		{
181
			$mime = Vfs::mime_content_type($src);
182
			list($app, $icon) = explode('/', Vfs::mime_icon($mime), 2);
183
			list(, $path) = explode($GLOBALS['egw_info']['server']['webserver_url'],
184
				Api\Image::find($app, $icon), 2);
185
			$dst = EGW_SERVER_ROOT.$path;
186
			if (function_exists('mime_content_type'))
187
			{
188
				$output_mime = mime_content_type($dst);
189
			}
190
			else
191
			{
192
				$output_mime = Api\MimeMagic::filename2mime($dst);
193
			}
194
		}
195
196
		if ($dst)
197
		{
198
			// Allow client to cache these, makes scrolling in filemanager much nicer
199
			// setting maximum allow caching time of one year, if url contains (non-empty) moditication time
200
			Api\Session::cache_control(empty($_GET['mtime']) ? 300 : 31536000, true);	// true = private / browser only caching
201
			header('Content-Type: '.$output_mime);
202
			readfile($dst);
203
			return true;
204
		}
205
	}
206
207
	return false;
208
}
209
210
/**
211
 * Get filename for thumbnail used for caching depending on it's size
212
 *
213
 * Priority is (highest to lowest) if given is: $height, $width, $minsize, $maxsize
214
 *
215
 * @param string $src
216
 * @param int $maxsize
217
 * @param int $height =null
218
 * @param int $width =null
219
 * @param int $minsize =null
220
 * @return string
221
 */
222
function gen_dstfile($src, $maxsize, $height=null, $width=null, $minsize=null)
223
{
224
	// Use the egroupware file cache to store the thumbnails on a per instance basis
225
	$cachefile = new Api\Cache\Files(array());
226
	$size = ($height > 0 ? 'h'.$height : ($width > 0 ? 'w'.$height : ($minsize > 0 ? 'm'.$minsize : $maxsize)));
227
	return $cachefile->filename(Api\Cache::keys(Api\Cache::INSTANCE, 'etemplate',
228
		'thumb_'.md5($src.$size).'.png'), true);
229
}
230
231
/**
232
 * Function which calculates the sizes of an image with the width w and the height
233
 * h, which should be scaled to an thumbnail of the maximum dimensions maxw and
234
 * maxh
235
 *
236
 * @param int $w original width of the image
237
 * @param int $h original height of the image
238
 * @param int $maxw maximum width of the image
239
 * @param int $maxh maximum height of the image
240
 * @param int $minw the minimum width of the thumbnail
241
 * @param int $minh the minimum height of the thumbnail
242
 * @returns an array with two elements, w, h or "false" if the original dimensions
243
 *   of the image are that "odd", that one of the output sizes is smaller than one pixel.
244
 *
245
 * TODO: As this is a general purpose function, it might probably be moved
246
 *   to some other php file or an "image utils" class.
247
 */
248
function get_scaled_image_size($w, $h, $maxw, $maxh, $minw=0, $minh=0)
249
{
250
	//Scale will contain the factor by which the image has to be scaled down
251
	$scale = 1.0;
252
253
	//Select the constraining dimension
254
	if ($w > $h) // landscape image: constraining factor $minh or $maxw
255
	{
256
		$scale = $minh ? $minh / $h : $maxw / $w;
257
	}
258
	else // portrail image: constraining factor $minw or $maxh
259
	{
260
		$scale = $minw ? $minw / $w : $maxh / $h;
261
	}
262
263
	// Don't scale images up
264
	if ($scale > 1.0)
265
	{
266
		$scale = 1.0;
267
	}
268
269
	$wout = round($w * $scale);
270
	$hout = round($h * $scale);
271
	//error_log(__METHOD__."(w=$w, h=$h, maxw=$maxw, maxh=$maxh, minw=$minw, minh=$minh) --> wout=$wout, hout=$hout");
272
273
	//Return the calculated values
274
	if ($wout < 1 || $hout < 1)
275
	{
276
		return false;
277
	}
278
	else
279
	{
280
		return array($wout, $hout);
281
	}
282
}
283
284
/**
285
 * Read thumbnail from image, without loading it completly using optional exif extension
286
 *
287
 * @param string $file
288
 * @return boolean|resource false or a gd_image
289
 */
290
function exif_thumbnail_load($file)
291
{
292
	if (!function_exists('exif_thumbnail') ||
293
		!($image = exif_thumbnail($file)))
294
	{
295
		return false;
296
	}
297
	return imagecreatefromstring($image);
298
}
299
300
/**
301
 * Loads the given imagefile - returns "false" if the file wasn't an image,
302
 * otherwise the gd-image is returned.
303
 *
304
 * @param string $file the file which to load
305
 * @param int $maxw the maximum width of the thumbnail
306
 * @param int $maxh the maximum height of the thumbnail
307
 * @returns boolean|resource false or a gd_image
308
 */
309
function gd_image_load($file,$maxw,$maxh)
310
{
311
	// Get mime type
312
	list($type, $image_type) = explode('/', $mime = Vfs::mime_content_type($file));
313
	// if $file is not from vfs, use Api\MimeMagic::filename2mime to get mime-type from extension
314
	if (!$type) list($type, $image_type) = explode('/', $mime = Api\MimeMagic::filename2mime($file));
315
316
	// Call the according gd constructor depending on the file type
317
	if($type == 'image')
318
	{
319
		if (in_array($image_type, array('tiff','jpeg')) && ($image = exif_thumbnail_load($file)))
320
		{
321
			return $image;
322
		}
323
		switch ($image_type)
324
		{
325
			case 'png':
326
				return imagecreatefrompng($file);
327
			case 'jpeg':
328
				return imagecreatefromjpeg($file);
329
			case 'gif':
330
				return imagecreatefromgif($file);
331
			case 'bmp':
332
				return imagecreatefromwbmp($file);
333
			case 'svg+xml':
334
				Api\Header\Content::type(Vfs::basename($file), $mime);
335
				readfile($file);
336
				exit;
337
		}
338
	}
339
	else if ($type == 'application')
340
	{
341
		$thumb = false;
342
		if(strpos($image_type,'vnd.oasis.opendocument.') === 0)
343
		{
344
			// OpenDocuments have thumbnails inside already
345
			$thumb = get_opendocument_thumbnail($file);
346
		}
347
		else if($image_type == 'pdf')
348
		{
349
			$thumb = get_pdf_thumbnail($file);
350
		}
351
		else if (strpos($image_type, 'vnd.openxmlformats-officedocument.') === 0)
352
		{
353
			// TODO: Figure out how this can be done reliably
354
			//$thumb = get_msoffice_thumbnail($file);
355
		}
356
		// Mark it with mime type icon
357
		if($thumb)
358
		{
359
			// Need to scale first, or the mark will be wrong size
360
			$scaled = get_scaled_image_size(imagesx($thumb), imagesy($thumb), $maxw, $maxh);
361
			if ($scaled !== false)
362
			{
363
				list($sw, $sh) = $scaled;
364
365
				//Now scale it down
366
				$img_dst = gd_create_transparent_image($sw, $sh);
367
				imagecopyresampled($img_dst, $thumb, 0, 0, 0, 0, $sw, $sh, imagesx($thumb), imagesy($thumb));
368
				$thumb = $img_dst;
369
			}
370
			$mime = Vfs::mime_content_type($file);
371
			$tag_image = null;
372
			corner_tag($thumb, $tag_image, $mime);
373
			imagedestroy($tag_image);
374
		}
375
		return $thumb;
376
	}
377
	return false;
378
}
379
380
/**
381
 * Extract the thumbnail from an opendocument file and apply a colored mask
382
 * so you can tell what type it is, and so it looks a little better in larger
383
 * thumbnails (eg: in tiled view)
384
 *
385
 * Inspired by thumbnails for nautilus:
386
 * http://bernaerts.dyndns.org/linux/76-gnome/285-gnome-shell-generate-libreoffice-thumbnail-nautilus
387
 *
388
 * @param string $file
389
 * @return resource GD image
390
 */
391
function get_opendocument_thumbnail($file)
392
{
393
	$mimetype = explode('/', Vfs::mime_content_type($file));
394
395
	// Image is already there, but we can't access them directly through VFS
396
	$ext = $mimetype == 'application/vnd.oasis.opendocument.text' ? '.odt' : '.ods';
397
	$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($file,$ext).'-');
398
	copy($file,$archive);
399
400
	$thumbnail_url = 'zip://'.$archive.'#Thumbnails/thumbnail.png';
401
	$image = imagecreatefromstring(file_get_contents($thumbnail_url));
402
	unlink($archive);
403
/*
404
	// Mask it with a color by type
405
	$mask = imagecreatefrompng('templates/default/images/opendocument.png');
406
	if($image)
407
	{
408
		$filter_color = array(0,0,0);
409
		switch($mimetype[1])
410
		{
411
			// Type colors from LibreOffice (https://wiki.documentfoundation.org/Design/Whiteboards/LibreOffice_Initial_Icons)
412
			case 'vnd.oasis.opendocument.text':
413
				$filter_color = array(2,63,98); break;
414
			case 'vnd.oasis.opendocument.spreadsheet':
415
				$filter_color = array(16,104,2); break;
416
			case 'vnd.oasis.opendocument.presentation':
417
				$filter_color = array(98,37,2); break;
418
			case 'vnd.oasis.opendocument.graphics':
419
				$filter_color = array(135,105,0); break;
420
			case 'vnd.oasis.opendocument.database':
421
				$filter_color = array(83,2,96); break;
422
		}
423
		imagefilter($mask, IMG_FILTER_COLORIZE, $filter_color[0],$filter_color[1],$filter_color[2] );
424
		imagecopyresampled($image, $mask,0,0,0,0,imagesx($image),imagesy($image),imagesx($mask),imagesy($mask));
425
	}
426
 *
427
 */
428
	return $image;
429
}
430
431
/**
432
 * Check if we have the necessary requirements to generate pdf thumbnails
433
 *
434
 * @return boolean
435
 */
436
function pdf_thumbnails_available()
437
{
438
	return class_exists('Imagick');
439
}
440
/**
441
 * Extract the thumbnail from a PDF file and apply a colored mask
442
 * so you can tell what type it is, and so it looks a little better in larger
443
 * thumbnails (eg: in tiled view).
444
 *
445
 * Requires ImageMagick & ghostscript.
446
 *
447
 * @param string $file
448
 * @return resource GD image
449
 */
450
function get_pdf_thumbnail($file)
451
{
452
	if(!pdf_thumbnails_available()) return false;
453
454
	// switch off max_excution_time, as some thumbnails take longer and
455
	// will be startet over and over again, if they dont finish
456
	@set_time_limit(0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

456
	/** @scrutinizer ignore-unhandled */ @set_time_limit(0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
457
458
	$im = new Imagick($file);
459
	$im->setimageformat('png');
460
	$im->setresolution(300, 300);
461
462
	$gd = imagecreatefromstring($im->getimageblob());
463
	return $gd;
464
}
465
466
/**
467
 * Put the tag image in the corner of the target image
468
 *
469
 * Used for thumbnails of documents, so you can still see the mime type
470
 *
471
 * @param resource& $target_image
472
 * @param resource&|null $tag_image
473
 * @param string|null $mime Use correct mime type icon instead of $tag_image
474
 */
475
function corner_tag(&$target_image, &$tag_image, $mime)
476
{
477
	$target_width = imagesx($target_image);
478
	$target_height = imagesy($target_image);
479
480
	// Find mime image, if no tag image set
481
	if(!$tag_image && $mime)
482
	{
483
		list($app, $icon) = explode('/', Vfs::mime_icon($mime), 2);
484
		list(, $path) = explode($GLOBALS['egw_info']['server']['webserver_url'],
485
			Api\Image::find($app, $icon), 2);
486
		$dst = EGW_SERVER_ROOT.$path;
487
		$tag_image = imagecreatefrompng($dst);
488
	}
489
490
	// Find correct size - max 1/3 target
491
	$tag_size = get_scaled_image_size(imagesx($tag_image), imagesy($tag_image), $target_width / 3, $target_height / 3);
492
	if(!$tag_size) return;
493
	list($tag_width,$tag_height) = $tag_size;
494
495
	// Put it in
496
	if($mime)
497
	{
498
		imagecopyresampled($target_image,$tag_image,
499
			$target_width - $tag_width,
500
			$target_height - $tag_height,
501
			0,0,
502
			$tag_width,
503
			$tag_height,
504
			imagesx($tag_image),
505
			imagesy($tag_image)
506
		);
507
	}
508
}
509
510
/**
511
 * Create an gd_image with transparent background.
512
 *
513
 * @param int $w the width of the resulting image
514
 * @param int $h the height of the resutling image
515
 */
516
function gd_create_transparent_image($w, $h)
517
{
518
	if (!($gdVersion = gdVersion()))
519
	{
520
		//Looking up the gd version failed, return false
521
		return false;
522
	}
523
	elseif ($gdVersion >= 2)
524
	{
525
		//Create an 32-bit image and fill it with transparency.
526
		$img_dst = imagecreatetruecolor($w, $h);
527
		imageSaveAlpha($img_dst, true);
528
		$trans_color = imagecolorallocatealpha($img_dst, 0, 0, 0, 127);
529
		imagefill($img_dst, 0, 0, $trans_color);
530
531
		return $img_dst;
532
	}
533
	else
534
	{
535
		//Just crate a simple image
536
		return imagecreate($w, $h);
537
	}
538
}
539
540
/**
541
 * Creates a scaled version of the given image - returns the gd-image or false if the
542
 * process failed.
543
 *
544
 * @param string $file the filename of the file
545
 * @param int $maxw the maximum width of the thumbnail
546
 * @param int $maxh the maximum height of the thumbnail
547
 * @param int $minw the minimum width of the thumbnail
548
 * @param int $minh the minimum height of the thumbnail
549
 * @return the gd_image or false if one of the steps taken to produce the thumbnail
550
 *   failed.
551
 */
552
function gd_image_thumbnail($file, $maxw, $maxh, $minw, $minh)
553
{
554
	//Load the image
555
	if (($img_src = gd_image_load($file,$maxw,$maxh)) !== false)
556
	{
557
		//Get the constraints of the image
558
		$w = imagesx($img_src);
559
		$h = imagesy($img_src);
560
561
		//Calculate the actual size of the thumbnail
562
		$scaled = get_scaled_image_size($w, $h, $maxw, $maxh, $minw, $minh);
563
		if ($scaled !== false)
564
		{
565
			list($sw, $sh) = $scaled;
566
567
			//Now scale it down
568
			$img_dst = gd_create_transparent_image($sw, $sh);
569
			imagecopyresampled($img_dst, $img_src, 0, 0, 0, 0, $sw, $sh, $w, $h);
570
			return $img_dst;
571
		}
572
	}
573
574
	return false;
575
}
576
577
/**
578
* Get which version of GD is installed, if any.
579
*
580
* Returns the version (1 or 2) of the GD extension.
581
* Off the php manual page, thanks Hagan Fox
582
*/
583
function gdVersion($user_ver = 0)
584
{
585
	if (! extension_loaded('gd')) { return; }
586
	static $gd_ver = 0;
587
588
	// Just accept the specified setting if it's 1.
589
	if ($user_ver == 1) { $gd_ver = 1; return 1; }
590
591
	// Use the static variable if function was called previously.
592
	if ($user_ver !=2 && $gd_ver > 0 ) { return $gd_ver; }
593
594
	// Use the gd_info() function if possible.
595
	if (function_exists('gd_info')) {
596
		$ver_info = gd_info();
597
		$match = null;
598
		preg_match('/\d/', $ver_info['GD Version'], $match);
599
		$gd_ver = $match[0];
600
		return $match[0];
601
	}
602
603
	// If phpinfo() is disabled use a specified / fail-safe choice...
604
	if (preg_match('/phpinfo/', ini_get('disable_functions'))) {
605
		if ($user_ver == 2) {
606
			$gd_ver = 2;
607
			return 2;
608
		} else {
609
			$gd_ver = 1;
610
			return 1;
611
		}
612
	}
613
	// ...otherwise use phpinfo().
614
	ob_start();
615
	phpinfo(8);
616
	$info = stristr(ob_get_clean(), 'gd version');
617
	if (preg_match('/\d/', $info, $match)) $gd_ver = $match[0];
618
	return $match[0];
619
}
620