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
8use Tiki\Package\ComposerManager;
9
10class PreferencesLib
11{
12	private $data = [];
13	private $usageArray;
14	private $file = '';
15	private $files = [];
16	// Fake preferences controlled by the system
17	private $system_modified = [ 'tiki_release', 'tiki_version_last_check'];
18	// prefs with system info etc
19	private $system_info = [ 'fgal_use_dir', 'sender_email' ];
20
21	/**
22	 * Returns a list of preferences that can be translated
23	 *
24	 * @return array List of preferences
25	 */
26	public function getTranslatablePreferences()
27	{
28		global $prefs;
29
30		// Due to performance reasons and the small list of preferences to be translated, returned the current
31		// list of translatable preferences as hardcoded list instead of dynamically searching all preferences
32		/*
33		$translatablePreferences = [];
34		foreach ($prefs as $key => $val) {
35			$definition = $this->getPreference($key);
36			if ($definition['translatable'] === true) {
37				$translatablePreferences[] = $key;
38			}
39		}
40		*/
41
42		$translatablePreferences = [
43			'browsertitle',
44			'metatag_keywords',
45			'metatag_description',
46		];
47
48		return $translatablePreferences;
49	}
50
51	/**
52	 * Set the translated value for a given preference
53	 *
54	 * @param string $pref preference to translate
55	 * @param string $lang target language
56	 * @param string $val value for the preference
57	 * @param string $defaultLanguage the default language
58	 */
59	public function setTranslatedPreference($pref, $lang, $val, $defaultLanguage)
60	{
61		$tikiLib = TikiLib::lib('tiki');
62		if ($lang != $defaultLanguage) {
63			$pref .= "_" . $lang;
64		}
65
66		if (empty($val)) {
67			$tikiLib->delete_preference($pref);
68		} else {
69			$tikiLib->set_preference($pref, $val);
70		}
71	}
72
73	/**
74	 * Retrieve a translated preference, in a given language, or the default if not set
75	 *
76	 * @param string $name preference name
77	 * @param string $lang language to retrieve
78	 * @return string translated preference with fallback for the default preference or empty string
79	 * @throws Exception
80	 */
81	public function getTranslatedPreference($name, $lang)
82	{
83		global $prefs;
84
85		$translatedPreference = $name;
86		if ($prefs['site_language'] != $lang) {
87			$translatedPreference .= '_' . $lang;
88		}
89
90		if (isset($prefs[$translatedPreference])) {
91			return $prefs[$translatedPreference];
92		}
93
94		return '';
95	}
96
97	function getPreference($name, $deps = true, $source = null, $get_pages = false)
98	{
99		global $prefs, $systemConfiguration;
100		static $id = 0;
101		$data = $this->loadData($name);
102
103		if (! isset($data[$name])) {
104			return false;
105		}
106		$defaults = [
107			'type' => '',
108			'helpurl' => '',
109			'help' => '',
110			'dependencies' => [],
111			'packages_required' => [],
112			'extensions' => [],
113			'dbfeatures' => [],
114			'options' => [],
115			'description' => '',
116			'size' => 40,
117			'detail' => '',
118			'warning' => '',
119			'hint' => '',
120			'shorthint' => '',
121			'perspective' => true,
122			'parameters' => [],
123			'admin' => '',
124			'module' => '',
125			'permission' => '',
126			'plugin' => '',
127			'view' => '',
128			'public' => false,
129			'translatable' => false,
130		];
131		if ($data[$name]['type'] === 'textarea') {
132			$defaults['size'] = 10;
133		}
134
135		$info = array_merge($defaults, $data[$name]);
136
137		if ($source == null) {
138			$source = $prefs;
139		}
140
141		$value = isset($source[$name]) ? $source[$name] : null;
142		if (! empty($value) && is_string($value) && (strlen($value) > 1 && $value{1} == ':') && false !== $unserialized = @unserialize($value)) {
143			$value = $unserialized;
144		}
145
146		$info['preference'] = $name;
147		if (isset($info['serialize'])) {
148			$fnc = $info['serialize'];
149			$info['value'] = $fnc($value);
150		} else {
151			$info['value'] = $value;
152		}
153
154		if (! isset($info['tags'])) {
155			$info['tags'] = ['advanced'];
156		}
157
158		$info['tags'][] = $name;
159		$info['tags'][] = 'all';
160
161		if ($this->checkPreferenceState($info['tags'], 'hide')) {
162			return ['hide' => true];
163		}
164
165		$info['notes'] = [];
166
167		$info['raw'] = isset($source[$name]) ? $source[$name] : null;
168		$info['id'] = 'pref-' . ++$id;
169
170		if (! empty($info['help']) && isset($prefs['feature_help']) && $prefs['feature_help'] == 'y') {
171			if (preg_match('/^https?:/i', $info['help'])) {
172				$info['helpurl'] = $info['help'];
173			} else {
174				$info['helpurl'] = $prefs['helpurl'] . $info['help'];
175			}
176		}
177
178		/* FIXME: Dependencies are not enforced currently. TODO: Activate disabled code below to enforce dependencies
179		// The value element is deprecated. Use either "configuredValue" or "effectiveValue"  instead.
180		$info['configuredValue'] = $info['effectiveValue'] = $info['value'];
181		*/
182		if ($deps && isset($info['dependencies'])) {
183			$info['dependencies'] = $this->getDependencies($info['dependencies']);
184			/* TODO: test
185			if ($info['type'] == 'flag' &&
186				$info['effectiveValue'] = 'y' && // Optimization
187					array_filter(array_column($info['dependencies'], 'met'), function($boolean) {
188						return ! $boolean;
189					})) {
190				$info['effectiveValue'] = 'n';
191			}
192			*/
193		}
194
195		if ($deps && isset($info['packages_required']) && ! empty($info['packages_required'])) {
196			$info['packages_required'] = $this->getPackagesRequired($info['packages_required']);
197		}
198
199		$info['available'] = true;
200
201		if (! $this->checkExtensions($info['extensions'])) {
202			$info['available'] = false;
203			$info['notes'][] = tr('Unmatched system requirement. Missing PHP extension among %0', implode(', ', $info['extensions']));
204		}
205
206		if (! $this->checkDatabaseFeatures($info['dbfeatures'])) {
207			$info['available'] = false;
208			$info['notes'][] = tr('Unmatched system requirement. The database you are using does not support this feature.');
209		}
210
211		if (! isset($info['default'])) {	// missing default in prefs definition file?
212			$info['modified'] = false;
213			trigger_error(tr('Missing default for preference "%0"', $name), E_USER_WARNING);
214		} else {
215			$info['modified'] = str_replace("\r\n", "\n", $info['value']) != $info['default'];
216		}
217
218		if ($get_pages) {
219			$info['pages'] = $this->getPreferenceLocations($name);
220		}
221
222		if (isset($systemConfiguration->preference->$name)) {
223			$info['available'] = false;
224			$info['notes'][] = tr('Configuration forced by host.');
225		}
226
227		if ($this->checkPreferenceState($info['tags'], 'deny')) {
228			$info['available'] = false;
229			$info['notes'][] = tr('Disabled by host.');
230		}
231
232		if (! $info['available']) {
233			$info['tags'][] = 'unavailable';
234		}
235
236		if ($info['modified'] && $info['available']) {
237			$info['tags'][] = 'modified';
238		}
239
240		$info['tagstring'] = implode(' ', $info['tags']);
241
242		$info = array_merge($defaults, $info);
243
244		if (! empty($info['permission'])) {
245			if (! isset($info['permission']['show_disabled_features'])) {
246				$info['permission']['show_disabled_features'] = 'y';
247			}
248			$info['permission'] = 'tiki-objectpermissions.php?' . http_build_query($info['permission'], '', '&');
249		}
250
251		if (! empty($info['admin'])) {
252			if (preg_match('/^\w+$/', $info['admin'])) {
253				$info['admin'] = 'tiki-admin.php?page=' . urlencode($info['admin']);
254			}
255		}
256
257		if (! empty($info['module'])) {
258			$info['module'] = 'tiki-admin_modules.php?cookietab=3&textFilter=' . urlencode($info['module']);
259		}
260
261		if (! empty($info['plugin'])) {
262			$info['plugin'] = 'tiki-admin.php?page=textarea&amp;cookietab=2&textFilter=' . urlencode($info['plugin']);
263		}
264
265		$smarty = TikiLib::lib('smarty');
266		$smarty->loadPlugin('smarty_function_icon');
267
268		if (! empty($info['admin']) || ! empty($info['permission']) || ! empty($info['view']) || ! empty($info['module']) || ! empty($info['plugin'])) {
269			$info['popup_html'] = '<ul class="list-unstyled">';
270
271			if (! empty($info['admin'])) {
272				$icon = smarty_function_icon([ 'name' => 'settings'], $smarty->getEmptyInternalTemplate());
273				$info['popup_html'] .= '<li><a class="icon" href="' . $info['admin'] . '">' . $icon . ' ' . tra('Settings') . '</a></li>';
274			}
275			if (! empty($info['permission'])) {
276				$icon = smarty_function_icon([ 'name' => 'permission'], $smarty->getEmptyInternalTemplate());
277				$info['popup_html'] .= '<li><a class="icon" href="' . $info['permission'] . '">' . $icon . ' ' . tra('Permissions') . '</a></li>';
278			}
279			if (! empty($info['view'])) {
280				$icon = smarty_function_icon([ 'name' => 'view'], $smarty->getEmptyInternalTemplate());
281				$info['popup_html'] .= '<li><a class="icon" href="' . $info['view'] . '">' . $icon . ' ' . tra('View') . '</a></li>';
282			}
283			if (! empty($info['module'])) {
284				$icon = smarty_function_icon([ 'name' => 'module'], $smarty->getEmptyInternalTemplate());
285				$info['popup_html'] .= '<li><a class="icon" href="' . $info['module'] . '">' . $icon . ' ' . tra('Modules') . '</a></li>';
286			}
287			if (! empty($info['plugin'])) {
288				$icon = smarty_function_icon([ 'name' => 'plugin'], $smarty->getEmptyInternalTemplate());
289				$info['popup_html'] .= '<li><a class="icon" href="' . $info['plugin'] . '">' . $icon . ' ' . tra('Plugins') . '</a></li>';
290			}
291			$info['popup_html'] .= '</ul>';
292		}
293
294		if (isset($prefs['connect_feature']) && $prefs['connect_feature'] === 'y') {
295			$connectlib = TikiLib::lib('connect');
296			$currentVote = $connectlib->getVote($info['preference']);
297
298			$info['voting_html'] = '';
299
300			if (! in_array('like', $currentVote)) {
301				$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'like', tra('Like')), $smarty->getEmptyInternalTemplate());
302			} else {
303				$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unlike', tra("Don't like")), $smarty->getEmptyInternalTemplate());
304			}
305//				if (!in_array('fix', $currentVote)) {
306//					$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'fix', tra('Fix me')), $smarty);
307//				} else {
308//					$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unfix', tra("Don't fix me")), $smarty);
309//				}
310//				if (!in_array('wtf', $currentVote)) {
311//					$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'wtf', tra("What's this for?")), $smarty);
312//				} else {
313//					$info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unwtf', tra("What's this for?")), $smarty);
314//				}
315		}
316
317		if (! $info['available']) {
318			$info['parameters']['disabled'] = 'disabled';
319		}
320
321		$info['params'] = '';
322		if (! empty($info['parameters'])) {
323			foreach ($info['parameters'] as $param => $value) {
324				$info['params'] .= ' ' . $param . '="' . $value . '"';
325			}
326		}
327
328		/**
329		 * If the unified index is enabled, replace simple object selection preferences with object selectors
330		 */
331		if ($info['type'] == 'text' && ! empty($info['profile_reference']) && $prefs['feature_search'] == 'y') {
332			$objectlib = TikiLib::lib('object');
333			$type = $objectlib->getSelectorType($info['profile_reference']);
334
335			if ($type) {
336				$info['selector_type'] = $type;
337
338				if (empty($info['separator'])) {
339					$info['type'] = 'selector';
340				} else {
341					$info['type'] = 'multiselector';
342				}
343			}
344		}
345
346		foreach (['name', 'preference'] as $key) {
347			if (empty($info[$key])) {
348				trigger_error(tr('Missing preference "%0" for "%1"', $key, $name));
349			}
350		}
351
352		return $info;
353	}
354
355	private function getVoteIconParams($pref, $vote, $label)
356	{
357		$iconname = [
358			'like' => 'thumbs-up',
359			'unlike' => 'thumbs-down'
360		];
361		return [
362			'name' => $iconname[$vote],
363			'title' => $label,
364			'href' => '#', 'onclick' => 'connectVote(\'' . $pref . '\', \'' . $vote . '\', this);return false;',
365			'class' => '',
366			'iclass' => 'icon connectVoter',
367			'istyle' => 'display:none',
368		];
369	}
370
371	/**
372	 * Check preference state
373	 * @param $tags
374	 * @param $state
375	 * @return bool
376	 */
377	private function checkPreferenceState($tags, $state)
378	{
379		static $rules = null;
380
381		if (is_null($rules)) {
382			global $systemConfiguration;
383			$rules = $systemConfiguration->rules->toArray();
384			krsort($rules, SORT_NUMERIC);
385
386			foreach ($rules as & $rule) {
387				$parts = explode(' ', $rule);
388				$type = array_shift($parts);
389				$rule = [$type, $parts];
390			}
391		}
392
393
394		foreach ($rules as $rule) {
395			$intersect = array_intersect($rule[1], $tags);
396
397			if (count($intersect)) {
398				return $rule[0] == $state;
399			}
400		}
401
402		return false;
403	}
404
405	private function checkExtensions($extensions)
406	{
407		if (count($extensions) == 0) {
408			return true;
409		}
410
411		$installed = get_loaded_extensions();
412
413		foreach ($extensions as $ext) {
414			if (! in_array($ext, $installed)) {
415				return false;
416			}
417		}
418
419		return true;
420	}
421
422	private function checkDatabaseFeatures($features)
423	{
424		if (in_array('mysql_fulltext', $features)) {
425			return TikiDb::get()->isMySQLFulltextSearchSupported();
426		}
427
428		return true;
429	}
430
431	/**
432	 * Unset hidden preferences based on the configuration file settings
433	 * @param $preferences
434	 * @return array
435	 */
436	function unsetHiddenPreferences($preferences)
437	{
438		if (empty($preferences)) {
439			return [];
440		}
441
442		foreach ($preferences as $key => $preference) {
443			$preferenceInfo = $this->getPreference($preference);
444
445			if (isset($preferenceInfo['hide']) && $preferenceInfo['hide'] === true) {
446				unset($preferences[$key]);
447			}
448		}
449
450		return $preferences;
451	}
452
453	function getMatchingPreferences($criteria, $filters = null, $maxRecords = 50, $sort = '')
454	{
455		$index = $this->getIndex();
456
457		$query = new Search_Query($criteria);
458		$query->setCount($maxRecords);
459
460		if ($sort) {
461			$query->setOrder($sort);
462		}
463		if ($filters) {
464			$this->buildPreferenceFilter($query, $filters);
465		}
466		$results = $query->search($index);
467
468		$prefs = [];
469		foreach ($results as $hit) {
470			$prefs[] = $hit['object_id'];
471		}
472
473		return $prefs;
474	}
475
476	/**
477	 * @param      $handled
478	 * @param      $data
479	 * @param null $limitation
480	 *
481	 * @return array
482	 */
483
484	function applyChanges($handled, $data, $limitation = null)
485	{
486		global $user_overrider_prefs;
487		$tikilib = TikiLib::lib('tiki');
488
489		if (is_array($limitation)) {
490			$handled = array_intersect($handled, $limitation);
491		}
492
493		$resets = isset($data['lm_reset']) ? (array) $data['lm_reset'] : [];
494
495		$changes = [];
496		foreach ($handled as $pref) {
497			if (in_array($pref, $resets)) {
498				$tikilib->delete_preference($pref);
499				$changes[$pref] = ['type' => 'reset'];
500			} else {
501				$value = $this->formatPreference($pref, $data);
502				$realPref = in_array($pref, $user_overrider_prefs) ? "site_$pref" : $pref;
503				$old = $this->formatPreference($pref, [$pref => $tikilib->get_preference($realPref)]);
504				if ($old != $value) {
505					if ($tikilib->set_preference($pref, $value)) {
506						$changes[$pref] = ['type' => 'changed', 'new' => $value, 'old' => $old];
507					}
508				}
509			}
510		}
511
512		return $changes;
513	}
514
515	function formatPreference($pref, $data)
516	{
517		if (false !== $info = $this->getPreference($pref)) {
518			$function = '_get' . ucfirst($info['type']) . 'Value';
519			$value = $this->$function($info, $data);
520			return $value;
521		} else {
522			if (isset($data[$pref])) {
523				return $data[$pref];
524			}
525			return null;
526		}
527	}
528
529	function getInput(JitFilter $filter, $preferences = [], $environment = '')
530	{
531		$out = [];
532
533		foreach ($preferences as $name) {
534			$info = $this->getPreference($name);
535
536			if ($environment == 'perspective' && isset($info['perspective']) && $info['perspective'] === false) {
537				continue;
538			}
539
540			if (isset($info['filter'])) {
541				$filter->replaceFilter($name, $info['filter']);
542			}
543
544			if (isset($info['separator'])) {
545				$out[ $name ] = $filter->asArray($name, $info['separator']);
546			} else {
547				$out[ $name ] = $filter[$name];
548			}
549		}
550
551		return $out;
552	}
553
554	function getExtraSortColumns()
555	{
556		global $prefs;
557		if (isset($prefs['rating_advanced']) && $prefs['rating_advanced'] == 'y') {
558			return TikiDb::get()->fetchMap("SELECT CONCAT('adv_rating_', ratingConfigId), name FROM tiki_rating_configs");
559		} else {
560			return [];
561		}
562	}
563
564	private function loadData($name)
565	{
566		if (in_array($name, $this->system_modified)) {
567			return null;
568		}
569		if (substr($name, 0, 3) == 'tp_') {
570			$midpos = strpos($name, '__', 3);
571			$pos = strpos($name, '__', $midpos + 2);
572			$file = substr($name, 0, $pos);
573		} elseif (substr($name, 0, 7) == 'themes_') {
574			$pos = strpos($name, '_', 7 + 1);
575			$file = substr($name, 0, $pos);
576		} elseif (false !== $pos = strpos($name, '_')) {
577			$file = substr($name, 0, $pos);
578		} elseif (file_exists(__DIR__ . "/prefs/{$name}.php")) {
579			$file = $name;
580		} else {
581			$file = 'global';
582		}
583
584		return $this->getFileData($file);
585	}
586
587	private function getFileData($file, $partial = false)
588	{
589		if (! isset($this->files[$file])) {
590			   $this->realLoad($file, $partial);
591		}
592
593		$ret = [];
594		if (isset($this->files[$file])) {
595			$ret = $this->files[$file];
596		}
597
598		if ($partial) {
599			unset($this->files[$file]);
600		}
601
602		return $ret;
603	}
604
605	private function realLoad($file, $partial)
606	{
607		$inc_file = __DIR__ . "/prefs/{$file}.php";
608		if (substr($file, 0, 3) == "tp_") {
609			$paths = \Tiki\Package\ExtensionManager::getPaths();
610			$package = str_replace('__', '/', substr($file, 3));
611			$inc_file = $paths[$package] . "/prefs/{$file}.php";
612		}
613		if (preg_match('/^themes_(.*)$/', $file, $matches)) {
614			$themeName = $matches[1];
615			$themePath = TikiLib::lib('theme')->get_theme_path($themeName);
616			$inc_file = $themePath . "prefs/{$file}.php";
617		}
618		if (file_exists($inc_file)) {
619			require_once $inc_file;
620			$function = "prefs_{$file}_list";
621			if (function_exists($function)) {
622				$this->files[$file] = $function($partial);
623			} else {
624				$this->files[$file] = [];
625			}
626		}
627	}
628
629	private function getDependencies($dependencies)
630	{
631		$out = [];
632
633		foreach ((array) $dependencies as $key => $dep) {
634			$info = $this->getPreference($dep, false);
635			if ($info) {
636				$out[] = [
637					'name' => $dep,
638					'label' => $info['name'],
639					'type' => $info['type'],
640					'link' => 'tiki-admin.php?lm_criteria=' . urlencode($info['name']),
641					'met' =>
642						( $info['type'] == 'flag' && $info['value'] == 'y' )
643						|| ( $info['type'] != 'flag' && ! empty($info['value']) )
644				];
645			} elseif ($key == 'profiles') {
646				foreach ((array) $dep as $profile) {
647					$out[] = [
648						'name' => $profile,
649						'label' => $profile,
650						'type' => 'profile',
651						'link' => 'tiki-admin.php?page=profiles&list=List&profile=' . urlencode($profile),
652						'met' => // FIXME: $info is false, the following surely won't behave as intended. This should indicate whether the profile was applied.
653						( $info['type'] == 'flag' && $info['value'] == 'y' )
654							|| ( $info['type'] != 'flag' && ! empty($info['value']) )
655					];
656				}
657			}
658		}
659
660		return $out;
661	}
662
663	private function getPackagesRequired($packages)
664	{
665		$out = [];
666
667		foreach ((array) $packages as $key => $dep) {
668			$met = class_exists($dep) || file_exists($dep);
669
670			$package = [
671				'name' => $key,
672				'label' => $key,
673				'type' => 'composer',
674				'link' => 'tiki-admin.php?page=packages',
675				'met' => $met
676			];
677
678			if ($packageInfo = ComposerManager::getPackageInfo($key)) {
679				$package['name'] = $packageInfo['name'];
680				$package['label'] = $packageInfo['name'];
681
682				if (! empty($packageInfo['link'])) {
683					$package['link'] = $packageInfo['link'];
684				}
685			}
686
687			$out[] = $package;
688		}
689
690		return $out;
691	}
692
693	/**
694	 * @param bool $fallback Rebuild fallback search index
695	 * @return Search_Index_Interface|\ZendSearch\Lucene\SearchIndexInterface|null
696	 * @throws Exception
697	 */
698	public function rebuildIndex($fallback = false)
699	{
700		global $prefs;
701
702		$index = TikiLib::lib('unifiedsearch')->getIndex('preference', ! $fallback);
703		$index->destroy();
704
705		$typeFactory = $index->getTypeFactory();
706
707		$indexed = [];
708
709		foreach ($this->getAvailableFiles() as $file) {
710			$data = $this->getFileData($file);
711
712			foreach ($data as $pref => $info) {
713				$prefInfo = $this->getPreference($pref);
714				if ($prefInfo) {
715					$info = $prefInfo;
716				} else {
717					$info['preference'] = $pref;
718					if (empty($info['tags'])) {
719						$info['tags'] = ['missing'];
720					}
721				}
722				$doc = $this->indexPreference($typeFactory, $pref, $info);
723				$index->addDocument($doc);
724
725				$indexed[] = $pref;
726			}
727		}
728
729		// Rebuild fallback index
730		list($fallbackEngine) = TikiLib::lib('unifiedsearch')->getFallbackEngineDetails();
731		if (! $fallback && $fallbackEngine) {
732			$defaultEngine = $prefs['unified_engine'];
733			$prefs['unified_engine'] = $fallbackEngine;
734			$this->rebuildIndex(true);
735			$prefs['unified_engine'] = $defaultEngine;
736		}
737
738		return $index;
739	}
740
741	private function getIndex()
742	{
743		$index = TikiLib::lib('unifiedsearch')->getIndex('preference');
744
745		if (! $index->exists()) {
746			$index = null;
747			return $this->rebuildIndex();
748		}
749
750		return $index;
751	}
752
753	function indexNeedsRebuilding()
754	{
755		$index = TikiLib::lib('unifiedsearch')->getIndex('preference');
756		return ! $index->exists();
757	}
758
759	public function getPreferenceLocations($name)
760	{
761		if (! $this->usageArray) {
762			$this->loadPreferenceLocations();
763		}
764
765		$pages = [];
766		foreach ($this->usageArray as $pg => $pfs) {
767			foreach ($pfs as $pf) {
768				if ($pf[0] == $name) {
769					$pages[] = [$pg, $pf[1]];
770				}
771			}
772		}
773
774		if (strpos($name, 'wikiplugin_') === 0 || strpos($name, 'wikiplugininline_') === 0) {
775			$pages[] = ['textarea', 2];	// plugins are included in textarea admin dynamically
776		}
777		if (strpos($name, 'trackerfield_') === 0) {
778			$pages[] = ['trackers', 3];	// trackerfields are also included in tracker admin dynamically
779		}
780
781		return $pages;
782	}
783
784	private function loadPreferenceLocations()
785	{
786		global $prefs;
787
788		// check for or create array of where each pref is used
789		$file = 'temp/cache/preference-usage-index';
790		if (! file_exists($file)) {
791			$prefs_usage_array = [];
792			$fp = opendir('templates/admin/');
793
794			while (false !== ($f = readdir($fp))) {
795				preg_match('/^include_(.*)\.tpl$/', $f, $m);
796				if (count($m) > 0) {
797					$page = $m[1];
798					$c = file_get_contents('templates/admin/' . $f);
799					preg_match_all('/{preference.*name=[\'"]?(\w*)[\'"]?.*}/i', $c, $m2, PREG_OFFSET_CAPTURE);
800					if (count($m2[1]) > 0) {
801						// count number of tabs in front of each found pref
802						foreach ($m2[1] as & $found) {
803							$tabs = preg_match_all('/{\/tab}/i', substr($c, 0, $found[1]), $m3);
804							if ($tabs === false) {
805								$tabs = 0;
806							} else {
807								$tabs++;
808							}
809							if ($prefs['site_layout'] !== 'classic' && $page === 'look' && $tabs > 2) {
810								$tabs--;	// hidden tab #3 for shadow layers
811							}
812							$found[1] = $tabs;	// replace char offset with tab number
813						}
814						$prefs_usage_array[$page] = $m2[1];
815					}
816				}
817			}
818			file_put_contents($file, serialize($prefs_usage_array));
819		} else {
820			$prefs_usage_array = unserialize(file_get_contents($file));
821		}
822
823		$this->usageArray = $prefs_usage_array;
824	}
825
826	private function indexPreference($typeFactory, $pref, $info)
827	{
828		$contents = [
829			$info['preference'],
830			// also index the parts of the pref name individually, e.g. wikiplugin_plugin_name as wikiplugin plugin name
831			str_replace('_', ' ', $info['preference']),
832			$info['name'],
833			isset($info['description']) ? $info['description'] : '',
834			isset($info['keywords']) ? $info['keywords'] : '',
835		];
836
837		if (isset($info['options'])) {
838			$contents = array_merge($contents, $info['options']);
839		}
840
841		return [
842			'object_type' => $typeFactory->identifier('preference'),
843			'object_id' => $typeFactory->identifier($pref),
844			'contents' => $typeFactory->plaintext(implode(' ', $contents)),
845			'tags' => $typeFactory->plaintext(implode(' ', $info['tags'])),
846		];
847	}
848
849	private function _getFlagValue($info, $data)
850	{
851		$name = $info['preference'];
852		if (isset($data[$name])&& ! empty($data[$name]) && $data[$name] != 'n') {
853			$ret = 'y';
854		} else {
855			$ret = 'n';
856		}
857
858		return $ret;
859	}
860
861	private function _getSelectorValue($info, $data)
862	{
863		$name = $info['preference'];
864		if (! empty($data[$name])) {
865			$value = $data[$name];
866
867			if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) {
868				return $filter->filter($value);
869			} else {
870				return $value;
871			}
872		}
873	}
874
875	private function _getMultiselectorValue($info, $data)
876	{
877		$name = $info['preference'];
878
879		if (isset($data[$name]) && ! empty($data[$name])) {
880			if (! is_array($data[$name])) {
881				$value = explode($info['separator'], $data[$name]);
882			} else {
883				$value = $data[$name];
884			}
885		} else {
886			$value = [];
887		}
888
889		if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) {
890			return array_map([ $filter, 'filter' ], $value);
891		} else {
892			return $value;
893		}
894	}
895
896	private function _getTextValue($info, $data)
897	{
898		$name = $info['preference'];
899
900		if (isset($info['separator']) && is_string($data[$name])) {
901			if (! empty($data[$name])) {
902				$value = explode($info['separator'], $data[$name]);
903			} else {
904				$value = [];
905			}
906		} else {
907			$value = $data[$name];
908		}
909
910		if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) {
911			if (is_array($value)) {
912				$value =  array_map([ $filter, 'filter' ], $value);
913			} else {
914				$value = $filter->filter($value);
915			}
916		}
917		return $this->applyConstraints($info, $value);
918	}
919
920	private function _getPasswordValue($info, $data)
921	{
922		$name = $info['preference'];
923
924		if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) {
925			return $filter->filter($data[$name]);
926		} else {
927			return $data[$name];
928		}
929	}
930
931	private function _getTextareaValue($info, $data)
932	{
933		$name = $info['preference'];
934
935		if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) {
936			$value = $filter->filter($data[$name]);
937		} else {
938			$value = $data[$name];
939		}
940		$value = str_replace("\r", "", $value);
941
942		if (isset($info['unserialize'])) {
943			$fnc = $info['unserialize'];
944
945			return $fnc($value);
946		} else {
947			return $value;
948		}
949	}
950
951	private function _getListValue($info, $data)
952	{
953		$name = $info['preference'];
954		$value = isset($data[$name]) ? $data[$name] : null;
955
956		$options = $info['options'];
957
958		if (isset($options[$value])) {
959			return $value;
960		} else {
961			$keys = array_keys($options);
962			return reset($keys);
963		}
964	}
965
966	private function _getMultilistValue($info, $data)
967	{
968		$name = $info['preference'];
969		$value = isset($data[$name]) ? (array) $data[$name] : [];
970
971		$options = $info['options'];
972		$options = array_keys($options);
973
974		return array_intersect($value, $options);
975	}
976
977	private function _getRadioValue($info, $data)
978	{
979		$name = $info['preference'];
980		$value = isset($data[$name]) ? $data[$name] : null;
981
982		$options = $info['options'];
983		$options = array_keys($options);
984
985		if (in_array($value, $options)) {
986			return $value;
987		} else {
988			return '';
989		}
990	}
991
992	private function _getMulticheckboxValue($info, $data)
993	{
994		return $this->_getMultilistValue($info, $data);
995	}
996
997	/**
998	 * Apply constraints (e.g., min or max) defined in the preference info. Currently only used in text type preference.
999	 *
1000	 * @param $info		array	preference info from definition
1001	 * @param $value	mixed	value submitted for the preference to be changed to
1002	 * @return 			mixed	value preference will be changed to after applying constraints
1003	 */
1004	private function applyConstraints($info, $value)
1005	{
1006		if (isset($info['constraints'])) {
1007			$original = $value;
1008			foreach ($info['constraints'] as $type => $constraint) {
1009				switch ($type) {
1010					case 'min':
1011						if ($value < $constraint) {
1012							$value = $constraint;
1013							Feedback::warning(tr('%0 set to minimum of %1 instead of submitted value of %2',
1014								$info['preference'], $constraint, $original));
1015						}
1016						break;
1017					case 'max':
1018						if ($value > $constraint) {
1019							$value = $constraint;
1020							Feedback::warning(tr('%0 set to maximum of %1 instead of submitted value of %2',
1021								$info['preference'], $constraint, $original));
1022						}
1023						break;
1024				}
1025			}
1026		}
1027		return $value;
1028	}
1029
1030	// for export as yaml
1031
1032	/**
1033	 * @global TikiLib $tikilib
1034	 * @param bool $added shows current prefs not in defaults
1035	 * @return array (prefname => array( 'current' => current value, 'default' => default value ))
1036	 */
1037	// NOTE: tikilib contains a similar method called getModifiedPreferences
1038	function getModifiedPrefsForExport($added = false)
1039	{
1040		$tikilib = TikiLib::lib('tiki');
1041
1042		$prefs = $tikilib->getModifiedPreferences();
1043
1044		$defaults = get_default_prefs();
1045		$modified = [];
1046
1047		foreach ($prefs as $pref => $value) {
1048			if (( $added && ! isset($defaults[$pref])) || (isset($defaults[$pref]) && $value !== $defaults[$pref] )) {
1049				if (! in_array($pref, $this->system_modified) && ! in_array($pref, $this->system_info)) {	// prefs modified by the system and with system info etc
1050					$preferenceInformation = $this->getPreference($pref);
1051					$modified[$pref] = [
1052						'current' => ['serial' => $value, 'expanded' => $preferenceInformation['value']],
1053					];
1054					if (isset($defaults[$pref])) {
1055						$modified[$pref]['default'] = $defaults[$pref];
1056					}
1057				}
1058			}
1059		}
1060		ksort($modified);
1061
1062		return $modified;
1063	}
1064
1065	function getDefaults()
1066	{
1067		$defaults = [];
1068
1069		foreach ($this->getAvailableFiles() as $file) {
1070			$data = $this->getFileData($file, true);
1071
1072			foreach ($data as $name => $info) {
1073				if (isset($info['default'])) {
1074					$defaults[$name] = $info['default'];
1075				} else {
1076					$defaults[$name] = '';
1077				}
1078			}
1079		}
1080
1081		return $defaults;
1082	}
1083
1084	private function getAvailableFiles()
1085	{
1086		$files = [];
1087		foreach (glob(__DIR__ . '/prefs/*.php') as $file) {
1088			if (basename($file) === "index.php") {
1089				continue;
1090			}
1091			$files[] = substr(basename($file), 0, -4);
1092		}
1093		foreach (TikiLib::lib('theme')->get_available_themes() as $theme => $label) {
1094			$themePath = TikiLib::lib('theme')->get_theme_path($theme);
1095			foreach (glob($themePath . 'prefs/*.php') as $file) {
1096				if (basename($file) === "index.php") {
1097					continue;
1098				}
1099				$files[] = substr(basename($file), 0, -4);
1100			}
1101		}
1102		foreach (\Tiki\Package\ExtensionManager::getPaths() as $path) {
1103			foreach (glob($path . '/prefs/*.php') as $file) {
1104				if (basename($file) === "index.php") {
1105					continue;
1106				}
1107				$files[] = substr(basename($file), 0, -4);
1108			}
1109		}
1110		return $files;
1111	}
1112
1113	function setFilters($tags)
1114	{
1115		global $user;
1116
1117		if (! in_array('basic', $tags)) {
1118			$tags[] = 'basic';
1119		}
1120		TikiLib::lib('tiki')->set_user_preference($user, 'pref_filters', implode(',', $tags));
1121	}
1122
1123	public function getEnabledFilters()
1124	{
1125		global $user;
1126		$tikilib = TikiLib::lib('tiki');
1127		$filters = $tikilib->get_user_preference($user, 'pref_filters', 'basic');
1128		$filters = explode(',', $filters);
1129		return $filters;
1130	}
1131
1132	function getFilters($filters = null)
1133	{
1134		if (! $filters) {
1135			$filters = $this->getEnabledFilters();
1136		}
1137
1138		$out = [
1139			'basic' => [
1140				'label' => tra('Basic'),
1141				'type' => 'positive',
1142			],
1143			'advanced' => [
1144				'label' => tra('Advanced'),
1145				'type' => 'positive',
1146			],
1147			'experimental' => [
1148				'label' => tra('Experimental'),
1149				'type' => 'negative',
1150			],
1151			'unavailable' => [
1152				'label' => tra('Unavailable'),
1153				'type' => 'negative',
1154			],
1155			'deprecated' => [
1156				'label' => tra('Deprecated'),
1157				'type' => 'negative',
1158			],
1159		];
1160
1161		foreach ($out as $key => & $info) {
1162			$info['selected'] = in_array($key, $filters);
1163		}
1164
1165		return $out;
1166	}
1167
1168	private function buildPreferenceFilter($query, $input = null)
1169	{
1170		$filters = $this->getFilters($input);
1171
1172		foreach ($filters as $tag => $info) {
1173			if ($info['selected']) {
1174				$positive[] = $tag;
1175			} elseif ($info['type'] == 'negative') {
1176				$query->filterContent("NOT $tag", 'tags');
1177			}
1178		}
1179
1180		if (count($positive)) {
1181			$query->filterContent(implode(' OR ', $positive), 'tags');
1182		}
1183
1184		return $query;
1185	}
1186
1187	/***
1188	 * Store 10 most recently changed prefs for quickadmin module menu
1189	 *
1190	 * @param string $name        preference name
1191	 * @param string $auser       optional user
1192	 */
1193
1194	public function addRecent($name, $auser = null)
1195	{
1196		global $user;
1197
1198		if (! $auser) {
1199			$auser = $user;
1200		}
1201
1202		$list = (array) $this->getRecent($auser);
1203		array_unshift($list, $name);
1204		$list = array_unique($list);
1205		$list = array_slice($list, 0, 10);
1206
1207		TikiLib::lib('tiki')->set_user_preference($auser, 'admin_recent_prefs', serialize($list));
1208	}
1209
1210	/***
1211	 * Get recent prefs list
1212	 *
1213	 * @param null $auser	option user
1214	 * @return array		array of pref names
1215	 */
1216
1217	public function getRecent($auser = null)
1218	{
1219		global $user;
1220		$tikilib = TikiLib::lib('tiki');
1221
1222		if (! $auser) {
1223			$auser = $user;
1224		}
1225
1226		$recent = $tikilib->get_user_preference($auser, 'admin_recent_prefs');
1227
1228		if (empty($recent)) {
1229			return [];
1230		} else {
1231			return unserialize($recent);
1232		}
1233	}
1234
1235	/**
1236	 * Export preferences
1237	 *
1238	 * @param Tiki_Profile_Writer $writer
1239	 * @param string $preferenceName
1240	 * @param bool $all
1241	 * @return bool
1242	 */
1243	public function exportPreference(Tiki_Profile_Writer $writer, $preferenceName, $all = null)
1244	{
1245		if (isset($preferenceName) && ! $all) {
1246			$listPrefs = [];
1247			$listPrefs[$preferenceName] = true;
1248		} else {
1249			$listPrefs = $this->getModifiedPrefsForExport(true);
1250		}
1251
1252		if (empty($listPrefs)) {
1253			return false;
1254		}
1255
1256		foreach ($listPrefs as $preferenceName => $value) {
1257			if (is_string($preferenceName)) {
1258				if ($info = $this->getPreference($preferenceName)) {
1259					if (isset($info['profile_reference'])) {
1260						$writer->setPreference($preferenceName, $writer->getReference($info['profile_reference'], $info['value']));
1261					} else {
1262						$writer->setPreference($preferenceName, $info['value']);
1263					}
1264				}
1265			}
1266		}
1267
1268		return true;
1269	}
1270
1271	public function getPackagePrefs()
1272	{
1273		global $prefs;
1274		$ret = [];
1275		foreach (array_keys($prefs) as $prefName) {
1276			if (substr($prefName, 0, 3) == 'tp_') {
1277				$ret[] = $prefName;
1278			}
1279		}
1280		return $ret;
1281	}
1282
1283	/**
1284	 * Get a list of preferences that belong to themes
1285	 *
1286	 * @return array
1287	 * @throws Exception
1288	 */
1289	public function getThemePrefs()
1290	{
1291		global $prefs;
1292		$ret = [];
1293		foreach (array_keys($prefs) as $prefName) {
1294			if (substr($prefName, 0, 7) == 'themes_') {
1295				$ret[] = $prefName;
1296			}
1297		}
1298
1299		$themes = TikiLib::lib('theme')->get_available_themes();
1300		$preferences = [];
1301		foreach ($themes as $key => $theme) {
1302			$themePref = array_filter($ret, function ($pref) use ($key) {
1303				$pattern = '/^themes_' . $key . '_.*/';
1304				return preg_match($pattern, $pref);
1305			});
1306
1307			if (! empty($themePref)) {
1308				$preferences[$theme] = $themePref;
1309			}
1310		}
1311
1312		return $preferences;
1313	}
1314
1315	/**
1316	 * Filter hidden preferences using an array of preference names
1317	 * @return array
1318	 */
1319	public function filterHiddenPreferences($preferences)
1320	{
1321		$hiddenPreferences = [];
1322
1323		if(! empty($preferences)) {
1324			foreach ($preferences as $preference) {
1325				$preferenceDetails = $this->getPreference($preference['name']);
1326
1327				if (! empty($preferenceDetails['hide']) && $preferenceDetails['hide'] === true) {
1328					$hiddenPreferences[] = $preference['name'];
1329				}
1330			}
1331		}
1332
1333		return $hiddenPreferences;
1334	}
1335}
1336