1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8/**
9 * Handler class for Image
10 *
11 * Letter key: ~i~
12 *
13 */
14class Tracker_field_Image extends Tracker_Field_File
15{
16	private $imgMimeTypes;
17	private $imgMaxSize;
18
19	public static function getTypes()
20	{
21		return [
22			'i' => [
23				'name' => tr('Image'),
24				'description' => tr('Deprecated in favor of the Files field.'),
25				'help' => 'Image Tracker Field',
26				'prefs' => ['trackerfield_image'],
27				'tags' => ['deprecated'],
28				'warning' => tra('Deprecated in favor of the Files field.'),
29				'default' => 'n',
30				'params' => [
31					'xListSize' => [
32						'name' => tr('List image width'),
33						'description' => tr('Display size in pixels'),
34						'filter' => 'int',
35						'default' => 30,
36						'legacy_index' => 0,
37					],
38					'yListSize' => [
39						'name' => tr('List image height'),
40						'description' => tr('Display size in pixels'),
41						'filter' => 'int',
42						'default' => 30,
43						'legacy_index' => 1,
44					],
45					'xDetailSize' => [
46						'name' => tr('Detail image width'),
47						'description' => tr('Display size in pixels'),
48						'filter' => 'int',
49						'default' => 300,
50						'legacy_index' => 2,
51					],
52					'yDetailSize' => [
53						'name' => tr('Detail image height'),
54						'description' => tr('Display size in pixels'),
55						'filter' => 'int',
56						'default' => 300,
57						'legacy_index' => 3,
58					],
59					'uploadLimitScale' => [
60						'name' => tr('Maximum image size'),
61						'description' => tr('Maximum image width or height in pixels.'),
62						'filter' => 'int',
63						'default' => '1000',
64						'legacy_index' => 4,
65					],
66					'shadowbox' => [
67						'name' => tr('Shadowbox'),
68						'description' => tr('Shadowbox usage on this field'),
69						'filter' => 'alpha',
70						'options' => [
71							'' => tr('Do not use'),
72							'individual' => tr('One box per item'),
73							'group' => tr('Use the same box for all images'),
74						],
75						'legacy_index' => 5,
76					],
77					'imageMissingIcon' => [
78						'name' => tr('Missing Icon'),
79						'description' => tr('Icon to use when no images have been uploaded.'),
80						'filter' => 'url',
81						'legacy_index' => 6,
82					],
83				],
84			],
85		];
86	}
87
88	function __construct($fieldInfo, $itemData, $trackerDefinition)
89	{
90		parent::__construct($fieldInfo, $itemData, $trackerDefinition);
91		$this->imgMimeTypes = ['image/jpeg', 'image/gif', 'image/png', 'image/pjpeg', 'image/bmp'];
92		$this->imgMaxSize = (1048576 * 4); // 4Mo
93	}
94
95	function getFieldData(array $requestData = [])
96	{
97		global $prefs;
98		$smarty = TikiLib::lib('smarty');
99		$ins_id = $this->getInsertId();
100
101		if (! empty($prefs['fgal_match_regex']) && ! empty($_FILES[$ins_id]['name'])) {
102			if (! preg_match('/' . $prefs['fgal_match_regex'] . '/', $_FILES[$ins_id]['name'], $reqs)) {
103				$smarty->assign('msg', tra('Invalid imagename (using filters for filenames)'));
104				$smarty->display("error.tpl");
105				die;
106			}
107		}
108		if (! empty($prefs['fgal_nmatch_regex']) && ! empty($_FILES[$ins_id]['name'])) {
109			if (preg_match('/' . $prefs['fgal_nmatch_regex'] . '/', $_FILES[$ins_id]['name'], $reqs)) {
110				$smarty->assign('msg', tra('Invalid imagename (using filters for filenames)'));
111				$smarty->display("error.tpl");
112				die;
113			}
114		}
115
116		// "Blank" means remove image
117		if (! empty($requestData[$ins_id]) && $requestData[$ins_id] == 'blank') {
118			return [ 'value' => 'blank' ];
119		}
120
121		if (! empty($requestData)) {
122			return parent::getFieldData($requestData);
123		} else {
124			return [ 'value' => $this->getValue() ];
125		}
126	}
127
128	function renderInnerOutput($context = [])
129	{
130		global $prefs;
131		$smarty = TikiLib::lib('smarty');
132
133		$val = $this->getConfiguration('value');
134		$list_mode = ! empty($context['list_mode']) ? $context['list_mode'] : 'n';
135		if ($list_mode == 'csv') {
136			return $val; // return the filename
137		}
138		$pre = '';
139		if (! empty($val) && file_exists($val)) {
140			$params['file'] = $val;
141			$shadowtype = $this->getOption('shadowbox');
142			if ($prefs['feature_shadowbox'] == 'y' && ! empty($shadowtype)) {
143				switch ($shadowtype) {
144					case 'item':
145						$rel = '[' . $this->getItemId() . ']';
146						break;
147					case 'individual':
148						$rel = '';
149						break;
150					default:
151						$rel = '[' . $this->getConfiguration('fieldId') . ']';
152						break;
153				}
154				$pre = "<a href=\"$val\" data-box=\"shadowbox$rel;type=img\">";
155			}
156			if ($this->getOption('xListSize') || $this->getOption('yListSize') || $this->getOption('xDetailSize') || $this->getOption('yDetailSize')) {
157				$image_size_info = getimagesize($val);
158			}
159			if ($list_mode != 'n') {
160				if ($this->getOption('xListSize') || $this->getOption('yListSize')) {
161					list( $params['width'], $params['height']) = $this->get_resize_dimensions(
162						$image_size_info[0],
163						$image_size_info[1],
164						$this->getOption('xListSize'),
165						$this->getOption('yListSize')
166					);
167				}
168			} else {
169				if ($this->getOption('xDetailSize') || $this->getOption('yDetailSize')) {
170					list( $params['width'], $params['height']) = $this->get_resize_dimensions(
171						$image_size_info[0],
172						$image_size_info[1],
173						$this->getOption('xDetailSize'),
174						$this->getOption('yDetailSize')
175					);
176				}
177			}
178		} else {
179			if ($this->getOption('imageMissingIcon')) {
180				$params['file'] = $this->getOption('imageMissingIcon');
181				$params['alt'] = 'n/a';
182			} else {
183				return '';
184			}
185		}
186		$smarty->loadPlugin('smarty_function_html_image');
187		$ret = smarty_function_html_image($params, $smarty->getEmptyInternalTemplate());
188		if (! empty($pre)) {
189			$ret = $pre . $ret . '</a>';
190		}
191		return $ret;
192	}
193
194	function renderInput($context = [])
195	{
196		return $this->renderTemplate(
197			'trackerinput/image.tpl',
198			$context,
199			[
200				'image_tag' => $this->renderInnerOutput($context),
201			]
202		);
203	}
204
205	function handleSave($value, $oldValue)
206	{
207		if (! empty($value)) {
208			$old_file = $oldValue;
209
210			if ($value == 'blank') {
211				if (file_exists($old_file)) {
212					unlink($old_file);
213				}
214
215				return [
216					'value' => '',
217				];
218			}
219
220			$type = $this->getConfiguration('file_type');
221
222			if ($this->isImageType($type)) {
223				if ($maxSize = $this->getOption('uploadLimitScale')) {
224					$imagegallib = TikiLib::lib('imagegal');	// TODO: refactor to use Image class directly and remove dependency on imagegals
225					$imagegallib->image = $value;
226					$imagegallib->readimagefromstring();
227					$imagegallib->getimageinfo();
228					if ($imagegallib->xsize > $maxSize || $imagegallib->ysize > $maxSize) {
229						$imagegallib->rescaleImage($maxSize, $maxSize);
230						$value = $imagegallib->image;
231					}
232				}
233				$filesize = $this->getConfiguration('file_size');
234				if ($filesize <= $this->imgMaxSize) {
235					$itemId = $this->getItemId();
236					$file_name = $this->getImageFilename($this->getConfiguration('file_name'), $itemId, $this->getConfiguration('fieldId'));
237
238					file_put_contents($file_name, $value);
239					chmod($file_name, 0644); // seems necessary on some system (see move_uploaded_file doc on php.net
240
241					if (file_exists($old_file) && $old_file != $file_name) {
242						unlink($old_file);
243					}
244
245					return [
246						'value' => $file_name,
247					];
248				}
249			}
250		}
251
252		return [
253			'value' => false,
254		];
255	}
256
257	/**
258	 * Calculate the size of a resized image
259	 *
260	 * TODO move to a lib (Images depends on Imagick or GD which this doesn't need)
261	 *
262	 * @param int $image_width (existing image width)
263	 * @param int $image_height	(existing image height)
264	 * @param int $max_width (max width to scale to)
265	 * @param int $max_height (optional max height)
266	 * @param bool $upscale (whether to make images larger - default = false)
267	 *
268	 * @return array(int $resized_width, int $resized_height)
269	 */
270	private function get_resize_dimensions($image_width, $image_height, $max_width = null, $max_height = null, $upscale = false)
271	{
272		if (! $upscale && $image_width <= $max_width && $image_height <= $max_height) {
273			return [$image_width, $image_height];
274		}
275		if (! $max_height || ($max_width && $image_width > $image_height && $image_height < $max_height)) {
276			$ratio = $max_width / $image_width;
277		} else {
278			$ratio = $max_height / $image_height;
279			if ($max_width && round($image_width * $ratio) > $max_width) {
280				$ratio = $max_width / $image_width;
281			}
282		}
283		return [round($image_width * $ratio), round($image_height * $ratio)];
284	}
285
286	function getImageFilename($name, $itemId, $fieldId)
287	{
288		$ext = pathinfo($name, PATHINFO_EXTENSION);
289		if (! in_array($ext, ['png', 'gif', 'jpg', 'jpeg'])) {
290			$ext = 'jpg';
291		}
292
293		do {
294			$name = md5(uniqid("$name.$itemId.$fieldId"));
295			$name .= '.' . $ext;
296		} while (file_exists("img/trackers/$name"));
297
298		return "img/trackers/$name";
299	}
300
301	function isImageType($mimeType)
302	{
303		return in_array($mimeType, $this->imgMimeTypes);
304	}
305
306	function getDocumentPart(Search_Type_Factory_Interface $typeFactory)
307	{
308		$value = $this->getValue();
309		$baseKey = $this->getBaseKey();
310
311		return [
312			$baseKey => $typeFactory->plaintext($value),
313		];
314	}
315}
316