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//this script may only be included - so its better to die if called directly.
9if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
10	header("location: index.php");
11	exit;
12}
13
14/**
15 *
16 */
17class ModLib extends TikiLib
18{
19
20	public $pref_errors = [];
21
22	// additional module zones added to this array will be exposed to tiki.tpl
23	// TODO change modules user interface to enable additional zones
24	public $module_zones = [
25		'top' => 'top_modules',
26		'topbar' => 'topbar_modules',
27		'pagetop' => 'pagetop_modules',
28		'left' => 'left_modules',
29		'right' => 'right_modules',
30		'pagebottom' => 'pagebottom_modules',
31		'bottom' => 'bottom_modules',
32	];
33
34	public $cssfiles  = [
35		'calendar_new'	=> [
36			'csspath'	=> 'themes/base_files/feature_css/calendar.css',
37			'rank'		=> 20,
38		],
39		'action_calendar'	=> [
40			'csspath'	=> 'themes/base_files/feature_css/calendar.css',
41			'rank'		=> 20,
42		],
43	];
44
45	function __construct()
46	{
47		global $prefs;
48
49		if (! empty($prefs['module_zone_available_extra'])) {
50			foreach (array_filter((array) $prefs['module_zone_available_extra']) as $name) {
51				$this->module_zones[$name] = $name . '_modules';
52			}
53		}
54	}
55
56	/**
57	 * @param      $name
58	 * @param      $title
59	 * @param      $data
60	 * @param null $parse
61	 *
62	 * @return TikiDb_Pdo_Result
63	 * @throws Exception
64	 */
65	function replace_user_module($name, $title, $data, $parse = null)
66	{
67		global $prefs;
68
69		if ((! empty($name)) && (! empty($data))) {
70			$query = "delete from `tiki_user_modules` where `name`=?";
71			$this->query($query, [$name], -1, -1, false);
72			$query = "insert into `tiki_user_modules`(`name`,`title`,`data`, `parse`) values(?,?,?,?)";
73			$result = $this->query($query, [$name,$title,$data,$parse]);
74
75			$cachelib = TikiLib::lib('cache');
76			$cachelib->invalidate("user_modules_$name");
77
78			$wikilib = TikiLib::lib('wiki');	// used to require lib/wiki/wikilib.php where convertToTiki9 lives
79			$converter = new convertToTiki9();
80			$converter->saveObjectStatus($name, 'tiki_user_modules', 'new9.0+');
81
82			return $result;
83		}
84	}
85
86	/**
87	 * @param int  $moduleId
88	 * @param      $name
89	 * @param      $title
90	 * @param      $position
91	 * @param      $order
92	 * @param int  $cache_time
93	 * @param int  $rows
94	 * @param null $groups
95	 * @param null $params
96	 * @param null $type
97	 *
98	 * @return bool
99	 * @throws Exception
100	 */
101	function assign_module($moduleId = 0, $name, $title, $position, $order, $cache_time = 0, $rows = 10, $groups = null, $params = null, $type = null)
102	{
103		//check for valid values
104		$cache_time = is_numeric($cache_time) ? $cache_time : 0;
105		$rows = is_numeric($rows) ? $rows : 10;
106
107		if (is_array($params)) {
108			$params = $this->serializeParameters($name, $params);
109		}
110
111		if ($moduleId) {
112			$query = "update `tiki_modules` set `name`=?,`title`=?,`position`=?,`ord`=?,`cache_time`=?,`rows`=?,`groups`=?,`params`=?,`type`=? where `moduleId`=?";
113			$result = $this->query($query, [$name,$title,$position,(int) $order,(int) $cache_time,(int) $rows,$groups,$params,$type, $moduleId]);
114			if (! $result || ! $result->numRows()) {
115				$moduleId = false;
116			}
117		} else {
118			$query = "delete from `tiki_modules` where `name`=? and `position`=? and `ord`=? and `params`=?";
119			$this->query($query, [$name, $position, (int)$order, $params]);
120			$query = "insert into `tiki_modules`(`name`,`title`,`position`,`ord`,`cache_time`,`rows`,`groups`,`params`,`type`) values(?,?,?,?,?,?,?,?,?)";
121			$this->query($query, [$name,$title,$position,(int) $order,(int) $cache_time,(int) $rows,$groups,$params,$type]);
122			$moduleId = $this->lastInsertId(); //to return the recently created module
123			if ($type == "D" || $type == "P") {
124				$query = 'select `moduleId` from `tiki_modules` where `name`=? and `title`=? and `position`=? and `ord`=? and `cache_time`=? and `rows`=? and `groups`=? and `params`=? and `type`=?';
125				$moduleId = $this->getOne($query, [$name,$title,$position,(int) $order,(int) $cache_time,(int) $rows,$groups,$params,$type]);
126			}
127		}
128		if ($type == "D" || $type == "P") {
129			$usermoduleslib = TikiLib::lib('usermodules');
130			$usermoduleslib->add_module_users($moduleId, $name, $title, $position, $order, $cache_time, $rows, $groups, $params, $type);
131		}
132		return $moduleId;
133	}
134
135	/* Returns the requested module assignation. A module assignation is represented by an array similar to a tiki_modules record. The groups field is unserialized in the module_groups key, a spaces-separated list of groups. */
136	/**
137	 * @param $moduleId
138	 * @return mixed
139	 */
140	function get_assigned_module($moduleId)
141	{
142		$query = "select * from `tiki_modules` where `moduleId`=?";
143		$result = $this->query($query, [$moduleId]);
144		$res = $result->fetchRow();
145
146		if ($res["groups"]) {
147			$grps = unserialize($res["groups"]);
148			if (! empty($grps)) {
149				$res["module_groups"] = implode(' ', $grps);
150			}
151		}
152
153		return $res;
154	}
155
156	/**
157	 * @param $moduleId
158	 * @return bool
159	 */
160	function unassign_module($moduleId)
161	{
162		$query = "delete from `tiki_modules` where `moduleId`=?";
163		$result = $this->query($query, [$moduleId]);
164		$query = "delete from `tiki_user_assigned_modules` where `moduleId`=?";
165		$this->query($query, [$moduleId]);
166		return $result && $result->numRows();
167	}
168
169	/**
170	 * @param $name
171	 * @return int
172	 */
173	function get_rows($name)
174	{
175		$query = "select `rows` from `tiki_modules` where `name`=?";
176
177		$rows = $this->getOne($query, [$name]);
178
179		if ($rows == 0) {
180			$rows = 10;
181		}
182
183		return $rows;
184	}
185
186	/**
187	 * @param $moduleId
188	 *
189	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
190	 */
191	function module_up($moduleId)
192	{
193		$query = "update `tiki_modules` set `ord`=`ord`-1 where `moduleId`=?";
194		return $this->query($query, [$moduleId]);
195	}
196
197	/**
198	 * @param $moduleId
199	 *
200	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
201	 */
202	function module_down($moduleId)
203	{
204		$query = "update `tiki_modules` set `ord`=`ord`+1 where `moduleId`=?";
205		return $this->query($query, [$moduleId]);
206	}
207
208	/**
209	 * Sets position of module to left - this method does not appear to be used
210	 *
211	 * @param $moduleId
212	 *
213	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
214	 */
215	function module_left($moduleId)
216	{
217		$query = "update `tiki_modules` set `position`='left' where `moduleId`=?";
218		return $this->query($query, [$moduleId]);
219	}
220
221	/**
222	 * Sets position of module as right - this method does not appear to be used
223	 *
224	 * @param $moduleId
225	 *
226	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
227	 */
228	function module_right($moduleId)
229	{
230		$query = "update `tiki_modules` set `position`='right' where `moduleId`=?";
231		return $this->query($query, [$moduleId]);
232	}
233
234	/**
235	 * Reset all module ord's according to supplied array or by displayed order
236	 *
237	 * @param array $module_order [zone][moduleId] (optional)
238	 *
239	 * @return int
240	 */
241	function reorder_modules($module_order = [])
242	{
243		global $user;
244		$all_modules = $this->get_modules_for_user($user, $this->module_zones);
245		if (empty($module_order)) {	// rewrite module order as displayed
246			foreach ($all_modules as $zone => $contents) {
247				$module_order[$zone] = [];
248				foreach ($contents as $index => $module) {
249					$module_order[$zone][$index] = (int) $module['moduleId'];
250				}
251			}
252		}
253		$section_map = array_flip($this->module_zones);
254		$bindvars = [];
255		$query = 'UPDATE `tiki_modules` SET `ord`=?, `position`=? WHERE `moduleId`=?;';
256		$i = 0;
257		foreach ($module_order as $zone => $contents) {
258			$section_initial = $section_map[$zone];
259			foreach ($contents as $index => $moduleId) {
260				if ($moduleId) {
261					if ($all_modules[$zone][$index]['moduleId'] != $moduleId
262						|| ($all_modules[$zone][$index]['ord'] != $index + 1
263						|| $all_modules[$zone][$index]['position'] != $section_initial))
264					{
265						$bindvars = [
266							$index + 1,
267							$section_initial,
268							$moduleId,
269						];
270						$result = $this->query($query, $bindvars);
271						if ($result && $result->numRows()) {
272							$i = $i + $result->numRows();
273						}
274					}
275				}
276			}
277		}
278		return $i;
279	}
280
281	/**
282	 * @return array
283	 */
284	function get_all_modules()
285	{
286		$user_modules = $this->list_user_modules();
287
288		$all_modules = [];
289
290		foreach ($user_modules["data"] as $um) {
291			$all_modules[] = $um["name"];
292		}
293
294		// Now add all the system modules
295		$h = opendir("templates/modules");
296		while (($file = readdir($h)) !== false) {
297			if (substr($file, 0, 4) == 'mod-' && preg_match("/\.tpl$/", $file)) {
298				if (! strstr($file, "nocache")) {
299					$name = substr($file, 4, strlen($file) - 8);
300
301					$all_modules[] = $name;
302				}
303			}
304		}
305		closedir($h);
306		return $all_modules;
307	}
308
309	/**
310	 * @param $name
311	 *
312	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
313	 * @throws Exception
314	 */
315	function remove_user_module($name)
316	{
317
318		$query = "delete from `tiki_modules` where `name`=?";
319		$this->query($query, [$name]);
320
321		$query = " delete from `tiki_user_modules` where `name`=?";
322		$result = $this->query($query, [$name]);
323
324		$cachelib = TikiLib::lib('cache');
325		$cachelib->invalidate('user_modules');
326
327		return $result;
328	}
329
330	/**
331	 * @param string $sort_mode
332	 * @return array
333	 */
334	function list_user_modules($sort_mode = 'name_asc')
335	{
336		$query = "select * from `tiki_user_modules` order by " . $this->convertSortMode($sort_mode);
337
338		$result = $this->query($query, []);
339		$query_cant = "select count(*) from `tiki_user_modules`";
340		$cant = $this->getOne($query_cant, []);
341		$ret = [];
342
343		while ($res = $result->fetchRow()) {
344			$ret[] = $res;
345		}
346
347		$retval = [];
348		$retval["data"] = $ret;
349		$retval["cant"] = $cant;
350		return $retval;
351	}
352
353	/**
354	 * @return int
355	 */
356	function clear_cache()
357	{
358		global $tikidomain;
359		$dircache = "modules/cache";
360		if ($tikidomain) {
361			$dircache .= "/$tikidomain";
362		}
363		$h = opendir($dircache);
364		$i = 0;
365		while (($file = readdir($h)) !== false) {
366			if (substr($file, 0, 3) == 'mod') {
367				$file = "$dircache/$file";
368				$result = unlink($file);
369				if ($result) {
370					$i++;
371				}
372			}
373		}
374		closedir($h);
375		return $i;
376	}
377	/* @param module_info = info of a module
378	 * @param user_groups = list of groups of a user
379	 * @param user = the user
380	 * @return string 'y' = ok, 'n' = not ok
381	 */
382	function check_groups($module_info, $user, $user_groups)
383	{
384		global $prefs, $tiki_p_admin;
385		if (empty($user)) {
386			$user_groups = [ 'Anonymous' ];
387		}
388		$pass = 'y';
389		if ($tiki_p_admin == 'y' && $prefs['modhideanonadmin'] == 'y' && $module_info['groups'] == serialize(['Anonymous']) &&
390				strpos($_SERVER["SCRIPT_NAME"], 'tiki-admin_modules.php') === false) {
391			$pass = 'n';
392		} elseif ($tiki_p_admin != 'y' && $prefs['modallgroups'] != 'y') {
393			if ($module_info['groups']) {
394				if (! is_array($module_info['groups'])) {
395					$module_groups = unserialize($module_info['groups']);
396				} else {
397					$module_groups = $module_info['groups'];
398				}
399			} else {
400				$module_groups = [];
401			}
402			if (! empty($module_groups)) {	// if no groups are set show to all users (modules revamp [MOD] in Tiki 7)
403				$pass = 'n';
404				if ($prefs['modseparateanon'] !== 'y') {
405					foreach ($module_groups as $mod_group) {
406						if (in_array($mod_group, $user_groups)) {
407							$pass = 'y';
408							break;
409						}
410					}
411				} else {
412					if (! $user) {
413						if (in_array('Anonymous', $module_groups)) {
414							$pass = 'y';
415						}
416					} else {
417						foreach ($module_groups as $mod_group) {
418							if ($mod_group === 'Anonymous') {
419								continue;
420							}
421							if (in_array($mod_group, $user_groups)) {
422								$pass = 'y';
423								break;
424							}
425						}
426					}
427				}
428			}
429		}
430		return $pass;
431	}
432
433	/**
434	 * @param $user
435	 * @param array $module_zones
436	 * @return array
437	 */
438	function get_modules_for_user($user, array $module_zones = [])
439	{
440		if (empty($module_zones)) {
441			$module_zones = $this->module_zones;
442		}
443		$list = $this->get_raw_module_list_for_user($user, $module_zones);
444
445		foreach ($list as & $partial) {
446			$partial = array_map([ $this, 'augment_module_parameters' ], $partial);
447			if (! $this->is_admin_mode(true)) {
448				$partial = array_values(array_filter($partial, [ $this, 'filter_active_module' ]));
449			}
450		}
451
452		return $list;
453	}
454
455	/**
456	 * @param $module
457	 * @return mixed
458	 */
459	function augment_module_parameters($module)
460	{
461		global $prefs;
462
463		parse_str($module['params'], $module_params);
464		$default_params = [
465			'decorations' => 'y',
466			'overflow' => 'n',
467			'nobox' => 'n',
468			'notitle' => 'n',
469			'error' => '',
470			'flip' => ( $prefs['user_flip_modules'] == 'module' ) ? 'n' : $prefs['user_flip_modules'],
471		];
472
473		if (! is_array($module_params)) {
474			$module_params = [];
475		}
476
477		$module_params = array_merge($default_params, $module_params);
478
479		$module_params['module_position'] = $module['position'];
480		$module_params['module_ord'] = $module['ord'];
481
482		if ($module['name'] == 'package' && ! empty($module_params['otherparams'])) {
483			parse_str($module_params['otherparams'], $other_params);
484			if (is_array($other_params)) {
485				$module_params = $module_params + $other_params;
486			}
487		}
488
489		if ($prefs['user_flip_modules'] === 'n') {
490			$module_params['flip'] = 'n';
491		}
492
493		if (isset($module_params['section']) && $module_params['section'] == 'wiki') {
494			$module_params['section'] = 'wiki page';
495		}
496
497		$module['params'] = $module_params;
498
499		return $module;
500	}
501
502	/**
503	 * @param $module
504	 * @return bool
505	 */
506	function filter_active_module($module)
507	{
508		global $section, $page, $prefs, $user;
509		$tikilib = TikiLib::lib('tiki');
510		// Validate preferences
511		$module_info = $this->get_module_info($module['name']);
512		$params = $module['params'];
513
514		if ($prefs['feature_perspective'] == 'y') {
515			$perspectivelib = TikiLib::lib('perspective');
516			$persp = $perspectivelib->get_current_perspective($prefs);
517			if (empty($persp)) {
518				$persp = 0;
519			}
520			if (isset($params['perspective']) && ! in_array($persp, (array) $params['perspective'])) {
521				return false;
522			}
523		}
524
525		if (isset($params["lang"]) && ! in_array($prefs['language'], (array) $params["lang"])) {
526			return false;
527		}
528
529		if (isset($params['section']) && ( ! isset($section)  || ! in_array($section, (array) $params['section']))) {
530			return false;
531		}
532
533		if (isset($params['nopage']) && isset($page) && isset($section) && $section == 'wiki page') {
534			if (in_array($page, (array) $params['nopage'])) {
535				return false;
536			}
537		}
538
539		if (isset($params['page'])) {
540			if (! isset($section) || $section != 'wiki page' || ! isset($page)) { // must be in a page
541				return false;
542			} elseif (! in_array($page, (array) $params['page'])) {
543				return false;
544			}
545		}
546
547		if (isset($params['theme'])) {
548			global $tc_theme;
549
550			$ok = false;
551			foreach ((array) $params['theme'] as $t) {
552				// remove any css extension
553				$t = preg_replace('/\.css$/i', '', $t);
554				if ($t{0} != '!') { // usual behavior
555					if (! empty($tc_theme) && $t === $tc_theme) {
556						$ok = true;
557					} elseif ($t === $prefs['theme'] && empty($tc_theme)) {
558						$ok = true;
559					}
560				} else { // negation behavior
561					$excluded_theme = substr($t, 1);
562					$ok = true;
563					if (! empty($tc_theme) && $excluded_theme === $tc_theme) {
564						return false;
565					} elseif ($excluded_theme === $prefs['theme'] && empty($tc_theme)) {
566						return false;
567					}
568				}
569			}
570			if (! $ok) {
571				return false;
572			}
573		}
574
575		if (! Perms::get()->admin) {
576			$user_groups = Perms::get()->getGroups();
577		} else {
578			$user_groups = [];
579		}
580
581		if ('y' != $this->check_groups($module, $user, $user_groups)) {
582			return false;
583		}
584
585		if (isset($params['creator']) && $section == 'wiki page' && isset($page)) {
586			if (! $page_info = $tikilib->get_page_info($page)) {
587				return false;
588			} elseif ($params['creator'] == 'y' && $page_info['creator'] != $user) {
589				return false;
590			} elseif ($params['creator'] == 'n' && $page_info['creator'] == $user) {
591				return false;
592			}
593		}
594
595		if (isset($params['contributor']) && $section == 'wiki page' && isset($page)) {
596			if (! $page_info = $tikilib->get_page_info($page)) {
597				return false;
598			} else {
599				$wikilib = TikiLib::lib('wiki');
600				$contributors = $wikilib->get_contributors($page);
601				$contributors[] = $page_info['creator'];
602				$in = in_array($user, $contributors);
603
604				if ($params['contributor'] == 'y' && ! $in) {
605					return false;
606				} elseif ($params['contributor'] == 'n' && $in) {
607					return false;
608				}
609			}
610		}
611
612		if ($module['name'] == 'login_box' && (basename($_SERVER['SCRIPT_NAME']) == 'tiki-login_scr.php' || basename($_SERVER['SCRIPT_NAME']) == 'tiki-login_openid.php')) {
613			return false;
614		}
615
616		if ($prefs['feature_categories'] == 'y') {
617			if ($this->is_hidden_by_category($params)) {
618				return false;
619			}
620
621			if ($this->is_hidden_by_no_category($params)) {
622				return false;
623			}
624		}
625
626		if ($prefs['cookie_consent_feature'] == 'y' && $prefs['cookie_consent_disable'] !== 'y') {		// check if consent required to show
627			if (! empty($params['cookie_consent']) && $params['cookie_consent'] === 'y') {
628				global $feature_no_cookie;
629				if ($feature_no_cookie) {
630					return false;
631				}
632			}
633		}
634
635		foreach ($module_info['prefs'] as $p) {
636			if ($prefs[$p] != 'y') {
637				$this->add_pref_error($module['name'], $p);
638				return false;
639			}
640		}
641
642		return true;
643	}
644
645	/**
646	 * @param $params
647	 * @return bool
648	 */
649	private function is_hidden_by_category($params)
650	{
651		global $cat_type, $cat_objid;
652		if (empty($params['category'])) {
653			return false;
654		}
655
656		if (empty($cat_type) || empty($cat_objid)) {
657			return true;
658		}
659
660		$catIds = TikiLib::lib('categ')->get_object_categories($cat_type, $cat_objid);
661
662		if (empty($catIds)) {
663			return true;
664		}
665
666		// Multi-value params of custom modules need transformation into an array
667		if (is_array($params['category'])) {
668			$categories = (array) $params['category'];
669		} else {
670			$categories = explode(';', $params['category']);
671		}
672
673		return ! $this->matches_any_in_category_list($categories, $catIds, ! empty($params['subtree']));
674	}
675
676	/**
677	 * @param $params
678	 * @return bool
679	 */
680	private function is_hidden_by_no_category($params)
681	{
682		global $cat_type, $cat_objid;
683		if (empty($params['nocategory'])) {
684			return false;
685		}
686
687		if (empty($cat_type) || empty($cat_objid)) {
688			return false;
689		}
690
691		$catIds = TikiLib::lib('categ')->get_object_categories($cat_type, $cat_objid);
692
693		if (empty($catIds)) {
694			return false;
695		}
696
697		// Multi-value params of custom modules need transformation into an array
698		if (is_array($params['nocategory'])) {
699			$categories = (array) $params['nocategory'];
700		} else {
701			$categories = explode(';', $params['nocategory']);
702		}
703
704		return $this->matches_any_in_category_list($categories, $catIds, ! empty($params['subtree']));
705	}
706
707	/**
708	 * @param $desiredList
709	 * @param $categoryList
710	 * @param bool $deep
711	 * @return bool
712	 */
713	private function matches_any_in_category_list($desiredList, $categoryList, $deep = false)
714	{
715		if (empty($categoryList)) {
716			return false;
717		}
718
719		$allcats = null;	// only needed if category names are used
720
721		foreach ($desiredList as $category) {
722			if (is_numeric($category)) {
723				if (in_array($category, $categoryList)) {
724					return true;
725				}
726			} else {
727				if (! $allcats) {
728					// gets all categories (cached but perms are checked so only load all if necessary)
729					$allcats = TikiLib::lib('categ')->getCategories();
730				}
731				foreach ($categoryList as $id) {
732					if (isset($allcats[$id]) && $allcats[$id]['name'] == $category) {
733						return true;
734					}
735				}
736			}
737		}
738
739		if ($deep) {
740			$nextList = [];
741			foreach ($categoryList as $id) {
742				if (isset($allcats[$id]) && $allcats[$id]['parentId']) {
743					$nextList[] = $allcats[$id]['parentId'];
744				}
745			}
746
747			return $this->matches_any_in_category_list($desiredList, $nextList, $deep);
748		}
749
750		return false;
751	}
752
753	/**
754	 * @param       $user
755	 * @param array $module_zones
756	 *
757	 * @return array
758	 * @throws Exception
759	 */
760	private function get_raw_module_list_for_user($user, array $module_zones)
761	{
762		global $prefs;
763		$usermoduleslib = TikiLib::lib('usermodules');
764
765		$out = array_fill_keys(array_values($module_zones), []);
766
767		if (! empty($prefs['module_file'])) {
768			$out = array_merge($out, $this->read_module_file($prefs['module_file']));
769		} elseif ($prefs['user_assigned_modules'] == 'y'
770			//need to use Perms class instead of $tiki_p_configure_modules global as the global is null
771			//for some reason when feature_modulecontrols is not set
772			&& Perms::get()->configure_modules
773			&& $user
774			&& $usermoduleslib->user_has_assigned_modules($user) ) {
775			foreach ($module_zones as $zone => $zone_name) {
776				$out[$zone_name] = $usermoduleslib->get_assigned_modules_user($user, $zone);
777			}
778		} else {
779			$modules_by_position = $this->get_assigned_modules(null, 'y');
780			foreach ($module_zones as $zone => $zone_name) {
781				if (isset($modules_by_position[$zone])) {
782					$out[$zone_name] = $modules_by_position[$zone];
783				}
784			}
785		}
786
787		return $out;
788	}
789
790	/**
791	 * @return array
792	 */
793	function list_module_files()
794	{
795		$files = [];
796		if (is_dir('modules')) {
797			if ($dh = opendir('modules')) {
798				while (($file = readdir($dh)) !== false) {
799					if (preg_match("/^mod-func-.*\.php$/", $file)) {
800						array_push($files, $file);
801					}
802				}
803				closedir($dh);
804			}
805		}
806		sort($files);
807		return $files;
808	}
809
810	/**
811	 * @param $module
812	 * @return array|mixed
813	 */
814	function get_module_info($module)
815	{
816		if (is_array($module)) {
817			$moduleName = $module['name'];
818		} else {
819			$moduleName = $module;
820		}
821
822		global $prefs;
823
824		$cachelib = TikiLib::lib('cache');
825		$cacheKey = 'module.' . $moduleName . $prefs['language'];
826		$info = $cachelib->getSerialized($cacheKey, 'module');
827
828		if ($info) {
829			if (! isset($info['cachekeygen'])) {
830				$info['cachekeygen'] = [ $this, 'createDefaultCacheKey' ];
831			}
832			return $info;
833		}
834
835		$phpfuncfile = 'modules/mod-func-' . $moduleName . '.php';
836		$info_func = "module_{$moduleName}_info";
837		$info = [];
838
839		if (file_exists($phpfuncfile)) {
840			include_once $phpfuncfile;
841
842			if (function_exists($info_func)) {
843				$info = $info_func();
844				if (! empty($info['params'])) {
845					foreach ($info['params'] as &$p) {
846						$p['section'] = 'module';
847					}
848				}
849			}
850
851			$info['type'] = 'function';
852		}
853
854		$defaults = [
855			'name' => $moduleName,
856			'description' => tra('Description not available'),
857			'type' => 'include',
858			'prefs' => [],
859			'params' => [],
860		];
861
862		$info = array_merge($defaults, $info);
863
864		$info['params'] = array_merge(
865			$info['params'],
866			[
867				'title' => [
868					'name' => tra('Module Title'),
869					'description' => tra('Title to display at the top of the box.'),
870					'filter' => 'striptags',
871					'section' => 'appearance',
872				],
873				'nobox' => [
874					'name' => tra('No Box'),
875					'description' => 'y|n ' . tra('Show only the content'),
876					'section' => 'appearance',
877				],
878				'decorations' => [
879					'name' => tra('Title, background, etcs'),
880					'description' => 'y|n ' . tra('Show module decorations'),
881					'section' => 'appearance',
882				],
883				'notitle' => [
884					'name' => tra('No Title'),
885					'description' => 'y|n ' . tra('Hide module title'),
886					'filter' => 'alpha',
887					'section' => 'appearance',
888				],
889				'category' => [
890					'name' => tra('Category'),
891					'description' => tra('Module displayed depending on category. Separate multiple category IDs or names by semi-colons.'),
892					'section' => 'visibility',
893					'separator' => ';',
894					'filter' => 'alnum',
895					'profile_reference' => 'category',
896				],
897				'nocategory' => [
898					'name' => tra('No Category'),
899					'description' => tra('Module is hidden depending on category. Separate multiple category IDs or names by semi-colons. This takes precedence over the category parameter above.'),
900					'section' => 'visibility',
901					'separator' => ';',
902					'filter' => 'alnum',
903					'profile_reference' => 'category',
904				],
905				'subtree' => [
906					'name' => tra('Category subtrees'),
907					'description' => tra('Consider child categories of the categories listed in "category" and "no category" to be part of those categories. (0 or 1)'),
908					'section' => 'visibility',
909					'filter' => 'int',
910				],
911				'perspective' => [
912					'name' => tra('Perspective'),
913					'description' => tra('Module is displayed only in the listed perspective ID(s). Separate multiple perspective IDs by semi-colons.'),
914					'separator' => ';',
915					'filter' => 'digits',
916					'section' => 'visibility',
917					'profile_reference' => 'perspective',
918				],
919				'lang' => [
920					'name' => tra('Language'),
921					'description' => tra('Module is displayed only when the specified language(s) in use. Designate languages by two-character language codes. Separate multiple languages by semi-colons.'),
922					'separator' => ';',
923					'filter' => 'lang',
924					'section' => 'visibility',
925				],
926				'section' => [
927					'name' => tra('Section'),
928					'description' => tra('Module is displayed only in the specified sections. Separate multiple sections by semi-colons. Choose from: blogs; calendar; categories; cms (for "articles"); contacts; directory; faqs; featured_links; file_galleries; forums; galleries (for "image galleries"); gmaps; html_pages; maps; mytiki; newsletters; poll; quizzes; surveys; trackers; user_messages; webmail; wiki page'),
929					'separator' => ';',
930					'filter' => 'striptags',
931					'section' => 'visibility',
932				],
933				'page' => [
934					'name' => tra('Page Filter'),
935					'description' => tra('Module is displayed only on the specified page(s). Separate multiple page names by semi-colons.'),
936					'separator' => ';',
937					'filter' => 'pagename',
938					'section' => 'visibility',
939					'profile_reference' => 'wiki_page',
940				],
941				'nopage' => [
942					'name' => tra('No Page'),
943					'description' => tra('Module is not displayed on the specified page(s). Separate multiple page names by semi-colons.'),
944					'separator' => ';',
945					'filter' => 'pagename',
946					'section' => 'visibility',
947					'profile_reference' => 'wiki_page',
948				],
949				'theme' => [
950					'name' => tra('Theme'),
951					'description' => tra('Module is displayed or not displayed depending on the theme. (Enter the theme\'s file name, for example, "thenews.css".) Prefix the theme name with "!" for the module to not display. Separate multiple theme names by semi-colons.'),
952					'separator' => ';',
953					'filter' => 'themename',
954					'section' => 'visibility',
955				],
956				'creator' => [
957					'name' => tra('Creator'),
958					'description' => tra('Module only available based on the relationship of the user with the wiki page. Either only creators (y) or only non-creators (n) will see the module.'),
959					'filter' => 'alpha',
960					'section' => 'visibility',
961				],
962				'contributor' => [
963					'name' => tra('Contributor'),
964					'description' => tra('Module only available based on the relationship of the user with the wiki page. Either only contributors (y) or only non-contributors (n) will see the module.'),
965					'filter' => 'alpha',
966					'section' => 'visibility',
967				],
968				'flip' => [
969					'name' => tra('Flip'),
970					'description' => tra('Users can open and close the module.'),
971					'filter' => 'alpha',
972					'section' => 'appearance',
973				],
974				'style' => [
975					'name' => tra('Style'),
976					'description' => tra('CSS style attribute (for example, to position the module)'),
977					'section' => 'appearance',
978				],
979				'class' => [
980					'name' => tra('Class'),
981					'description' => tra('Extra class (for CSS or JavaScript)'),
982					'section' => 'appearance',
983				],
984				'topclass' => [
985					'name' => tra('Containing Class'),
986					'description' => tra('Custom CSS class of div around the module.'),
987					'section' => 'appearance',
988				],
989			]
990		);
991
992		if ($prefs['cookie_consent_feature'] === 'y' && $prefs['cookie_consent_disable'] !== 'y') {
993			$info['params']['cookie_consent'] = [
994				'name' => tra('Cookie Consent'),
995				'description' => 'n|y ' . tra('Show only if consent to accept cookies has been granted.'),
996				'filter' => 'alpha',
997				'section' => 'visibility',
998			];
999		}
1000
1001		// Parameters common to several modules, but not all
1002		$common_params = [
1003			'nonums' => [
1004				'name' => tra('No Numbers'),
1005				'description' => tra('If set to "y", the module will not number list items.'),
1006				'section' => 'appearance',
1007			],
1008			'rows' => [
1009				'name' => tra('Rows'),
1010				'description' => tra('Number of rows, or items, to display.') . ' ' . tra('Default: 10.'),
1011				'section' => 'appearance',
1012			]
1013		];
1014
1015		if ($info['type'] == 'function') {
1016			foreach ($common_params as $key => $common_param) {
1017				$info['params'][$key] = $common_param;
1018			}
1019		}
1020
1021		// Parameters are not required, unless specified.
1022		if (! empty($info['params'])) {
1023			foreach ($info['params'] as &$param) {
1024				if (! isset($param['required'])) {
1025					$param['required'] = false;
1026				}
1027			}
1028		}
1029
1030		$cachelib->cacheItem($cacheKey, serialize($info), 'module');
1031
1032		if (! isset($info['cachekeygen'])) {
1033			$info['cachekeygen'] = [ $this, 'createDefaultCacheKey' ];
1034		}
1035
1036		return $info;
1037	}
1038
1039	/**
1040	 * @param $mod_reference
1041	 * @return string
1042	 */
1043	function createDefaultCacheKey($mod_reference)
1044	{
1045		global $prefs;
1046		return $mod_reference['moduleId'] . '-' . $mod_reference['name'] . '-' . $prefs['language'] . '-' .
1047			   serialize($mod_reference['params']) . (isset($_SESSION['current_perspective']) ? '-p' . $_SESSION['current_perspective'] : '');
1048	}
1049
1050	/**
1051	 * @param $mod_reference
1052	 *
1053	 * @return bool|mixed|null|string|string[]
1054	 * @throws SmartyException
1055	 */
1056
1057	function execute_module($mod_reference)
1058	{
1059		global $prefs, $tiki_p_admin;
1060		$smarty = TikiLib::lib('smarty');
1061
1062		try {
1063			$defaults = [
1064				'style' => '',
1065				'nonums' => 'n',
1066			];
1067			$module_params = isset($mod_reference['params']) ? (array) $mod_reference['params'] : [];
1068			$module_params = array_merge($defaults, $module_params); // not sure why style doesn't get set sometime but is used in the tpl
1069
1070			$mod_reference = array_merge(['moduleId' => null, 'ord' => 0, 'position' => 0, 'rows' => 10], $mod_reference);
1071
1072			$info = $this->get_module_info($mod_reference);
1073			$cachefile = $this->get_cache_file($mod_reference, $info);
1074
1075			foreach ((array) $info['prefs'] as $preference) {
1076				if ($prefs[$preference] != 'y') {
1077					$smarty->loadPlugin('smarty_block_remarksbox');
1078
1079					return smarty_block_remarksbox(
1080						[
1081							'type' => 'warning',
1082							'title' => tr('Failed to execute "%0" module', $mod_reference['name']),
1083						],
1084						tr('Missing dependencies'),
1085						$smarty,
1086						$repeat
1087					);
1088				}
1089			}
1090
1091			if (! $cachefile || $this->require_cache_build($mod_reference, $cachefile) || $this->is_admin_mode()) {
1092				if ($this->is_admin_mode()) {
1093					require_once('lib/setup/timer.class.php');
1094					$timer = new timer('module');
1095					$timer->start('module');
1096				}
1097				if ($info['type'] == "function") { // Use the module name as default module title. This can be overriden later. A module can opt-out of this in favor of a dynamic default title set in the TPL using clear_assign in the main module function. It can also be overwritten in the main module function.
1098					$smarty->assign('tpl_module_title', tra($info['name']));
1099				}
1100
1101				$smarty->assign('nonums', $module_params['nonums']);
1102
1103				if ($info['type'] == 'include') {
1104					$phpfile = 'modules/mod-' . $mod_reference['name'] . '.php';
1105
1106					if (file_exists($phpfile)) {
1107						include $phpfile;
1108					}
1109				} elseif ($info['type'] == 'function') {
1110					$function = 'module_' . $mod_reference['name'];
1111					$phpfuncfile = 'modules/mod-func-' . $mod_reference['name'] . '.php';
1112
1113					if (file_exists($phpfuncfile)) {
1114						include_once $phpfuncfile;
1115					}
1116
1117					if (function_exists($function)) {
1118						$function($mod_reference, $module_params);
1119					}
1120				}
1121
1122				$ck = getCookie('mod-' . $mod_reference['name'] . $mod_reference['position'] . $mod_reference['ord'], 'menu', 'o');
1123				$smarty->assign('module_display', ($prefs['javascript_enabled'] == 'n' || $ck == 'o'));
1124
1125				$smarty->assign_by_ref('module_rows', $mod_reference['rows']);
1126				$smarty->assign_by_ref('module_params', $module_params); // module code can unassign this if it wants to hide params
1127				$smarty->assign('module_ord', $mod_reference['ord']);
1128				$smarty->assign('module_position', $mod_reference['position']);
1129				$smarty->assign('moduleId', $mod_reference['moduleId']);
1130				if (isset($module_params['title'])) {
1131					$smarty->assign('tpl_module_title', tra($module_params['title']));
1132				}
1133				$smarty->assign('tpl_module_name', $mod_reference['name']);
1134
1135				$tpl_module_style = empty($mod_reference['module_style']) ? '' : $mod_reference['module_style'];
1136
1137				if ($tiki_p_admin == 'y' && $this->is_admin_mode() && (! $this->filter_active_module($mod_reference) ||
1138							$prefs['modhideanonadmin'] == 'y' && (empty($mod_reference['groups']) || $mod_reference['groups'] == serialize(['Anonymous'])))) {
1139					$tpl_module_style .= 'opacity: 0.5;';
1140				}
1141				if (isset($module_params['overflow']) && $module_params['overflow'] === 'y') {
1142					$tpl_module_style .= 'overflow:visible !important;';
1143				}
1144				$smarty->assign('tpl_module_style', $tpl_module_style);
1145
1146				$template = 'modules/mod-' . $mod_reference['name'] . '.tpl';
1147
1148				if (file_exists('templates/' . $template)) {
1149					$data = $smarty->fetch($template);
1150				} else {
1151					$data = $this->get_user_module_content($mod_reference['name'], $module_params);
1152				}
1153				$smarty->clear_assign('module_params'); // ensure params not available outside current module
1154				$smarty->clear_assign('tpl_module_title');
1155				$smarty->clear_assign('tpl_module_name');
1156				$smarty->clear_assign('tpl_module_style');
1157
1158				if ($this->is_admin_mode() && $timer) {
1159					$elapsed = round($timer->stop('module'), 3);
1160					$data = preg_replace('/<div /', '<div title="Module Execution Time ' . $elapsed . 's" ', $data, 1);
1161				}
1162
1163				if (! empty($cachefile) && ! $this->is_admin_mode()) {
1164					file_put_contents($cachefile, $data);
1165				}
1166			} else {
1167				$data = file_get_contents($cachefile);
1168			}
1169
1170			return $data;
1171		} catch (Exception $e) {
1172			$smarty->loadPlugin('smarty_block_remarksbox');
1173			if ($tiki_p_admin == 'y') {
1174				$message = $e->getMessage();
1175			} else {
1176				$message = tr('Contact the system administrator');
1177			}
1178			$repeat = false;
1179			return smarty_block_remarksbox(
1180				[
1181					'type' => 'warning',
1182					'title' => tr('Failed to execute "%0" module', $mod_reference['name']),
1183				],
1184				html_entity_decode($message),
1185				$smarty,
1186				$repeat
1187			);
1188		}
1189	}
1190
1191	/**
1192	 * Returns true if on the admin modules page
1193	 *
1194	 * @param bool $ifShowingHiddenModules	 - check for $_REQUEST['show_hidden_modules'] as well
1195	 *
1196	 * @return bool
1197	 */
1198	function is_admin_mode($ifShowingHiddenModules = false)
1199	{
1200		global $tiki_p_admin_modules;
1201
1202		$ok = true;
1203		if ($ifShowingHiddenModules && empty($_REQUEST['show_hidden_modules'])) {
1204			$ok = false;
1205		}
1206		return $ok && $tiki_p_admin_modules === 'y' &&
1207				strpos($_SERVER["SCRIPT_NAME"], 'tiki-admin_modules.php') !== false;
1208	}
1209
1210	/**
1211	 * @param $name
1212	 * @param $module_params
1213	 * @return mixed
1214	 */
1215	function get_user_module_content($name, $module_params)
1216	{
1217		$smarty = TikiLib::lib('smarty');
1218		$tikilib = TikiLib::lib('tiki');
1219		$smarty->assign('module_type', 'module');
1220		$info = $this->get_user_module($name);
1221		if (! empty($info)) {
1222			// test if we have a menu
1223			if (strpos($info['data'], '{menu ') === 0 and strpos($info['data'], "css=n") === false) {
1224				$smarty->assign('module_type', 'cssmenu');
1225			}
1226
1227			$info = $this->parse($info);
1228
1229			// re-assign module_params for the custom module in case a module plugin is used inside it
1230			$smarty->assign_by_ref('module_params', $module_params);
1231			$smarty->assign('user_title', tra($info['title']));
1232			$smarty->assign_by_ref('user_data', $info['data']);
1233			$smarty->assign_by_ref('user_module_name', $info['name']);
1234
1235			return $smarty->fetch('modules/user_module.tpl');
1236		}
1237	}
1238
1239	/**
1240	 * Parses custom module content if the module requires
1241	 *
1242	 * @param $info
1243	 * @return mixed
1244	 */
1245	function parse($info)
1246	{
1247		if (isset($info['parse']) && $info['parse'] == 'y') {
1248			$parserlib = TikiLib::lib('parser');
1249			$info['data'] = $parserlib->parse_data($info['data'], [
1250				'is_html' => true,
1251				'suppress_icons' => true,
1252				'typography' => false,	// typography feature breaks quotes and causes smarty compiler errors, so disable it for custom modules
1253			]);
1254			$info['title'] = $parserlib->parse_data($info['title'], [
1255				'noparseplugins' => true,
1256				'is_html' => true,
1257			]);
1258		}
1259
1260		return $info;
1261	}
1262
1263	/**
1264	 * @param $mod_reference
1265	 * @param $info
1266	 * @return null|string
1267	 */
1268	function get_cache_file($mod_reference, $info)
1269	{
1270		global $tikidomain, $user;
1271		$nocache = 'templates/modules/mod-' . $mod_reference["name"] . '.tpl.nocache';
1272
1273		// Uncacheable
1274		if (! empty($user) || $mod_reference['cache_time'] <= 0 || file_exists($nocache)) {
1275			return null;
1276		}
1277
1278		$cb = $info['cachekeygen'];
1279
1280		$cachefile = 'modules/cache/';
1281		if ($tikidomain) {
1282			$cachefile .= "$tikidomain/";
1283		}
1284
1285		$cachefile .= 'mod-' . md5(call_user_func($cb, $mod_reference));
1286
1287		return $cachefile;
1288	}
1289
1290	// Returns whether $cachefile needs to be [re]built
1291	/**
1292	 * @param $mod_reference
1293	 * @param $cachefile
1294	 * @return bool
1295	 */
1296	function require_cache_build($mod_reference, $cachefile)
1297	{
1298		$tikilib = TikiLib::lib('tiki');
1299		return ! file_exists($cachefile)
1300			|| ( $tikilib->now - filemtime($cachefile) ) >= $mod_reference['cache_time'];
1301	}
1302
1303	/**
1304	 * @param $input
1305	 * @param $params
1306	 */
1307	function dispatchValues($input, & $params)
1308	{
1309		if (is_string($input)) {
1310			parse_str($input, $module_params);
1311		} else {
1312			$module_params = $input;
1313		}
1314
1315		foreach ($params as $name => & $inner) {
1316			if (isset($module_params[$name])) {
1317				if (isset($inner['separator'])) {
1318					$inner['value'] = implode($inner['separator'], (array) $module_params[$name]);
1319				} else {
1320					$inner['value'] = $module_params[$name];
1321				}
1322			} else {
1323				$inner['value'] = null;
1324			}
1325		}
1326		// resort params into sections
1327		$reorderedparams = [];
1328		foreach ($params as $k => $p) {
1329			if (! isset($reorderedparams[$p['section']])) {
1330				$reorderedparams[$p['section']] = [];
1331			}
1332			$reorderedparams[$p['section']][$k] = $p;
1333		}
1334		$params = $reorderedparams;
1335	}
1336
1337	/**
1338	 * @param $name
1339	 * @param $params
1340	 * @return string
1341	 */
1342	function serializeParameters($name, $params)
1343	{
1344		$info = $this->get_module_info($name);
1345		$expanded = [];
1346
1347		foreach ($info['params'] as $name => $def) {
1348			if (isset($def['filter'])) {
1349				$filter = TikiFilter::get($def['filter']);
1350			} else {
1351				$filter = null;
1352			}
1353
1354			if (isset($params[$name]) && $params[$name] !== '') {
1355				if (isset($def['separator']) && strpos($params[$name], $def['separator']) !== false) {
1356					$parts = explode($def['separator'], $params[$name]);
1357
1358					if ($filter) {
1359						foreach ($parts as & $single) {
1360							$single = $filter->filter($single);
1361							$single = trim($single);
1362						}
1363					}
1364				} else {
1365					$parts = $params[$name];
1366					if ($filter) {
1367						$parts = $filter->filter($parts);
1368					}
1369				}
1370
1371				$expanded[$name] = $parts;
1372			}
1373		}
1374		if (empty($expanded)) {
1375			return '';// http_build_query return NULL or '' depending on system
1376		}
1377
1378		return http_build_query($expanded, '', '&');
1379	}
1380
1381	/**
1382	 * @param $module_name
1383	 * @param $preference_name
1384	 */
1385	function add_pref_error($module_name, $preference_name)
1386	{
1387		$this->pref_errors[] = ['mod_name' => $module_name, 'pref_name' => $preference_name];
1388	}
1389
1390
1391	/* Returns all module assignations for a certain position, or all positions (by default). A module assignation
1392	is represented by an array similar to a tiki_modules record. The groups field is unserialized in the module_groups key, a spaces-separated list of groups.
1393	If asking for a specific position, returns an array of module assignations. If not, returns an array of arrays of modules assignations indexed by positions. For example: array("l" -> array("module assignation"))
1394	TODO: Document $displayed's effect */
1395	/**
1396	 * @param null $position
1397	 * @param string $displayed
1398	 * @return array
1399	 */
1400	function get_assigned_modules($position = null, $displayed = "n")
1401	{
1402
1403		$filter = '';
1404		$bindvars = [];
1405
1406		if ($position !== null) {
1407			$filter .= 'where `position`=?';
1408			$bindvars[] = $position;
1409		}
1410
1411		if ($displayed != 'n') {
1412			$filter .= ( $filter == '' ? 'where' : 'and' ) . " (`type` is null or `type` != ?)";
1413			$bindvars[] = 'y';
1414		}
1415
1416		$query = "select * from `tiki_modules` $filter order by " . $this->convertSortMode("ord_asc");
1417
1418		$result = $this->fetchAll($query, $bindvars);
1419
1420		$ret = [];
1421		foreach ($result as $res) {
1422			if ($res["groups"] && strlen($res["groups"]) > 1) {
1423				$grps = @unserialize($res["groups"]);
1424
1425				$res["module_groups"] = '';
1426				if (is_array($grps)) {
1427					foreach ($grps as $grp) {
1428						$res["module_groups"] .= " $grp ";
1429					}
1430				}
1431			} else {
1432				$res["module_groups"] = '&nbsp;';
1433			}
1434			if ($position === null) {
1435				if (! isset($ret[$res['position']])) {
1436					$ret[$res['position']] = [];
1437				}
1438				$ret[$res['position']][] = $res;
1439			} else {
1440				$ret[] = $res;
1441			}
1442		}
1443		return $ret;
1444	}
1445
1446	/**
1447	 * @param $name
1448	 * @return mixed
1449	 */
1450	function is_user_module($name)
1451	{
1452		return $this->table('tiki_user_modules')->fetchCount(['name' => $name]);
1453	}
1454
1455	/**
1456	 * @param $name
1457	 * @return mixed
1458	 */
1459	function get_user_module($name)
1460	{
1461		return $this->table('tiki_user_modules')->fetchFullRow(['name' => $name]);
1462	}
1463
1464	/**
1465	 * @global TikiLib $tikilib
1466	 * @param bool $added shows current prefs not in defaults
1467	 * @return array (prefname => array( 'current' => current value, 'default' => default value ))
1468	 */
1469	function getModulesForExport()
1470	{
1471		$export = [];
1472		$assigned_modules = $this->get_assigned_modules();
1473
1474		foreach ($assigned_modules as $zone => $modules) {
1475			foreach ($modules as $pos => $module) {
1476				$modtogo['type'] = 'module';
1477				$modtogo['data'] = [];
1478
1479				$modtogo['data']['name'] = $module['name'];
1480				parse_str($module['params'], $modtogo['data']['params']);
1481				$modtogo['data']['groups'] = unserialize($module['groups']);
1482				$modtogo['data']['order'] = $module['ord'];
1483
1484				$modtogo['data']['position'] = str_replace('_modules', '', $this->module_zones[$module['position']]);
1485
1486				if ($this->is_user_module($module['name'])) {
1487					$um = $this->get_user_module($module['name']);
1488					if (preg_match("/^\!*\{.*\}$/", trim($um['data']), $matches)) {	// start and end with { and } makes yaml parser think it's a serialized value
1489						$um['data'] = $um['data'] . "\n";							// so force it to be a literal block
1490					}
1491					$modtogo['data']['custom'] = $um['data'];		// the yaml dumper copes with linefeeds etc as a literal block
1492					$modtogo['data']['parse'] = empty($um['parse']) ? 'n' : $um['parse'];
1493				}
1494
1495				$export[] = $modtogo;
1496			}
1497		}
1498		return $export;
1499	}
1500
1501	/**
1502	 * @param $filename
1503	 * @return array|mixed
1504	 */
1505	private function read_module_file($filename)
1506	{
1507		$cachelib = TikiLib::lib('cache');
1508
1509		$expiry = filemtime($filename);
1510		if ($modules = $cachelib->getSerialized($filename, 'modules', $expiry)) {
1511			return $modules;
1512		}
1513
1514		$content = file_get_contents($filename);
1515		if (! $content) {
1516			Feedback::error(tr('Module file "%0" not found.', $filename));
1517			return '';
1518		}
1519
1520		$profile = Tiki_Profile::fromString("{CODE(caption=>YAML)}$content{CODE}");
1521
1522		$out = array_fill_keys(array_values($this->module_zones), []);
1523		foreach ($profile->getObjects() as $object) {
1524			if ($object->getType() == 'module') {
1525				$handler = new Tiki_Profile_InstallHandler_Module($object, []);
1526
1527				$data = $handler->getData();
1528				$object->replaceReferences($data);
1529				$data = $handler->formatData($data);
1530
1531				$data['groups'] = unserialize($data['groups']);
1532				$position = $data['position'];
1533				$zone = $this->module_zones[$position];
1534				$out[$zone][] = $data;
1535			}
1536		}
1537
1538		$cachelib->cacheItem($filename, serialize($out), 'modules');
1539		return $out;
1540	}
1541}
1542
1543/**
1544 * Function made available in the template files to behave differently depending on if a zone is empty or not.
1545 */
1546function zone_is_empty($zoneName)
1547{
1548	$smarty = TikiLib::lib('smarty');
1549	$moduleZones = $smarty->getTemplateVars('module_zones');
1550
1551	$key = $zoneName . '_modules';
1552	if (empty($moduleZones[$key])) {
1553		return true;
1554	}
1555
1556	foreach ($moduleZones[$key] as $module) {
1557		$data = (string) (isset($module['data']) ? $module['data'] : '');
1558		if (! empty($data)) {
1559			return false;
1560		}
1561	}
1562
1563	return true;
1564}
1565