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
8class Tracker_Field_Wiki extends Tracker_Field_Text implements Tracker_Field_Exportable
9{
10	public static function getTypes()
11	{
12		global $prefs;
13		if (isset($prefs['tracker_wikirelation_synctitle'])) {
14			$tracker_wikirelation_synctitle = $prefs['tracker_wikirelation_synctitle'];
15		} else {
16			$tracker_wikirelation_synctitle = 'n';
17		}
18		return [
19			'wiki' => [
20				'name' => tr('Wiki Page'),
21				'description' => tr('Embed an associated wiki page'),
22				'help' => 'Wiki page Tracker Field',
23				'prefs' => ['trackerfield_wiki'],
24				'tags' => ['basic'],
25				'default' => 'y',
26				'params' => [
27					'fieldIdForPagename' => [
28						'name' => tr('Field that is used for Wiki Page Name'),
29						'description' => tr('Field to get page name to create page name with.'),
30						'filter' => 'int',
31						'profile_reference' => 'tracker_field',
32						'parent' => 'input[name=trackerId]',
33						'parentkey' => 'tracker_id',
34					],
35					'namespace' => [
36						'name' => tr('Namespace for Wiki Page'),
37						'description' => tr('The namespace to use for the wiki page to prevent page name clashes. See namespace feature for more information.'),
38						'filter' => 'alpha',
39						'options' => [
40							'default' => tr('Default (trackerfield<fieldId>)'),
41							'none' => tr('No namespace'),
42							'custom' => tr('Custom namespace'),
43						],
44						'default' => (isset($prefs['namespace_enabled']) && $prefs['namespace_enabled'] === 'y' ? 'default' : 'none'),
45					],
46					'customnamespace' => [
47						'name' => tr('Custom Namespace'),
48						'description' => tr('The custom namespace to use if the custom option is selected.'),
49						'filter' => 'alpha',
50					],
51					'syncwikipagename' => [
52						'name' => tr('Rename Wiki Page when changed in tracker'),
53						'description' => tr('Rename associated wiki page when the field that is used for Wiki Page Name is changed.'),
54						'default' => $tracker_wikirelation_synctitle,
55						'filter' => 'alpha',
56												'options' => [
57														'n' => tr('No'),
58														'y' => tr('Yes'),
59												],
60										],
61					'syncwikipagedelete' => [
62												'name' => tr('Delete Wiki Page when tracker item is deleted'),
63												'description' => tr('Delete associated wiki page when the tracker item is deleted.'),
64												'default' => 'n',
65												'filter' => 'alpha',
66												'options' => [
67														'n' => tr('No'),
68														'y' => tr('Yes'),
69												],
70										],
71					'toolbars' => [
72						'name' => tr('Toolbars'),
73						'description' => tr('Enable the toolbars as syntax helpers.'),
74						'filter' => 'int',
75						'options' => [
76							0 => tr('Disable'),
77							1 => tr('Enable'),
78						],
79						'default' => 1,
80					],
81					'width' => [
82						'name' => tr('Width'),
83						'description' => tr('Size of the text area, in characters.'),
84						'filter' => 'int',
85					],
86					'height' => [
87						'name' => tr('Height'),
88						'description' => tr('Size of the text area, in lines.'),
89						'filter' => 'int',
90					],
91					'max' => [
92						'name' => tr('Character Limit'),
93						'description' => tr('Maximum number of characters to be stored.'),
94						'filter' => 'int',
95					],
96					'wordmax' => [
97						'name' => tr('Word Count'),
98						'description' => tr('Limit the length of the text, in number of words.'),
99						'filter' => 'int',
100					],
101					'wysiwyg' => [
102						'name' => tr('Use WYSIWYG'),
103						'description' => tr('Use a rich text editor instead of inputting plain text.'),
104						'default' => 'n',
105						'filter' => 'alpha',
106						'options' => [
107							'n' => tr('No'),
108							'y' => tr('Yes'),
109						],
110					],
111					'actions' => [
112						'name' => tr('Action Buttons'),
113						'description' => tr('Display wiki page buttons when editing the item.'),
114						'default' => 'n',
115						'filter' => 'alpha',
116						'options' => [
117							'n' => tr('No'),
118							'y' => tr('Yes'),
119						],
120					],
121					'samerow' => [
122						'name' => tr('Same Row'),
123						'description' => tr('Display the field name and input on the same row.'),
124						'deprecated' => false,
125						'filter' => 'int',
126						'default' => 1,
127						'options' => [
128							0 => tr('No'),
129							1 => tr('Yes'),
130						],
131					],
132					'removeBadChars' => [
133						'name' => tr('Remove Bad Chars'),
134						'description' => tr('Remove bad characters from the Wiki Page name.'),
135						'default' => 'n',
136						'filter' => 'alpha',
137						'options' => [
138							'n' => tr('No'),
139							'y' => tr('Yes'),
140						],
141					],
142				],
143			],
144		];
145	}
146
147	/**
148	 * @param $ins_fields_data
149	 * @param int $itemId           set to itemId when importing
150	 * @return bool
151	 */
152	function isValid($ins_fields_data, $itemId = 0)
153	{
154		global $prefs;
155
156		$pagenameField = $this->getOption('fieldIdForPagename');
157		$pagename = $this->cleanPageName($ins_fields_data[$pagenameField]['value']);
158		if (! $itemId) {
159			$itemId = $this->getItemId();
160		}
161
162		if ($this->getOption('namespace') !== 'none' && $prefs['namespace_enabled'] !== 'y') {
163			Feedback::error(tr('Warning: You need to enable the Namespace feature to use the namespace field.'));
164			return false;
165		}
166
167
168		if (TikiLib::lib('trk')->check_field_value_exists($pagename, $pagenameField, $itemId)) {
169			Feedback::error(tr('The page name provided already exists. Please choose another.'));
170			return false;
171		}
172
173		if ($prefs['wiki_badchar_prevent'] == 'y' && TikiLib::lib('wiki')->contains_badchars($pagename)) {
174			$bad_chars = TikiLib::lib('wiki')->get_badchars();
175			Feedback::error(tr(
176				'The page name specified "%0" contains unallowed characters. It will not be possible to save the page until those are removed: %1',
177				$pagename,
178				$bad_chars
179			));
180			return false;
181		}
182
183		return true;
184	}
185
186	function getFieldData(array $requestData = [])
187	{
188		$ins_id = $this->getInsertId();
189
190		global $user, $prefs;
191
192		$to_create_page = false;
193		$page_data = '';
194		$fieldId = $this->getConfiguration('fieldId');
195
196		if ($this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') {
197			$is_html = true;
198		} else {
199			$is_html = false;
200		}
201
202		$page_name = $this->getValue();
203		$insForPagenameField = 'ins_' . $this->getOption('fieldIdForPagename');
204
205		if (! $page_name) {
206			if (! empty($requestData[$insForPagenameField])) {
207				$page_name = $requestData[$insForPagenameField];	// from tabular import replace
208				$itemId = isset($requestData['itemId']) ? $requestData['itemId'] : 0;
209			} else if (! empty($requestData['itemId'])) {
210				$itemData = $this->getItemData();					// calculated field types like auto-increment need rendering
211				$definition = $this->getTrackerDefinition();
212				$factory = $definition->getFieldFactory();
213				$field_info = $definition->getField($this->getOption('fieldIdForPagename'));
214				if ($field_info) {
215					$handler = $factory->getHandler($field_info, $itemData);
216					$page_name = $handler->renderOutput(['list_mode' => 'csv']);
217				} else {
218					Feedback::error(tr('Missing Page Name field #%0 for Wiki field #%1', $this->getOption('fieldIdForPagename'), $fieldId));
219				}
220				$itemId = $requestData['itemId'];
221			}
222			$page_name = $this->getFullPageName($page_name);	// from tabular import replace
223		} else {
224			$itemId = $this->getItemId();
225		}
226
227		if ($page_name) {
228			// There is already a wiki pagename set (the value of the field is the wiki page name)
229			if (TikiLib::lib('tiki')->page_exists($page_name)) {
230				// Get wiki page content
231				$page_info = TikiLib::lib('tiki')->get_page_info($page_name);
232				$page_data = $page_info['data'];
233				if (! empty($requestData[$ins_id])) {
234					// There is new page data provided
235					if ($page_data != $requestData[$ins_id]) {
236						// Update page data
237						$edit_comment = 'Updated by Tracker Field ' . $fieldId;
238						$short_name = $requestData[$insForPagenameField];
239						$ins_fields_data[$this->getOption('fieldIdForPagename')]['value'] = $short_name;
240						if ($this->isValid($ins_fields_data, $itemId) === true) {
241							TikiLib::lib('tiki')->update_page($page_name, $requestData[$ins_id], $edit_comment, $user, TikiLib::lib('tiki')->get_ip_address(), '', 0, '', $is_html, null, null, $this->getOption('wysiwyg'));
242						}
243					}
244				}
245			} else {
246				$to_create_page = true;
247			}
248		} elseif (! empty($requestData[$ins_id])) {
249			// the field value is currently null and there is input, so would need to create page.
250			if ($short_name = $requestData[$insForPagenameField]) {
251				$page_name = $this->getFullPageName($short_name);
252				if ($page_name && ! TikiLib::lib('tiki')->page_exists($page_name)) {
253					$ins_fields_data[$this->getOption('fieldIdForPagename')]['value'] = $short_name;
254					if ($this->isValid($ins_fields_data) === true) {
255						$to_create_page = true;
256					}
257				} else {
258					Feedback::error(tr('Page "%0" already exists. Not overwriting.', $page_name));
259				}
260			}
261		}
262
263		if ($to_create_page) {
264			// Note we do not want to create blank pages, but if in the event a page that is already linked is deleted, a blank page will be created.
265			if (! empty($requestData[$ins_id])) {
266				$page_data = $requestData[$ins_id];
267			}
268			// re-clean the page name here incase it comes from legacy data, i.e. from a partial import
269			$page_name = $this->cleanPageName($page_name);
270			$edit_comment = 'Created by Tracker Field ' . $fieldId;
271			TikiLib::lib('tiki')->create_page($page_name, 0, $page_data, TikiLib::lib('tiki')->now, $edit_comment, $user, TikiLib::lib('tiki')->get_ip_address(), '', '', $is_html, null, $this->getOption('wysiwyg'));
272		}
273
274		if (empty($page_name) && $_SERVER['REQUEST_METHOD'] === 'POST' && empty($requestData[$insForPagenameField])) {
275			// saving a new item may have the wiki page name missing if it is an autoincrement field, so show a warning - TODO better somehow?
276			Feedback::error(tr('Missing Page Name field #%0 value for Wiki field #%1 (so page not created)', $this->getOption('fieldIdForPagename'), $fieldId));
277		}
278
279		$data = [
280			'value' => $page_name,
281			'page_data' => $page_data,
282		];
283
284		return $data;
285	}
286
287	function renderInput($context = [])
288	{
289		global $prefs;
290
291		static $firstTime = true;
292
293		$cols = $this->getOption('width');
294		$rows = $this->getOption('height');
295
296		if ($this->getOption('toolbars') === 0) {
297			$toolbars = false;
298		} else {
299			$toolbars = true;
300		}
301
302		$data = [
303			'toolbar' => $toolbars ? 'y' : 'n',
304			'cols' => ($cols >= 1) ? $cols : 80,
305			'rows' => ($rows >= 1) ? $rows : 6,
306			'keyup' => '',
307		];
308
309		if ($this->getOption('wordmax')) {
310			$data['keyup'] = "wordCount({$this->getOption('wordmax')}, this, 'cpt_{$this->getConfiguration('fieldId')}', '" . addcslashes(tr('Word Limit Exceeded'), "'") . "')";
311		} elseif ($this->getOption('max')) {
312			$data['keyup'] = "charCount({$this->getOption('max')}, this, 'cpt_{$this->getConfiguration('fieldId')}', '" . addcslashes(tr('Character Limit Exceeded'), "'") . "')";
313		}
314		$data['element_id'] = 'area_' . uniqid();
315		if ($firstTime && $this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') {	// html wysiwyg
316			$is_html = '<input type="hidden" id="allowhtml" value="1" />';
317			$firstTime = false;
318		} else {
319			$is_html = '';
320		}
321		$perms = Perms::get(['type' => 'wiki page', 'object' => $this->getValue('')]);
322		$data['perms'] = [
323			'view' => $perms->view,
324			'edit' => $perms->edit,
325			'wiki_view_source' => $perms->wiki_view_source,
326			'wiki_view_history' => $perms->wiki_view_history,
327		];
328		return $this->renderTemplate('trackerinput/wiki.tpl', $context, $data) . $is_html;
329	}
330
331	function renderOutput($context = [])
332	{
333		return $this->attemptParse($this->getConfiguration('page_data'));
334	}
335
336	function getDocumentPart(Search_Type_Factory_Interface $typeFactory)
337	{
338		$data = [];
339		$value = $this->getValue();
340		$baseKey = $this->getBaseKey();
341
342		if (! empty($value)) {
343			$info = TikiLib::lib('tiki')->get_page_info($value, true, true);
344			if ($info) {
345				$freshness_days = floor((time() - ($info['lastModif'])) / 86400);
346				$data = [
347					$baseKey => $typeFactory->identifier($value),
348					"{$baseKey}_text" => $typeFactory->wikitext($info['data']),
349					"{$baseKey}_raw" => $typeFactory->identifier($info['data']),
350					"{$baseKey}_creation_date" => $typeFactory->timestamp($info['created']),
351					"{$baseKey}_modification_date" => $typeFactory->timestamp($info['lastModif']),
352					"{$baseKey}_freshness_days" =>  $typeFactory->numeric($freshness_days),
353				];
354			}
355		}
356
357		return $data;
358	}
359
360	function getProvidedFields()
361	{
362		$baseKey = $this->getBaseKey();
363
364		$data = [
365			$baseKey, // the page name
366			"{$baseKey}_text", // wiki text (parsed)
367			"{$baseKey}_raw",  // unparsed wiki markup
368			"{$baseKey}_creation_date", // wiki page creation date
369			"{$baseKey}_modification_date", // wiki page modification date
370			"{$baseKey}_freshness_days", // wiki page "freshness" in days
371		];
372
373		return $data;
374	}
375
376	function getGlobalFields()
377	{
378		$baseKey = $this->getBaseKey();
379
380		$data = [
381			"{$baseKey}_text" => true,
382		];
383
384		return $data;
385	}
386
387	function getTabularSchema()
388	{
389		$definition = $this->getTrackerDefinition();
390		$schema = new Tracker\Tabular\Schema($definition);
391
392		$permName = $this->getConfiguration('permName');
393		$name = $this->getConfiguration('name');
394		$insertId = $this->getInsertId();
395		$baseKey = $this->getBaseKey();
396		$fieldIdForPagename = $this->getOption('fieldIdForPagename');
397		$fieldForPagename = $definition->getField($fieldIdForPagename);
398
399
400		$plain = function () {
401			return function ($value, $extra) {
402				if (isset($extra['text'])) {	// indexed value from addQuerySource _raw indexed field
403					$value = $extra['text'];
404				} else {
405					// not indexed yet, need to find page contents for $value
406					if (TikiLib::lib('tiki')->page_exists($value)) {
407						// Get wiki page content
408						$page_info = TikiLib::lib('tiki')->get_page_info($value);
409						$value = $page_info['data'];
410					}
411				}
412
413				return $value;
414			};
415		};
416
417		$render = function () use ($plain) {
418			$f = $plain();
419			return function ($value, $extra) use ($f) {
420				$value = $f($value, $extra);
421
422				return $this->attemptParse($value);
423			};
424		};
425
426		$schema->addNew($permName, 'default')
427			->setLabel($name)
428			->setRenderTransform(function ($value) {
429				return $value;
430			})
431			->setParseIntoTransform(function (& $info, $value) use ($permName) {
432				$info['fields'][$permName] = $value;
433			});
434
435		$schema->addNew($permName, 'content-raw')
436			->setLabel($name)
437			->addQuerySource('text', "{$baseKey}_raw")
438			->setRenderTransform($plain())
439			->setParseIntoTransform(function (& $info, $value) use ($permName, $fieldForPagename, $insertId) {
440				$data = $this->getFieldData([
441					$insertId => $value,
442					'ins_' . $fieldForPagename['fieldId'] => $info['fields'][$fieldForPagename['permName']],
443					'itemId' => empty($info['itemId']) ? 0 : $info['itemId'],
444				]);
445				$info['fields'][$permName] = $data['value'];
446			});
447
448		// convert incoming html to wiki syntax and the opposite on export
449		$schema->addNew($permName, 'content-wiki-html')
450			->setLabel($name)
451			->addQuerySource('text', "{$baseKey}_raw")
452			->setRenderTransform($render())
453			->setParseIntoTransform(function (& $info, $value) use ($permName, $fieldForPagename, $insertId) {
454				$data = $this->getFieldData([
455					$this->getInsertId() => TikiLib::lib('edit')->parseToWiki($value),
456					'ins_' . $fieldForPagename['fieldId'] => $info['fields'][$fieldForPagename['permName']],
457					'itemId' => empty($info['itemId']) ? 0 : $info['itemId'],
458				]);
459				$info['fields'][$permName] = $data['value'];
460			});
461
462		return $schema;
463	}
464
465	protected function attemptParse($text)
466	{
467		global $prefs;
468
469		$parseOptions = [];
470		if ($this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') {
471			$parseOptions['is_html'] = true;
472		}
473		return TikiLib::lib('parser')->parse_data($text, $parseOptions);
474	}
475
476	/**
477	 * Gets the full page name including the namespace and separator
478	 *
479	 * @param $short_name
480	 * @return string
481	 */
482	private function getFullPageName($short_name)
483	{
484		global $prefs;
485
486		if (empty($short_name)) {
487			return '';
488		}
489
490		$namespace = $this->getOption('namespace');
491		if ($namespace == 'none') {
492			$page_name = $short_name;
493		} elseif ($namespace == 'custom' && ! empty($this->getOption('customnamespace'))) {
494			$page_name = $this->getOption('customnamespace') . $prefs['namespace_separator'] . $short_name;
495		} else {
496			$page_name = 'trackerfield' . $this->getConfiguration('fieldId') . $prefs['namespace_separator'] . $short_name;
497		}
498
499		$page_name = $this->cleanPageName($page_name);
500
501		return $page_name;
502	}
503
504	/**
505	 * Gets and cleans the specified page name (i.e. the fieldIdForPagename field value with or without the namespace)
506	 * @param $page_name
507	 * @return string
508	 */
509	private function cleanPageName($page_name)
510	{
511		$wikilib = TikiLib::lib('wiki');
512		if ($this->getOption('removeBadChars') === 'y' && $wikilib->contains_badchars($page_name)) {
513			$bad_chars = $wikilib->get_badchars();
514			$page_name = preg_replace('/[' . preg_quote($bad_chars, '/') . ']/', ' ', $page_name);
515			$page_name = trim(preg_replace('/\s+/', ' ', $page_name));
516		}
517		if (strlen($page_name) > 160) {
518			$page_name = substr($page_name, 0, 160);
519		}
520		return $page_name;
521	}
522}
523