1<?php
2/**
3 * basic functions used by zenphoto
4 *
5 * @package core
6 * @subpackage functions\functions-main
7 *
8 */
9// force UTF-8 Ø
10
11global $_zp_current_context_stack, $_zp_HTML_cache;
12
13if (!function_exists("json_encode")) {
14	// load the drop-in replacement library
15	require_once(dirname(__FILE__) . '/lib-json.php');
16}
17
18require_once(dirname(__FILE__) . '/functions-basic.php');
19require_once(dirname(__FILE__) . '/functions-filter.php');
20require_once(SERVERPATH . '/' . ZENFOLDER . '/lib-kses.php');
21require_once dirname(__FILE__) . '/lib-htmLawed.php';
22
23$_zp_captcha = new _zp_captcha(); // this will be overridden by the plugin if enabled.
24$_zp_HTML_cache = new _zp_HTML_cache(); // this will be overridden by the plugin if enabled.
25//setup session before checking for logon cookie
26require_once(dirname(__FILE__) . '/functions-i18n.php');
27
28if (GALLERY_SESSION) {
29	zp_session_start();
30}
31
32define('ZENPHOTO_LOCALE', setMainDomain());
33
34require_once(dirname(__FILE__) . '/load_objectClasses.php');
35
36$_zp_current_context_stack = array();
37
38$_zp_albumthumb_selector = array(array('field' => '', 'direction' => '', 'desc' => 'random'),
39		array('field' => 'id', 'direction' => 'DESC', 'desc' => gettext('most recent')),
40		array('field' => 'mtime', 'direction' => '', 'desc' => gettext('oldest')),
41		array('field' => 'title', 'direction' => '', 'desc' => gettext('first alphabetically')),
42		array('field' => 'hitcounter', 'direction' => 'DESC', 'desc' => gettext('most viewed'))
43);
44
45$_zp_missing_album = new AlbumBase(gettext('missing'), false);
46$_zp_missing_image = new Transientimage($_zp_missing_album, SERVERPATH . '/' . ZENFOLDER . '/images/err-imagenotfound.png');
47
48if (extensionEnabled('zenpage')) {
49	if (getOption('enabled-zenpage-items') == 'news-and-pages' || getOption('enabled-zenpage-items') == 'news') {
50		define('ZP_NEWS_ENABLED', true);
51	} else {
52		define('ZP_NEWS_ENABLED', false);
53	}
54	if (getOption('enabled-zenpage-items') == 'news-and-pages' || getOption('enabled-zenpage-items') == 'pages') {
55		define('ZP_PAGES_ENABLED', true);
56	} else {
57		define('ZP_PAGES_ENABLED', false);
58	}
59} else {
60	define('ZP_NEWS_ENABLED', false);
61	define('ZP_PAGES_ENABLED', false);
62}
63
64zp_register_filter('content_macro', 'getCookieInfoMacro');
65
66/**
67 * parses the allowed HTML tags for use by htmLawed
68 *
69 * @param string &$source by name, contains the string with the tag options
70 * @return array the allowed_tags array.
71 * @since 1.1.3
72 * */
73function parseAllowedTags(&$source) {
74	$source = trim($source);
75	if (substr($source, 0, 1) != "(") {
76		return false;
77	}
78	$source = substr($source, 1); //strip off the open paren
79	$a = array();
80	while ((strlen($source) > 1) && (substr($source, 0, 1) != ")")) {
81		$i = strpos($source, '=>');
82		if ($i === false) {
83			return false;
84		}
85		$tag = trim(substr($source, 0, $i));
86		//strip forbidden tags from list
87		if ($tag == 'script') {
88			return 0;
89		}
90		$source = trim(substr($source, $i + 2));
91		if (substr($source, 0, 1) != "(") {
92			return false;
93		}
94		$x = parseAllowedTags($source);
95		if ($x === false) {
96			return false;
97		}
98		$a[$tag] = $x;
99	}
100	if (substr($source, 0, 1) != ')') {
101		return false;
102	}
103	$source = trim(substr($source, 1)); //strip the close paren
104	return $a;
105}
106
107/**
108 * Search for a thumbnail for the image
109 *
110 * @param $localpath local path of the image
111 * @return string
112 */
113function checkObjectsThumb($localpath) {
114	global $_zp_supported_images;
115	$image = stripSuffix($localpath);
116	$candidates = safe_glob($image . '.*');
117	foreach ($candidates as $file) {
118		$ext = substr($file, strrpos($file, '.') + 1);
119		if (in_array(strtolower($ext), $_zp_supported_images)) {
120			return basename($image . '.' . $ext);
121		}
122	}
123	return NULL;
124}
125
126/**
127 * Returns a truncated string
128 *
129 * @param string $string souirce string
130 * @param int $length how long it should be
131 * @param string $elipsis the text to tack on indicating shortening
132 * @return string
133 */
134function truncate_string($string, $length, $elipsis = '...') {
135	if (mb_strlen($string) > $length) {
136		$string = mb_substr($string, 0, $length);
137		$pos = mb_strrpos(strtr($string, array('~' => ' ', '!' => ' ', '@' => ' ', '#' => ' ', '$' => ' ', '%' => ' ', '^' => ' ', '&' => ' ', '*' => ' ', '(' => ' ', ')' => ' ', '+' => ' ', '=' => ' ', '-' => ' ', '{' => ' ', '}' => ' ', '[' => ' ', ']' => ' ', '|' => ' ', ':' => ' ', ';' => ' ', '<' => ' ', '>' => ' ', '.' => ' ', '?' => ' ', '/' => ' ', '\\', '\\' => ' ', "'" => ' ', "`" => ' ', '"' => ' ')), ' ');
138		if ($pos === FALSE) {
139			$string .= $elipsis;
140		} else {
141			$string = mb_substr($string, 0, $pos) . $elipsis;
142		}
143	}
144	return $string;
145}
146
147/**
148 * Fixes unbalanced HTML tags. Uses the library htmlawed or if available the native PHP extension tidy
149 *
150 * @param string $html
151 * @return string
152 */
153function tidyHTML($html) {
154	if (class_exists('tidy')) {
155		$options = array(
156				'new-blocklevel-tags' => 'article aside audio bdi canvas details dialog figcaption figure footer header main nav section source summary template track video',
157				'new-empty-tags' => 'command embed keygen source track wbr',
158				'new-inline-tags' => 'audio command datalist embed keygen mark menuitem meter output progress source time video wbr srcset sizes',
159				'show-body-only' => true,
160				'indent' => true,
161				'wrap' => 0
162		);
163		$tidy = new tidy();
164		$tidy->parseString($html, $options, 'utf8');
165		$tidy->cleanRepair();
166		return trim($tidy);
167	} else {
168		return trim(htmLawed($html, array('tidy' => '2s2n')));
169	}
170}
171
172/**
173 * Returns truncated html formatted content
174 *
175 * @param string $articlecontent the source string
176 * @param int $shorten new size
177 * @param string $shortenindicator
178 * @param bool $forceindicator set to true to include the indicator no matter what
179 * @return string
180 */
181function shortenContent($articlecontent, $shorten, $shortenindicator, $forceindicator = false) {
182	global $_user_tags;
183	if ($shorten && ($forceindicator || (mb_strlen($articlecontent) > $shorten))) {
184		$allowed_tags = getAllowedTags('allowed_tags');
185		$articlecontent = html_decode($articlecontent);
186		//remove script to be replaced later
187		$articlecontent = preg_replace('~<script.*?/script>~is', '', $articlecontent);
188
189		//remove HTML comments
190		$articlecontent = preg_replace('~<!--.*?-->~is', '', $articlecontent);
191		$short = mb_substr($articlecontent, 0, $shorten);
192		$short2 = kses($short . '</p>', $allowed_tags);
193
194		if (($l2 = mb_strlen($short2)) < $shorten) {
195			$c = 0;
196			$l1 = $shorten;
197			$delta = $shorten - $l2;
198			while ($l2 < $shorten && $c++ < 5) {
199				$open = mb_strrpos($short, '<');
200				if ($open > mb_strrpos($short, '>')) {
201					$l1 = mb_strpos($articlecontent, '>', $l1 + 1) + $delta;
202				} else {
203					$l1 = $l1 + $delta;
204				}
205				$short = mb_substr($articlecontent, 0, $l1);
206				preg_match_all('/(<p>)/', $short, $open);
207				preg_match_all('/(<\/p>)/', $short, $close);
208				if (count($open) > count($close))
209					$short .= '</p>';
210				$short2 = kses($short, $allowed_tags);
211				$l2 = mb_strlen($short2);
212			}
213			$shorten = $l1;
214		}
215		$short = truncate_string($articlecontent, $shorten, '');
216		if ($short != $articlecontent || $forceindicator) { //	we actually did remove some stuff
217			// drop open tag strings
218			$open = mb_strrpos($short, '<');
219			if ($open > mb_strrpos($short, '>')) {
220				$short = mb_substr($short, 0, $open);
221			}
222			$short = tidyHTML($short . $shortenindicator);
223		}
224		$articlecontent = $short;
225	}
226	if (isset($matches)) {
227		//replace the script text
228		foreach ($matches[0] as $script) {
229			$articlecontent = $script . $articlecontent;
230		}
231	}
232	return $articlecontent;
233}
234
235/**
236 * Returns the oldest ancestor of an alubm;
237 *
238 * @param string $album an album object
239 * @return object
240 */
241function getUrAlbum($album) {
242	if (!is_object($album))
243		return NULL;
244	while (true) {
245		$parent = $album->getParent();
246		if (is_null($parent)) {
247			return $album;
248		}
249		$album = $parent;
250	}
251}
252
253/**
254 * Returns a sort field part for querying
255 * Note: $sorttype may be a comma separated list of field names. If so,
256 *       these are peckmarked and returned otherwise unchanged.
257 *
258 * @param string $sorttype the 'Display" name of the sort
259 * @param string $default the default if $sorttype is empty
260 * @param string $table the database table being used.
261 * @return string
262 */
263function lookupSortKey($sorttype, $default, $table) {
264	global $_zp_fieldLists;
265	switch (strtolower($sorttype)) {
266		case 'random':
267			return 'RAND()';
268		case "manual":
269			return '`sort_order`';
270		case "filename":
271			switch ($table) {
272				case 'images':
273					return '`filename`';
274				case 'albums':
275					return '`folder`';
276			}
277		default:
278			if (empty($sorttype)) {
279				return '`' . $default . '`';
280			}
281			if (substr($sorttype, 0) == '(') {
282				return $sorttype;
283			}
284			if (is_array($_zp_fieldLists) && isset($_zp_fieldLists[$table])) {
285				$dbfields = $_zp_fieldLists[$table];
286			} else {
287				$result = db_list_fields($table);
288				$dbfields = array();
289				if ($result) {
290					foreach ($result as $row) {
291						$dbfields[strtolower($row['Field'])] = $row['Field'];
292					}
293				}
294				$_zp_fieldLists[$table] = $dbfields;
295			}
296			$sorttype = strtolower($sorttype);
297			$list = explode(',', $sorttype);
298			foreach ($list as $key => $field) {
299				if (array_key_exists($field, $dbfields)) {
300					$list[$key] = '`' . trim($dbfields[$field]) . '`';
301				}
302			}
303			return implode(',', $list);
304	}
305}
306
307/**
308 * Returns a formated date for output
309 *
310 * @param string $format the "strftime" format string
311 * @param date $dt the date to be output
312 * @return string
313 */
314function zpFormattedDate($format, $dt) {
315	global $_zp_UTF8;
316	$fdate = strftime($format, $dt);
317	$charset = 'ISO-8859-1';
318	$outputset = LOCAL_CHARSET;
319	if (function_exists('mb_internal_encoding')) {
320		if (($charset = mb_internal_encoding()) == $outputset) {
321			return $fdate;
322		}
323	}
324	return $_zp_UTF8->convert($fdate, $charset, $outputset);
325}
326
327/**
328 * Simple SQL timestamp formatting function.
329 *
330 * @param string $format formatting template
331 * @param int $mytimestamp timestamp
332 * @return string
333 */
334function myts_date($format, $mytimestamp) {
335	$timezoneadjust = getOption('time_offset');
336
337	$month = substr($mytimestamp, 4, 2);
338	$day = substr($mytimestamp, 6, 2);
339	$year = substr($mytimestamp, 0, 4);
340
341	$hour = substr($mytimestamp, 8, 2);
342	$min = substr($mytimestamp, 10, 2);
343	$sec = substr($mytimestamp, 12, 2);
344
345	$epoch = mktime($hour + $timezoneadjust, $min, $sec, $month, $day, $year);
346	$date = zpFormattedDate($format, $epoch);
347	return $date;
348}
349
350/**
351 * Send an mail to the mailing list. We also attempt to intercept any form injection
352 * attacks by slime ball spammers. Returns error message if send failure.
353 *
354 * @param string $subject  The subject of the email.
355 * @param string $message  The message contents of the email.
356 * @param array $email_list a list of email addresses to send to
357 * @param array $cc_addresses a list of addresses to send copies to.
358 * @param array $bcc_addresses a list of addresses to send blind copies to.
359 * @param string $replyTo reply-to address
360 *
361 * @return string
362 *
363 * @author Todd Papaioannou (lucky@luckyspin.org)
364 * @since  1.0.0
365 */
366function zp_mail($subject, $message, $email_list = NULL, $cc_addresses = NULL, $bcc_addresses = NULL, $replyTo = NULL) {
367	global $_zp_authority, $_zp_gallery, $_zp_UTF8;
368	$result = '';
369	if ($replyTo) {
370		$t = $replyTo;
371		if (!isValidEmail($m = array_shift($t))) {
372			if (empty($result)) {
373				$result = gettext('Mail send failed.');
374			}
375			$result .= sprintf(gettext('Invalid “reply-to” mail address %s.'), $m);
376		}
377	}
378	if (is_null($email_list)) {
379		$email_list = $_zp_authority->getAdminEmail();
380	} else {
381		foreach ($email_list as $key => $email) {
382			if (!isValidEmail($email)) {
383				unset($email_list[$key]);
384				if (empty($result)) {
385					$result = gettext('Mail send failed.');
386				}
387				$result .= ' ' . sprintf(gettext('Invalid “to” mail address %s.'), $email);
388			}
389		}
390	}
391	if (is_null($cc_addresses)) {
392		$cc_addresses = array();
393	} else {
394		if (empty($email_list) && !empty($cc_addresses)) {
395			if (empty($result)) {
396				$result = gettext('Mail send failed.');
397			}
398			$result .= ' ' . gettext('“cc” list provided without “to” address list.');
399			return $result;
400		}
401		foreach ($cc_addresses as $key => $email) {
402			if (!isValidEmail($email)) {
403				unset($cc_addresses[$key]);
404				if (empty($result)) {
405					$result = gettext('Mail send failed.');
406				}
407				$result = ' ' . sprintf(gettext('Invalid “cc” mail address %s.'), $email);
408			}
409		}
410	}
411	if (is_null($bcc_addresses)) {
412		$bcc_addresses = array();
413	} else {
414		foreach ($bcc_addresses as $key => $email) {
415			if (!isValidEmail($email)) {
416				unset($bcc_addresses[$key]);
417				if (empty($result)) {
418					$result = gettext('Mail send failed.');
419				}
420				$result = ' ' . sprintf(gettext('Invalid “bcc” mail address %s.'), $email);
421			}
422		}
423	}
424	if (count($email_list) + count($bcc_addresses) > 0) {
425		if (zp_has_filter('sendmail')) {
426
427			$from_mail = getOption('site_email');
428			$from_name = get_language_string(getOption('site_email_name'));
429
430			// Convert to UTF-8
431			if (LOCAL_CHARSET != 'UTF-8') {
432				$subject = $_zp_UTF8->convert($subject, LOCAL_CHARSET);
433				$message = $_zp_UTF8->convert($message, LOCAL_CHARSET);
434			}
435
436			//	we do not support rich text
437			$message = preg_replace('~<p[^>]*>~', "\n", $message); // Replace the start <p> or <p attr="">
438			$message = preg_replace('~</p>~', "\n", $message); // Replace the end
439			$message = preg_replace('~<br[^>]*>~', "\n", $message); // Replace <br> or <br ...>
440			$message = preg_replace('~<ol[^>]*>~', "", $message); // Replace the start <ol> or <ol attr="">
441			$message = preg_replace('~</ol>~', "", $message); // Replace the end
442			$message = preg_replace('~<ul[^>]*>~', "", $message); // Replace the start <ul> or <ul attr="">
443			$message = preg_replace('~</ul>~', "", $message); // Replace the end
444			$message = preg_replace('~<li[^>]*>~', ".\t", $message); // Replace the start <li> or <li attr="">
445			$message = preg_replace('~</li>~', "", $message); // Replace the end
446			$message = getBare($message);
447			$message = preg_replace('~\n\n\n+~', "\n\n", $message);
448
449			// Send the mail
450			if (count($email_list) > 0) {
451				$result = zp_apply_filter('sendmail', '', $email_list, $subject, $message, $from_mail, $from_name, $cc_addresses, $replyTo); // will be true if all mailers succeeded
452			}
453			if (count($bcc_addresses) > 0) {
454				foreach ($bcc_addresses as $bcc) {
455					$result = zp_apply_filter('sendmail', '', array($bcc), $subject, $message, $from_mail, $from_name, array(), $replyTo); // will be true if all mailers succeeded
456				}
457			}
458		} else {
459			$result = gettext('Mail send failed. There is no mail handler configured.');
460		}
461	} else {
462		if (empty($result)) {
463			$result = gettext('Mail send failed.');
464		}
465		$result .= ' ' . gettext('No “to” address list provided.');
466	}
467	return $result;
468}
469
470/**
471 * Sorts the results of a DB search by the current locale string for $field
472 *
473 * @param array $dbresult the result of the DB query
474 * @param string $field the field name to sort on
475 * @param bool $descending the direction of the sort
476 * @return array the sorted result
477 */
478function sortByMultilingual($dbresult, $field, $descending) {
479	$temp = array();
480	foreach ($dbresult as $key => $row) {
481		$temp[$key] = get_language_string($row[$field]);
482	}
483	sortArray($temp);
484	if ($descending) {
485		$temp = array_reverse($temp, true);
486	}
487	$result = array();
488	foreach ($temp as $key => $v) {
489		$result[] = $dbresult[$key];
490	}
491	return $result;
492}
493
494/**
495 * Checks to see access is allowed to an album
496 * Returns true if access is allowed.
497 * There is no password dialog--you must have already had authorization via a cookie.
498 *
499 * @param string $album album object or name of the album
500 * @param string &$hint becomes populated with the password hint.
501 * @return bool
502 */
503function checkAlbumPassword($album, &$hint = NULL) {
504	global $_zp_pre_authorization, $_zp_gallery;
505	if (is_object($album)) {
506		$albumname = $album->name;
507	} else {
508		$album = newAlbum($albumname = $album, true, true);
509	}
510	if (isset($_zp_pre_authorization[$albumname])) {
511		return $_zp_pre_authorization[$albumname];
512	}
513	$hash = $album->getPassword();
514	if (empty($hash)) {
515		$album = $album->getParent();
516		while (!is_null($album)) {
517			$hash = $album->getPassword();
518			$authType = "zpcms_auth_album_" . $album->getID();
519			$saved_auth = zp_getCookie($authType);
520
521			if (!empty($hash)) {
522				if ($saved_auth == $hash) {
523					$_zp_pre_authorization[$albumname] = $authType;
524					return $authType;
525				} else {
526					$hint = $album->getPasswordHint();
527					return false;
528				}
529			}
530			$album = $album->getParent();
531		}
532		// revert all tlhe way to the gallery
533		$hash = $_zp_gallery->getPassword();
534		$authType = 'zpcms_auth_gallery';
535		$saved_auth = zp_getCookie($authType);
536		if (empty($hash)) {
537			$authType = 'zp_public_access';
538		} else {
539			if ($saved_auth != $hash) {
540				$hint = $_zp_gallery->getPasswordHint();
541				return false;
542			}
543		}
544	} else {
545		$authType = "zpcms_auth_album_" . $album->getID();
546		$saved_auth = zp_getCookie($authType);
547		if ($saved_auth != $hash) {
548			$hint = $album->getPasswordHint();
549			return false;
550		}
551	}
552	$_zp_pre_authorization[$albumname] = $authType;
553	return $authType;
554}
555
556/**
557 * Returns a consolidated list of plugins
558 * The array structure is key=plugin name, value=plugin path
559 *
560 * @param string $pattern File system wildcard matching pattern to limit the search
561 * @param string $folder subfolder within the plugin folders to search
562 * @param bool $stripsuffix set to true to remove the suffix from the key name in the array
563 * @return array
564 */
565function getPluginFiles($pattern, $folder = '', $stripsuffix = true) {
566	if (!empty($folder) && substr($folder, -1) != '/')
567		$folder .= '/';
568	$list = array();
569	$curdir = getcwd();
570	$basepath = SERVERPATH . "/" . USER_PLUGIN_FOLDER . '/' . $folder;
571	if (is_dir($basepath)) {
572		chdir($basepath);
573		$filelist = safe_glob($pattern);
574		foreach ($filelist as $file) {
575			$key = filesystemToInternal($file);
576			if ($stripsuffix) {
577				$key = stripSuffix($key);
578			}
579			$list[$key] = $basepath . $file;
580		}
581	}
582	$basepath = SERVERPATH . "/" . ZENFOLDER . '/' . PLUGIN_FOLDER . '/' . $folder;
583	if (file_exists($basepath)) {
584		chdir($basepath);
585		$filelist = safe_glob($pattern);
586		foreach ($filelist as $file) {
587			$key = filesystemToInternal($file);
588			if ($stripsuffix) {
589				$key = stripSuffix($key);
590			}
591			$list[$key] = $basepath . $file;
592		}
593	}
594	chdir($curdir);
595	return $list;
596}
597
598/**
599 * Returns the fully qualified file name of the plugin file.
600 *
601 * Note: order of selection is:
602 * 	1-theme folder file (if $inTheme is set)
603 *  2-user plugin folder file
604 *  3-zp-extensions file
605 * first file found is used
606 *
607 * @param string $plugin is the name of the plugin file, typically something.php
608 * @param bool $inTheme tells where to find the plugin.
609 *   true means look in the current theme
610 *   false means look in the zp-core/plugins folder.
611 * @param bool $webpath return a WEBPATH rather than a SERVERPATH
612 *
613 * @return string
614 */
615function getPlugin($plugin, $inTheme = false, $webpath = false) {
616	global $_zp_gallery;
617	$plugin = ltrim($plugin, './\\');
618	$pluginFile = NULL;
619	if ($inTheme === true) {
620		$inTheme = $_zp_gallery->getCurrentTheme();
621	}
622	if ($inTheme) {
623		$pluginFile = '/' . THEMEFOLDER . '/' . internalToFilesystem($inTheme . '/' . $plugin);
624		if (!file_exists(SERVERPATH . $pluginFile)) {
625			$pluginFile = false;
626		}
627	}
628	if (!$pluginFile) {
629		$pluginFile = '/' . USER_PLUGIN_FOLDER . '/' . internalToFilesystem($plugin);
630		if (!file_exists(SERVERPATH . $pluginFile)) {
631			$pluginFile = '/' . ZENFOLDER . '/' . PLUGIN_FOLDER . '/' . internalToFilesystem($plugin);
632			if (!file_exists(SERVERPATH . $pluginFile)) {
633				$pluginFile = false;
634			}
635		}
636	}
637	if ($pluginFile) {
638		if ($webpath) {
639			if (is_string($webpath)) {
640				return $webpath . filesystemToInternal($pluginFile);
641			} else {
642				return WEBPATH . filesystemToInternal($pluginFile);
643			}
644		} else {
645			return SERVERPATH . $pluginFile;
646		}
647	}
648	return false;
649}
650
651/**
652 * Returns an array of the currently enabled plugins
653 *
654 * @return array
655 */
656function getEnabledPlugins() {
657	global $_EnabledPlugins;
658	if (is_array($_EnabledPlugins)) {
659		return $_EnabledPlugins;
660	}
661	$_EnabledPlugins = array();
662	$sortlist = getPluginFiles('*.php');
663	foreach ($sortlist as $extension => $path) {
664		$opt = 'zp_plugin_' . $extension;
665		if ($option = getOption($opt)) {
666			$_EnabledPlugins[$extension] = array('priority' => $option, 'path' => $path);
667		}
668	}
669	$_EnabledPlugins = sortMultiArray($_EnabledPlugins, 'priority', true);
670	return $_EnabledPlugins;
671}
672
673/**
674 * Returns if a plugin is enabled
675 * @param string $extension
676 * @return bool
677 */
678function extensionEnabled($extension) {
679	return getOption('zp_plugin_' . $extension);
680}
681
682/**
683 * Enables a plugin
684 * @param string $extension
685 * @param int $priority
686 * @param bool $persistent
687 */
688function enableExtension($extension, $priority, $persistent = true) {
689	setOption('zp_plugin_' . $extension, $priority, $persistent);
690}
691
692/**
693 * Disables an extension
694 * @param string $extension
695 * @param bool $persistent
696 *
697 * @since ZenphotoCMS 1.5.2
698 */
699function disableExtension($extension, $persistent = true) {
700	setOption('zp_plugin_' . $extension, 0, $persistent);
701}
702
703/**
704 * Gets an array of comments for the current admin
705 *
706 * @param int $number how many comments desired
707 * @return array
708 */
709function fetchComments($number) {
710	if ($number) {
711		$limit = " LIMIT $number";
712	} else {
713		$limit = '';
714	}
715
716	$comments = array();
717	if (zp_loggedin(ADMIN_RIGHTS | COMMENT_RIGHTS)) {
718		if (zp_loggedin(ADMIN_RIGHTS | MANAGE_ALL_ALBUM_RIGHTS)) {
719			$sql = "SELECT *, (date + 0) AS date FROM " . prefix('comments') . " ORDER BY id DESC$limit";
720			$comments = query_full_array($sql);
721		} else {
722			$albumlist = getManagedAlbumList();
723			$albumIDs = array();
724			foreach ($albumlist as $albumname) {
725				$subalbums = getAllSubAlbumIDs($albumname);
726				foreach ($subalbums as $ID) {
727					$albumIDs[] = $ID['id'];
728				}
729			}
730			if (count($albumIDs) > 0) {
731				$sql = "SELECT  *, (`date` + 0) AS date FROM " . prefix('comments') . " WHERE ";
732
733				$sql .= " (`type`='albums' AND (";
734				$i = 0;
735				foreach ($albumIDs as $ID) {
736					if ($i > 0) {
737						$sql .= " OR ";
738					}
739					$sql .= "(" . prefix('comments') . ".ownerid=$ID)";
740					$i++;
741				}
742				$sql .= ")) ";
743				$sql .= " ORDER BY id DESC$limit";
744				$albumcomments = query($sql);
745				if ($albumcomments) {
746					while ($comment = db_fetch_assoc($albumcomments)) {
747						$comments[$comment['id']] = $comment;
748					}
749					db_free_result($albumcomments);
750				}
751				$sql = "SELECT *, " . prefix('comments') . ".id as id, " .
752								prefix('comments') . ".name as name, (" . prefix('comments') . ".date + 0) AS date, " .
753								prefix('images') . ".`albumid` as albumid," .
754								prefix('images') . ".`id` as imageid" .
755								" FROM " . prefix('comments') . "," . prefix('images') . " WHERE ";
756
757				$sql .= "(`type` IN (" . zp_image_types("'") . ") AND (";
758				$i = 0;
759				foreach ($albumIDs as $ID) {
760					if ($i > 0) {
761						$sql .= " OR ";
762					}
763					$sql .= "(" . prefix('comments') . ".ownerid=" . prefix('images') . ".id AND " . prefix('images') . ".albumid=$ID)";
764					$i++;
765				}
766				$sql .= "))";
767				$sql .= " ORDER BY " . prefix('images') . ".`id` DESC$limit";
768				$imagecomments = query($sql);
769				if ($imagecomments) {
770					while ($comment = db_fetch_assoc($imagecomments)) {
771						$comments[$comment['id']] = $comment;
772					}
773					db_free_result($imagecomments);
774				}
775				krsort($comments);
776				if ($number) {
777					if ($number < count($comments)) {
778						$comments = array_slice($comments, 0, $number);
779					}
780				}
781			}
782		}
783	}
784	return $comments;
785}
786
787/**
788 * Populates and returns the $_zp_admin_album_list array
789 * @return array
790 */
791function getManagedAlbumList() {
792	global $_zp_admin_album_list, $_zp_current_admin_obj;
793	$_zp_admin_album_list = array();
794	if (zp_loggedin(MANAGE_ALL_ALBUM_RIGHTS)) {
795		$sql = "SELECT `folder` FROM " . prefix('albums') . ' WHERE `parentid` IS NULL';
796		$albums = query($sql);
797		if ($albums) {
798			while ($album = db_fetch_assoc($albums)) {
799				$_zp_admin_album_list[$album['folder']] = 32767;
800			}
801			db_free_result($albums);
802		}
803	} else {
804		if ($_zp_current_admin_obj) {
805			$_zp_admin_album_list = array();
806			$objects = $_zp_current_admin_obj->getObjects();
807			foreach ($objects as $object) {
808				if ($object['type'] == 'album') {
809					$_zp_admin_album_list[$object['data']] = $object['edit'];
810				}
811			}
812		}
813	}
814	return array_keys($_zp_admin_album_list);
815}
816
817/**
818 * Returns a list of album names managed by $id
819 *
820 * @param string $type which kind of object
821 * @param int $id admin ID
822 * @param bool $rights set true for album sub-rights
823 * @return array
824 */
825function populateManagedObjectsList($type, $id, $rights = false) {
826	if ($id <= 0) {
827		return array();
828	}
829	$cv = array();
830	if (empty($type) || substr($type, 0, 5) == 'album') {
831		$sql = "SELECT " . prefix('albums') . ".`folder`," . prefix('albums') . ".`title`," . prefix('admin_to_object') . ".`edit` FROM " . prefix('albums') . ", " .
832						prefix('admin_to_object') . " WHERE " . prefix('admin_to_object') . ".adminid=" . $id .
833						" AND " . prefix('albums') . ".id=" . prefix('admin_to_object') . ".objectid AND " . prefix('admin_to_object') . ".type LIKE 'album%'";
834		$currentvalues = query($sql, false);
835		if ($currentvalues) {
836			while ($albumitem = db_fetch_assoc($currentvalues)) {
837				$folder = $albumitem['folder'];
838				$name = get_language_string($albumitem['title']);
839				if ($type && !$rights) {
840					$cv[$name] = $folder;
841				} else {
842					$cv[] = array('data' => $folder, 'name' => $name, 'type' => 'album', 'edit' => $albumitem['edit'] + 0);
843				}
844			}
845			db_free_result($currentvalues);
846		}
847	}
848	if (empty($type) || $type == 'pages') {
849		$sql = 'SELECT ' . prefix('pages') . '.`title`,' . prefix('pages') . '.`titlelink` FROM ' . prefix('pages') . ', ' .
850						prefix('admin_to_object') . " WHERE " . prefix('admin_to_object') . ".adminid=" . $id .
851						" AND " . prefix('pages') . ".id=" . prefix('admin_to_object') . ".objectid AND " . prefix('admin_to_object') . ".type='pages'";
852		$currentvalues = query($sql, false);
853		if ($currentvalues) {
854			while ($item = db_fetch_assoc($currentvalues)) {
855				if ($type) {
856					$cv[get_language_string($item['title'])] = $item['titlelink'];
857				} else {
858					$cv[] = array('data' => $item['titlelink'], 'name' => $item['title'], 'type' => 'pages');
859				}
860			}
861			db_free_result($currentvalues);
862		}
863	}
864	if (empty($type) || $type == 'news') {
865		$sql = 'SELECT ' . prefix('news_categories') . '.`titlelink`,' . prefix('news_categories') . '.`title` FROM ' . prefix('news_categories') . ', ' .
866						prefix('admin_to_object') . " WHERE " . prefix('admin_to_object') . ".adminid=" . $id .
867						" AND " . prefix('news_categories') . ".id=" . prefix('admin_to_object') . ".objectid AND " . prefix('admin_to_object') . ".type='news'";
868		$currentvalues = query($sql, false);
869		if ($currentvalues) {
870			while ($item = db_fetch_assoc($currentvalues)) {
871				if ($type) {
872					$cv[get_language_string($item['title'])] = $item['titlelink'];
873				} else {
874					$cv[] = array('data' => $item['titlelink'], 'name' => $item['title'], 'type' => 'news');
875				}
876			}
877			db_free_result($currentvalues);
878		}
879	}
880	return $cv;
881}
882
883/**
884 * Returns  an array of album ids whose parent is the folder
885 * @param string $albumfolder folder name if you want a album different >>from the current album
886 * @return array
887 */
888function getAllSubAlbumIDs($albumfolder = '') {
889	global $_zp_current_album;
890	if (empty($albumfolder)) {
891		if (isset($_zp_current_album)) {
892			$albumfolder = $_zp_current_album->getFileName();
893		} else {
894			return null;
895		}
896	}
897	$query = "SELECT `id`,`folder`, `show` FROM " . prefix('albums') . " WHERE `folder` LIKE " . db_quote(db_LIKE_escape($albumfolder) . '%');
898	$subIDs = query_full_array($query);
899	return $subIDs;
900}
901
902/**
903 * recovers search parameters from stored cookie, clears the cookie
904 *
905 * @param string $what the page type
906 * @param string $album Name of the album
907 * @param string $image Name of the image
908 */
909function handleSearchParms($what, $album = NULL, $image = NULL) {
910	global $_zp_current_search, $zp_request, $_zp_last_album, $_zp_current_album,
911	$_zp_current_zenpage_news, $_zp_current_zenpage_page, $_zp_gallery, $_zp_loggedin, $_zp_gallery_page;
912	$_zp_last_album = zp_getCookie('zpcms_search_lastalbum');
913	if (is_object($zp_request) && get_class($zp_request) == 'SearchEngine') { //	we are are on a search
914		zp_setCookie('zpcms_search_parent', 'searchresults');
915		return $zp_request->getAlbumList();
916	}
917	$params = zp_getCookie('zpcms_search_params');
918	if (!empty($params)) {
919		$searchparent = zp_getCookie('zpcms_search_parent');
920		$context = get_context();
921		$_zp_current_search = new SearchEngine();
922		$_zp_current_search->setSearchParams($params);
923		// check to see if we are still "in the search context"
924		if (!is_null($image)) {
925			$dynamic_album = $_zp_current_search->getDynamicAlbum();
926			if ($_zp_current_search->getImageIndex($album->name, $image->filename) !== false) {
927				if ($dynamic_album) {
928					$_zp_current_album = $dynamic_album;
929				}
930				$context = $context | ZP_SEARCH_LINKED | ZP_IMAGE_LINKED;
931			}
932		}
933		if (!is_null($album)) {
934			$albumname = $album->name;
935			zp_setCookie('zpcms_search_lastalbum', $albumname);
936			if ($_zp_gallery_page == 'album.php') {
937				$searchparent = 'searchresults_album'; // so we know we are in an album search result so any of its images that are also results don't throw us out of context
938			}
939			if (hasDynamicAlbumSuffix($albumname) && !is_dir(ALBUM_FOLDER_SERVERPATH . $albumname)) {
940				$albumname = stripSuffix($albumname); // strip off the suffix as it will not be reflected in the search path
941			}
942			//	see if the album is within the search context. NB for these purposes we need to look at all albums!
943			$save_logon = $_zp_loggedin;
944			$_zp_loggedin = $_zp_loggedin | VIEW_ALL_RIGHTS;
945			$search_album_list = $_zp_current_search->getAlbums(0);
946			$_zp_loggedin = $save_logon;
947			foreach ($search_album_list as $searchalbum) {
948				if (strpos($albumname, $searchalbum) !== false) {
949					if ($searchparent == 'searchresults_album') {
950						$context = $context | ZP_SEARCH_LINKED | ZP_ALBUM_LINKED;
951					} else {
952						$context = $context | ZP_SEARCH_LINKED | ZP_IMAGE_LINKED;
953					}
954					break;
955				}
956			}
957			zp_setCookie('zpcms_search_parent', $searchparent);
958		} else {
959			zp_clearCookie('zpcms_search_parent');
960			zp_clearCookie('zpcms_search_lastalbum');
961		}
962		if (!is_null($_zp_current_zenpage_page)) {
963			$pages = $_zp_current_search->getPages();
964			if (!empty($pages)) {
965				$tltlelink = $_zp_current_zenpage_page->getTitlelink();
966				foreach ($pages as $apage) {
967					if ($apage == $tltlelink) {
968						$context = $context | ZP_SEARCH_LINKED;
969						break;
970					}
971				}
972			}
973		}
974		if (!is_null($_zp_current_zenpage_news)) {
975			$news = $_zp_current_search->getArticles(0, NULL, true);
976			if (!empty($news)) {
977				$tltlelink = $_zp_current_zenpage_news->getTitlelink();
978				foreach ($news as $anews) {
979					if ($anews['titlelink'] == $tltlelink) {
980						$context = $context | ZP_SEARCH_LINKED;
981						break;
982					}
983				}
984			}
985		}
986		if (($context & ZP_SEARCH_LINKED)) {
987			set_context($context);
988		} else { // not an object in the current search path
989			$_zp_current_search = null;
990			rem_context(ZP_SEARCH);
991			if (!isset($_REQUEST['preserve_search_params'])) {
992				zp_clearCookie("zpcms_search_params");
993			}
994		}
995	}
996}
997
998/**
999 * Returns the number of album thumbs that go on a gallery page
1000 *
1001 * @return int
1002 */
1003function galleryAlbumsPerPage() {
1004	return max(1, getOption('albums_per_page'));
1005}
1006
1007/**
1008 * Returns the theme folder
1009 * If there is an album theme, loads the theme options.
1010 *
1011 * @param object $album album object if override desired
1012 *
1013 * @return string
1014 */
1015function setupTheme($album = NULL) {
1016	global $_zp_gallery, $_zp_current_album, $_zp_current_search, $_zp_themeroot;
1017	$albumtheme = '';
1018	if (is_null($album)) {
1019		if (in_context(ZP_SEARCH_LINKED)) {
1020			if (!$album = $_zp_current_search->getDynamicAlbum()) {
1021				$album = $_zp_current_album;
1022			}
1023		} else {
1024			$album = $_zp_current_album;
1025		}
1026	}
1027	$theme = $_zp_gallery->getCurrentTheme();
1028	$id = 0;
1029	if (!is_null($album)) {
1030		$parent = getUrAlbum($album);
1031		$albumtheme = $parent->getAlbumTheme();
1032		if (!empty($albumtheme)) {
1033			$theme = $albumtheme;
1034			$id = $parent->getID();
1035		}
1036	}
1037	$theme = zp_apply_filter('setupTheme', $theme);
1038	$_zp_gallery->setCurrentTheme($theme);
1039	$themeindex = getPlugin('index.php', $theme);
1040	if (empty($theme) || empty($themeindex)) {
1041		header('Last-Modified: ' . ZP_LAST_MODIFIED);
1042		header('Content-Type: text/html; charset=' . LOCAL_CHARSET);
1043		?>
1044		<!DOCTYPE html>
1045		<html>
1046			<head>
1047			</head>
1048			<body>
1049				<strong><?php printf(gettext('Zenphoto found no theme scripts. Please check the <em>%s</em> folder of your installation.'), THEMEFOLDER); ?></strong>
1050			</body>
1051		</html>
1052		<?php
1053		exitZP();
1054	} else {
1055		loadLocalOptions($id, $theme);
1056		$_zp_themeroot = WEBPATH . "/" . THEMEFOLDER . "/$theme";
1057	}
1058	return $theme;
1059}
1060
1061/**
1062 * Returns an array of unique tag names
1063 *
1064 * @param bool $checkaccess Set to true if you wish to exclude tags that are assigned to items (or are not assigned at all) the visitor is not allowed to see
1065 * Beware that this may cause overhead on large sites. Usage of the static_html_cache plugin is strongely recommended.
1066 * @return array
1067 */
1068function getAllTagsUnique($checkaccess = false) {
1069	global $_zp_unique_tags, $_zp_unique_tags_excluded;
1070	if (zp_loggedin(VIEW_ALL_RIGHTS)) {
1071		$checkaccess = false;
1072	}
1073	//need to cache all and filtered tags indiviually
1074	if ($checkaccess) {
1075		if (!is_null($_zp_unique_tags_excluded)) {
1076			return $_zp_unique_tags_excluded; // cache them.
1077		}
1078	} else {
1079		if (!is_null($_zp_unique_tags)) {
1080			return $_zp_unique_tags; // cache them.
1081		}
1082	}
1083	$all_unique_tags = array();
1084	$sql = "SELECT DISTINCT `name`, `id` FROM " . prefix('tags') . ' ORDER BY `name`';
1085	$unique_tags = query($sql);
1086	if ($unique_tags) {
1087		while ($tagrow = db_fetch_assoc($unique_tags)) {
1088			if ($checkaccess) {
1089				if (getTagCountByAccess($tagrow) != 0) {
1090					$all_unique_tags[] = $tagrow['name'];
1091				}
1092			} else {
1093				$all_unique_tags[] = $tagrow['name'];
1094			}
1095		}
1096		db_free_result($unique_tags);
1097	}
1098	if ($checkaccess) {
1099		$_zp_unique_tags_excluded = $all_unique_tags;
1100		return $_zp_unique_tags_excluded;
1101	} else {
1102		$_zp_unique_tags = $all_unique_tags;
1103		return $_zp_unique_tags;
1104	}
1105}
1106
1107/**
1108 * Returns an array indexed by 'tag' with the element value the count of the tag
1109 *
1110 * @param bool $exclude_unassigned Set to true if you wish to exclude tags that are not assigne to any item
1111 * @param bool $checkaccess Set to true if you wish to exclude tags that are assigned to items (or are not assigned at all) the visitor is not allowed to see
1112 * If set to true it overrides the $exclude_unassigned parameter.
1113 * Beware that this may cause overhead on large sites. Usage of the static_html_cache plugin is strongely recommended.
1114 * @return array
1115 */
1116function getAllTagsCount($exclude_unassigned = false, $checkaccess = false) {
1117	global $_zp_count_tags;
1118	if (!is_null($_zp_count_tags)) {
1119		return $_zp_count_tags;
1120	}
1121	if (zp_loggedin(VIEW_ALL_RIGHTS)) {
1122		$exclude_unassigned = false;
1123		$checkaccess = false;
1124	}
1125	$_zp_count_tags = array();
1126	$sql = "SELECT DISTINCT tags.name, tags.id, (SELECT COUNT(*) FROM " . prefix('obj_to_tag') . " as object WHERE object.tagid = tags.id) AS count FROM " . prefix('tags') . " as tags ORDER BY `name`";
1127	$tagresult = query($sql);
1128	if ($tagresult) {
1129		while ($tag = db_fetch_assoc($tagresult)) {
1130			if ($checkaccess) {
1131				$count = getTagCountByAccess($tag);
1132				if ($count != 0) {
1133					$_zp_count_tags[$tag['name']] = $count;
1134				}
1135			} else {
1136				if ($exclude_unassigned) {
1137					if ($tag['count'] != 0) {
1138						$_zp_count_tags[$tag['name']] = $tag['count'];
1139					}
1140				} else {
1141					$_zp_count_tags[$tag['name']] = $tag['count'];
1142				}
1143			}
1144		}
1145		db_free_result($tagresult);
1146	}
1147	return $_zp_count_tags;
1148}
1149
1150/**
1151 * Checks if a tag is assigned at all and if it can be viewed by the current visitor and returns the corrected count
1152 * Helper function used optionally within getAllTagsCount() and getAllTagsUnique()
1153 *
1154 * @global obj $_zp_zenpage
1155 * @param array $tag Array representing a tag containing at least its name and id
1156 * @return int
1157 */
1158function getTagCountByAccess($tag) {
1159	global $_zp_zenpage, $_zp_object_to_tags;
1160	if (array_key_exists('count', $tag) && $tag['count'] == 0) {
1161		return $tag['count'];
1162	}
1163	$hidealbums = getNotViewableAlbums();
1164	$hideimages = getNotViewableImages();
1165	$hidenews = array();
1166	$hidepages = array();
1167	if (extensionEnabled('Zenpage')) {
1168		$hidenews = $_zp_zenpage->getNotViewableNews();
1169		$hidepages = $_zp_zenpage->getNotViewablePages();
1170	}
1171	//skip checks if there are no unviewable items at all
1172	if (empty($hidealbums) && empty($hideimages) && empty($hidenews) && empty($hidepages)) {
1173		if (array_key_exists('count', $tag)) {
1174			return $tag['count'];
1175		}
1176		return 0;
1177	}
1178	if (is_null($_zp_object_to_tags)) {
1179		$sql = "SELECT tagid, type, objectid FROM " . prefix('obj_to_tag') . " ORDER BY tagid";
1180		$_zp_object_to_tags = query_full_array($sql);
1181	}
1182	$count = '';
1183	if ($_zp_object_to_tags) {
1184		foreach ($_zp_object_to_tags as $tagcheck) {
1185			if ($tagcheck['tagid'] == $tag['id']) {
1186				switch ($tagcheck['type']) {
1187					case 'albums':
1188						if (!in_array($tagcheck['objectid'], $hidealbums)) {
1189							$count++;
1190						}
1191						break;
1192					case 'images':
1193						if (!in_array($tagcheck['objectid'], $hideimages)) {
1194							$count++;
1195						}
1196						break;
1197					case 'news':
1198						if (ZP_NEWS_ENABLED) {
1199							if (!in_array($tagcheck['objectid'], $hidenews)) {
1200								$count++;
1201							}
1202						}
1203						break;
1204					case 'pages':
1205						if (ZP_PAGES_ENABLED) {
1206							if (!in_array($tagcheck['objectid'], $hidepages)) {
1207								$count++;
1208							}
1209						}
1210						break;
1211				}
1212			}
1213		}
1214	}
1215	if (empty($count)) {
1216		$count = 0;
1217	}
1218	return $count;
1219}
1220
1221/**
1222 * Stores tags for an object
1223 *
1224 * @param array $tags the tag values
1225 * @param int $id the record id of the album/image
1226 * @param string $tbl database table of the object
1227 */
1228function storeTags($tags, $id, $tbl) {
1229	if ($id) {
1230		$tagsLC = array();
1231		foreach ($tags as $key => $tag) {
1232			$tag = trim($tag);
1233			if (!empty($tag)) {
1234				$lc_tag = mb_strtolower($tag);
1235				if (!in_array($lc_tag, $tagsLC)) {
1236					$tagsLC[$tag] = $lc_tag;
1237				}
1238			}
1239		}
1240		$sql = "SELECT `id`, `tagid` from " . prefix('obj_to_tag') . " WHERE `objectid`='" . $id . "' AND `type`='" . $tbl . "'";
1241		$result = query($sql);
1242		$existing = array();
1243		if ($result) {
1244			while ($row = db_fetch_assoc($result)) {
1245				$dbtag = query_single_row("SELECT `name` FROM " . prefix('tags') . " WHERE `id`='" . $row['tagid'] . "'");
1246				$existingLC = mb_strtolower($dbtag['name']);
1247				if (in_array($existingLC, $tagsLC)) { // tag already set no action needed
1248					$existing[] = $existingLC;
1249				} else { // tag no longer set, remove it
1250					query("DELETE FROM " . prefix('obj_to_tag') . " WHERE `id`='" . $row['id'] . "'");
1251				}
1252			}
1253			db_free_result($result);
1254		}
1255		$tags = array_diff($tagsLC, $existing); // new tags for the object
1256		foreach ($tags as $key => $tag) {
1257			$dbtag = query_single_row("SELECT `id` FROM " . prefix('tags') . " WHERE `name`=" . db_quote($key));
1258			if (!is_array($dbtag)) { // tag does not exist
1259				query("INSERT INTO " . prefix('tags') . " (name) VALUES (" . db_quote($key) . ")", false);
1260				$dbtag = array('id' => db_insert_id());
1261			}
1262			query("INSERT INTO " . prefix('obj_to_tag') . "(`objectid`, `tagid`, `type`) VALUES (" . $id . "," . $dbtag['id'] . ",'" . $tbl . "')");
1263		}
1264	}
1265}
1266
1267/**
1268 * Retrieves the tags for an object
1269 * Returns them in an array
1270 *
1271 * @param int $id the record id of the album/image
1272 * @param string $tbl 'albums' or 'images', etc.
1273 * @return unknown
1274 */
1275function readTags($id, $tbl) {
1276	$tags = array();
1277	$result = query("SELECT `tagid` FROM " . prefix('obj_to_tag') . " WHERE `type`='" . $tbl . "' AND `objectid`='" . $id . "'");
1278	if ($result) {
1279		while ($row = db_fetch_assoc($result)) {
1280			$dbtag = query_single_row("SELECT `name` FROM" . prefix('tags') . " WHERE `id`='" . $row['tagid'] . "'");
1281			if ($dbtag) {
1282				$tags[] = $dbtag['name'];
1283			}
1284		}
1285		db_free_result($result);
1286	}
1287	sortArray($tags);
1288	return $tags;
1289}
1290
1291/**
1292 * Creates the body of a select list
1293 *
1294 * @param array $currentValue list of items to be flagged as checked
1295 * @param array $list the elements of the select list
1296 * @param bool $descending set true for a ascending order sort. Set to null to keep the array as it is passed.
1297 * @param bool $localize set true if the keys as description should be listed instead of the plain values
1298 */
1299function generateListFromArray($currentValue, $list, $descending, $localize) {
1300	if ($localize) {
1301		$list = array_flip($list);
1302		if (!is_null($descending)) {
1303			if ($descending) {
1304				sortArray($list, true);
1305			} else {
1306				sortArray($list);
1307			}
1308		}
1309		$list = array_flip($list);
1310	} else {
1311		if (!is_null($descending)) {
1312			if ($descending) {
1313				sortArray($list, true);
1314			} else {
1315				sortArray($list);
1316			}
1317		}
1318	}
1319
1320	foreach ($list as $key => $item) {
1321		echo '<option value="' . html_encode($item) . '"';
1322		if (in_array($item, $currentValue)) {
1323			echo ' selected="selected"';
1324		}
1325		if ($localize) {
1326			$display = $key;
1327		} else {
1328			$display = $item;
1329		}
1330		echo '>' . $display . "</option>" . "\n";
1331	}
1332}
1333
1334/**
1335 * Generates a selection list from files found on disk
1336 *
1337 * @param strig $currentValue the current value of the selector
1338 * @param string $root directory path to search
1339 * @param string $suffix suffix to select for
1340 * @param bool $descending set true to get a reverse order sort. Set to null to keep the array as it is passed.
1341 */
1342function generateListFromFiles($currentValue, $root, $suffix, $descending = false) {
1343	if (is_dir($root)) {
1344		$curdir = getcwd();
1345		chdir($root);
1346		$filelist = safe_glob('*' . $suffix);
1347		$list = array();
1348		foreach ($filelist as $file) {
1349			$file = str_replace($suffix, '', $file);
1350			$list[] = filesystemToInternal($file);
1351		}
1352		generateListFromArray(array($currentValue), $list, $descending, false);
1353		chdir($curdir);
1354	}
1355}
1356
1357/**
1358 * Helper to generate attributename="attributevalue" for HTML elements based on an array
1359 * consisting of key => value pairs
1360 *
1361 * Returns a string with with prependend space to directly use with an HTML element.
1362 *
1363 * Note:
1364 * There is no check if these attributes are valid. Also values are not html_encoded  as that could
1365 * break for example JS event handlers. Do this in your attribute definition array as needed.
1366 * Attributes with an empty value are skipped except the alt attribute or known boolean attributes (see in function definition)
1367 *
1368 * @since ZenphotoCMS 1.5.8
1369 * @param array $attributes key => value pairs of element attribute name and value. e.g. array('class' => 'someclass', 'id' => 'someid');
1370 * @param array $exclude Names of attributes to exclude (in case already set otherwise)
1371 * @return string
1372 */
1373function generateAttributesFromArray($attributes = array(), $exclude = array()) {
1374	$boolean_attr = array(
1375			'allowfullscreen',
1376			'allowpaymentrequest',
1377			'async',
1378			'autofocus',
1379			'autoplay',
1380			'checked',
1381			'controls',
1382			'default',
1383			'disabled',
1384			'formnovalidate',
1385			'hidden',
1386			'ismap',
1387			'itemscope',
1388			'loop',
1389			'multiple',
1390			'muted',
1391			'nomodule',
1392			'novalidate',
1393			'open',
1394			'playsinline',
1395			'readonly',
1396			'required',
1397			'reversed',
1398			'selected',
1399			'truespeed'
1400	);
1401	$attr = '';
1402	if (!empty($attributes) && is_array($attributes)) {
1403		foreach ($attributes as $key => $val) {
1404			if (!in_array($key, $exclude)) {
1405				if (empty($val)) {
1406					if (in_array($key, $boolean_attr)) {
1407						$attr .= ' ' . $key;
1408					} else if ($key == 'alt') {
1409						$attr .= ' ' . $key . '=""';
1410					}
1411				} else {
1412					$attr .= ' ' . $key . '="' . $val . '"';
1413				}
1414			}
1415		}
1416	}
1417	return $attr;
1418}
1419
1420/**
1421 * @param string $url The link URL
1422 * @param string $text The text to go with the link
1423 * @param string $title Text for the title tag
1424 * @param string $class optional class
1425 * @param string $id optional id
1426 * @param array  $extra_attr Additional attributes as array of key => value pairs
1427 */
1428function getLinkHTML($url, $text, $title = NULL, $class = NULL, $id = NULL, $extra_attr = array()) {
1429	$attr = array(
1430			'href' => html_encode($url),
1431			'title' => html_encode(getBare($title)),
1432			'class' => $class,
1433			'id' => $id
1434	);
1435	$attr_final = array_merge($attr, $extra_attr);
1436	$attributes = generateAttributesFromArray($attr_final);
1437	return '<a' . $attributes . '>' . html_encode($text) . '</a>';
1438}
1439
1440/**
1441 * General link printing function
1442 * @param string $url The link URL
1443 * @param string $text The text to go with the link
1444 * @param string $title Text for the title tag
1445 * @param string $class optional class
1446 * @param string $id optional id
1447 */
1448function printLinkHTML($url, $text, $title = NULL, $class = NULL, $id = NULL) {
1449	echo getLinkHTML($url, $text, $title, $class, $id);
1450}
1451
1452/**
1453 * shuffles an array maintaining the keys
1454 *
1455 * @param array $array
1456 * @return boolean
1457 */
1458function shuffle_assoc(&$array) {
1459	$keys = array_keys($array);
1460	shuffle($keys);
1461	foreach ($keys as $key) {
1462		$new[$key] = $array[$key];
1463	}
1464	$array = $new;
1465	return true;
1466}
1467
1468/**
1469 * sorts the found albums (images) by the required key(s)
1470 *
1471 * NB: this sort is sensitive to the key(s) chosen and makes
1472 * the appropriate sorts based on same. Some multi-key sorts
1473 * will not make any sense and will give unexpected results.
1474 * Most notably any that contain the keys "title" or "desc"
1475 * as these require multi-lingual sorts.
1476 *
1477 * @param array $results
1478 * @param string $sortkey
1479 * @param string $order
1480 */
1481function sortByKey($results, $sortkey, $order) {
1482	$sortkey = str_replace('`', '', $sortkey);
1483	switch ($sortkey) {
1484		case 'title':
1485		case 'desc':
1486			return sortByMultilingual($results, $sortkey, $order);
1487		case 'RAND()':
1488			shuffle($results);
1489			return $results;
1490		default:
1491			if (preg_match('`[\/\(\)\*\+\-!\^\%\<\>\=\&\|]`', $sortkey)) {
1492				return $results; //	We cannot deal with expressions
1493			}
1494	}
1495	$indicies = explode(',', $sortkey);
1496	foreach ($indicies as $key => $index) {
1497		$indicies[$key] = trim($index);
1498	}
1499	$results = sortMultiArray($results, $indicies, $order, true, false, true);
1500	return $results;
1501}
1502
1503/**
1504 * multidimensional array column sort
1505 *
1506 * If the system's PHP has the native intl extension and its Collator class available
1507 * the sorting is locale aware (true natural order) and always case sensitive if $natsort is set to true
1508 *
1509 * @param array $array The multidimensional array to be sorted
1510 * @param mixed $index Which key(s) should be sorted by
1511 * @param string $descending true for descending sortorder
1512 * @param bool $natsort If natural order should be used. If available sorting will be locale aware.
1513 * @param bool $case_sensitive If the sort should be case sensitive. Note if $natsort is true and locale aware sorting is available sorting is always case sensitive
1514 * @param bool $preservekeys Default false,
1515 * @param array $remove_criteria Array of indices to remove.
1516 * @return array
1517 *
1518 * @author redoc (http://codingforums.com/showthread.php?t=71904)
1519 */
1520function sortMultiArray($array, $index, $descending = false, $natsort = true, $case_sensitive = false, $preservekeys = false, $remove_criteria = array()) {
1521	if (is_array($array) && count($array) > 0) {
1522		if (is_array($index)) {
1523			$indicies = $index;
1524		} else {
1525			$indicies = array($index);
1526		}
1527		if ($descending) {
1528			$separator = '~~';
1529		} else {
1530			$separator = '  ';
1531		}
1532		foreach ($array as $key => $row) {
1533			$temp[$key] = '';
1534			foreach ($indicies as $index) {
1535				if (is_array($row) && array_key_exists($index, $row)) {
1536					$temp[$key] .= get_language_string($row[$index]) . $separator;
1537					if (in_array($index, $remove_criteria)) {
1538						unset($array[$key][$index]);
1539					}
1540				}
1541			}
1542			$temp[$key] .= $key;
1543		}
1544		sortArray($temp, $descending, $natsort, $case_sensitive);
1545		foreach (array_keys($temp) as $key) {
1546			if (!$preservekeys && is_numeric($key)) {
1547				$sorted[] = $array[$key];
1548			} else {
1549				$sorted[$key] = $array[$key];
1550			}
1551		}
1552		return $sorted;
1553	}
1554	return $array;
1555}
1556
1557/**
1558 * General one dimensional array sorting function. Key/value associations are preserved.
1559 *
1560 * If the system's PHP has the native intl extension and its Collator class available and $natsort is set to true
1561 * the sorting is locale sensitive (true natural order).
1562 *
1563 * The function follows native PHP array sorting functions (natcasesort() etc.) and uses the array by reference and returns true or false on success or failure.
1564 *
1565 * @since ZenphotoCMS 1.5.8
1566 *
1567 * @param array $array The array to sort. The array is passed by reference
1568 * @param string  $descending true for descending sorts (default false)
1569 * @param bool $natsort If natural order should be used (default true). If available sorting will be locale sensitive.
1570 * @param bool $case_sensitive If the sort should be case sensitive (default false). Note if $natsort is true and locale aware sorting is available sorting is always case sensitive
1571 * @return boolean
1572 */
1573function sortArray(&$array, $descending = false, $natsort = true, $case_sensitive = false) {
1574	$success = false;
1575	if (is_array($array) && count($array) > 0) {
1576		if ($natsort) {
1577			if (class_exists('collator')) {
1578				$locale = getUserLocale();
1579				$collator = new Collator($locale);
1580				if ($case_sensitive) {
1581					$collator->setAttribute(Collator::CASE_FIRST, Collator::UPPER_FIRST);
1582				}
1583				$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
1584				$success = $collator->asort($array, Collator::SORT_STRING);
1585			} else {
1586				if ($case_sensitive) {
1587					$success = natsort($array);
1588				} else {
1589					$success = natcasesort($array);
1590				}
1591			}
1592			if ($descending) {
1593				$array = array_reverse($array, true);
1594			}
1595		} else {
1596			if ($descending) {
1597				$success = arsort($array);
1598			} else {
1599				$success = asort($array);
1600			}
1601		}
1602	}
1603	return $success;
1604}
1605
1606/**
1607 * Returns a list of album IDs that the current viewer is not allowed to see
1608 *
1609 * @return array
1610 */
1611function getNotViewableAlbums() {
1612	global $_zp_not_viewable_album_list;
1613	if (zp_loggedin(ADMIN_RIGHTS | MANAGE_ALL_ALBUM_RIGHTS))
1614		return array(); //admins can see all
1615	if (is_null($_zp_not_viewable_album_list)) {
1616		$sql = 'SELECT `folder`, `id`, `password`, `show` FROM ' . prefix('albums') . ' WHERE `show`=0 OR `password`!=""';
1617		$result = query($sql);
1618		if ($result) {
1619			$_zp_not_viewable_album_list = array();
1620			while ($row = db_fetch_assoc($result)) {
1621				if (checkAlbumPassword($row['folder'])) {
1622					$album = newAlbum($row['folder']);
1623					if (!($row['show'] || $album->isMyItem(LIST_RIGHTS))) {
1624						$_zp_not_viewable_album_list[] = $row['id'];
1625					}
1626				} else {
1627					$_zp_not_viewable_album_list[] = $row['id'];
1628				}
1629			}
1630			db_free_result($result);
1631		}
1632	}
1633	return $_zp_not_viewable_album_list;
1634}
1635
1636/**
1637 * Returns a list of image IDs that the current viewer is not allowed to see
1638 *
1639 * @return array
1640 */
1641function getNotViewableImages() {
1642	global $_zp_not_viewable_image_list;
1643	if (zp_loggedin(ADMIN_RIGHTS | MANAGE_ALL_ALBUM_RIGHTS)) {
1644		return array(); //admins can see all
1645	}
1646	$hidealbums = getNotViewableAlbums();
1647	$where = '';
1648	if (!is_null($hidealbums)) {
1649		$where = ' OR `albumid` in (' . implode(',', $hidealbums) . ')';
1650	}
1651	if (is_null($_zp_not_viewable_image_list)) {
1652		$sql = 'SELECT DISTINCT `id` FROM ' . prefix('images') . ' WHERE `show` = 0' . $where;
1653		$result = query($sql);
1654		if ($result) {
1655			$_zp_not_viewable_image_list = array();
1656			while ($row = db_fetch_assoc($result)) {
1657				$_zp_not_viewable_image_list[] = $row['id'];
1658			}
1659		}
1660	}
1661	return $_zp_not_viewable_image_list;
1662}
1663
1664/**
1665 * Checks to see if a URL is valid
1666 *
1667 * @param string $url the URL being checked
1668 * @return bool
1669 */
1670function isValidURL($url) {
1671	if (filter_var($url, FILTER_VALIDATE_URL)) {
1672		return true;
1673	}
1674	/*
1675	 * Above does not allow the newer UTF8 internation domain names.
1676	 * @see Alexander Terehov https://github.com/terales/php-url-validation-example
1677	 */
1678	if (parse_url($url, PHP_URL_SCHEME) && parse_url($url, PHP_URL_HOST)) {
1679		return true;
1680	}
1681	return false;
1682}
1683
1684/**
1685 * pattern match function Works with characters with diacritical marks where the PHP one does not.
1686 *
1687 * @param string $pattern pattern
1688 * @param string $string haystack
1689 * @return bool
1690 */
1691function safe_fnmatch($pattern, $string) {
1692	return @preg_match('/^' . strtr(addcslashes($pattern, '\\.+^$(){}=!<>|'), array('*' => '.*', '?' => '.?')) . '$/i', $string);
1693}
1694
1695/**
1696 * Returns true if the mail address passed is valid.
1697 * It uses PHP's internal `filter_var` functions to validate the syntax but not the existence.
1698 *
1699 * @since ZenphotoCMS 1.5.2
1700 *
1701 * @param string $email An email address
1702 * @return boolean
1703 */
1704function isValidEmail($email) {
1705	if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
1706		return true;
1707	}
1708	return false;
1709}
1710
1711/**
1712 * returns a list of comment record 'types' for "images"
1713 * @param string $quote quotation mark to use
1714 *
1715 * @return string
1716 */
1717function zp_image_types($quote) {
1718	global $_zp_extra_filetypes;
1719	$typelist = $quote . 'images' . $quote . ',' . $quote . '_images' . $quote . ',';
1720	$types = array_unique($_zp_extra_filetypes);
1721	foreach ($types as $type) {
1722		$typelist .= $quote . strtolower($type) . 's' . $quote . ',';
1723	}
1724	return substr($typelist, 0, -1);
1725}
1726
1727/**
1728
1729 * Returns video argument of the current Image.
1730 *
1731 * @param object $image optional image object
1732 * @return bool
1733 */
1734function isImageVideo($image = NULL) {
1735	if (is_null($image)) {
1736		if (!in_context(ZP_IMAGE))
1737			return false;
1738		global $_zp_current_image;
1739		$image = $_zp_current_image;
1740	}
1741	return strtolower(get_class($image)) == 'video';
1742}
1743
1744/**
1745 * Returns true if the image is a standard photo type
1746 *
1747 * @param object $image optional image object
1748 * @return bool
1749 */
1750function isImagePhoto($image = NULL) {
1751	if (is_null($image)) {
1752		if (!in_context(ZP_IMAGE))
1753			return false;
1754		global $_zp_current_image;
1755		$image = $_zp_current_image;
1756	}
1757	$class = strtolower(get_class($image));
1758	return $class == 'image' || $class == 'transientimage';
1759}
1760
1761/**
1762 * Copies a directory recursively
1763 * @param string $srcdir the source directory.
1764 * @param string $dstdir the destination directory.
1765 * @return the total number of files copied.
1766 */
1767function dircopy($srcdir, $dstdir) {
1768	$num = 0;
1769	if (!is_dir($dstdir))
1770		mkdir($dstdir);
1771	if ($curdir = opendir($srcdir)) {
1772		while ($file = readdir($curdir)) {
1773			if ($file != '.' && $file != '..') {
1774				$srcfile = $srcdir . '/' . $file;
1775				$dstfile = $dstdir . '/' . $file;
1776				if (is_file($srcfile)) {
1777					if (is_file($dstfile))
1778						$ow = filemtime($srcfile) - filemtime($dstfile);
1779					else
1780						$ow = 1;
1781					if ($ow > 0) {
1782						if (copy($srcfile, $dstfile)) {
1783							touch($dstfile, filemtime($srcfile));
1784							$num++;
1785						}
1786					}
1787				} else if (is_dir($srcfile)) {
1788					$num += dircopy($srcfile, $dstfile);
1789				}
1790			}
1791		}
1792		closedir($curdir);
1793	}
1794	return $num;
1795}
1796
1797/**
1798 * Returns a byte size from a size value (eg: 100M).
1799 *
1800 * @param int $bytes
1801 * @return string
1802 */
1803function byteConvert($bytes) {
1804	if ($bytes <= 0)
1805		return gettext('0 Bytes');
1806	$convention = 1024; //[1000->10^x|1024->2^x]
1807	$s = array('Bytes', 'kB', 'mB', 'GB', 'TB', 'PB', 'EB', 'ZB');
1808	$e = floor(log($bytes, $convention));
1809	return round($bytes / pow($convention, $e), 2) . ' ' . $s[$e];
1810}
1811
1812/**
1813 * Converts a datetime to connoical form
1814 *
1815 * @param string $datetime input date/time string
1816 * @param bool $raw set to true to return the timestamp otherwise you get a string
1817 * @return mixed
1818 */
1819function dateTimeConvert($datetime, $raw = false) {
1820	// Convert 'yyyy:mm:dd hh:mm:ss' to 'yyyy-mm-dd hh:mm:ss' for Windows' strtotime compatibility
1821	$datetime = preg_replace('/(\d{4}):(\d{2}):(\d{2})/', ' \1-\2-\3', $datetime);
1822	$time = strtotime($datetime);
1823	if ($time == -1 || $time === false)
1824		return false;
1825	if ($raw)
1826		return $time;
1827	return date('Y-m-d H:i:s', $time);
1828}
1829
1830/* * * Context Manipulation Functions ****** */
1831/* * *************************************** */
1832
1833/* Contexts are simply constants that tell us what variables are available to us
1834 * at any given time. They should be set and unset with those variables.
1835 */
1836
1837function get_context() {
1838	global $_zp_current_context;
1839	return $_zp_current_context;
1840}
1841
1842function set_context($context) {
1843	global $_zp_current_context;
1844	$_zp_current_context = $context;
1845}
1846
1847function in_context($context) {
1848	return get_context() & $context;
1849}
1850
1851function add_context($context) {
1852	set_context(get_context() | $context);
1853}
1854
1855function rem_context($context) {
1856	global $_zp_current_context;
1857	set_context(get_context() & ~$context);
1858}
1859
1860// Use save and restore rather than add/remove when modifying contexts.
1861function save_context() {
1862	global $_zp_current_context, $_zp_current_context_stack;
1863	array_push($_zp_current_context_stack, $_zp_current_context);
1864}
1865
1866function restore_context() {
1867	global $_zp_current_context, $_zp_current_context_stack;
1868	$_zp_current_context = array_pop($_zp_current_context_stack);
1869}
1870
1871/**
1872 * checks password posting
1873 *
1874 * @param string $authType override of athorization type
1875 */
1876function zp_handle_password($authType = NULL, $check_auth = NULL, $check_user = NULL) {
1877	global $_zp_loggedin, $_zp_login_error, $_zp_current_album, $_zp_current_zenpage_page, $_zp_current_category, $_zp_current_zenpage_news, $_zp_gallery;
1878	if (empty($authType)) { // not supplied by caller
1879		$check_auth = '';
1880		if (isset($_GET['z']) && @$_GET['p'] == 'full-image' || isset($_GET['p']) && $_GET['p'] == '*full-image') {
1881			$authType = 'zpcms_auth_image';
1882			$check_auth = getOption('protected_image_password');
1883			$check_user = getOption('protected_image_user');
1884		} else if (in_context(ZP_SEARCH)) { // search page
1885			$authType = 'zpcms_auth_search';
1886			$check_auth = getOption('search_password');
1887			$check_user = getOption('search_user');
1888		} else if (in_context(ZP_ALBUM)) { // album page
1889			$authType = "zpcms_auth_album_" . $_zp_current_album->getID();
1890			$check_auth = $_zp_current_album->getPassword();
1891			$check_user = $_zp_current_album->getUser();
1892			if (empty($check_auth)) {
1893				$parent = $_zp_current_album->getParent();
1894				while (!is_null($parent)) {
1895					$check_auth = $parent->getPassword();
1896					$check_user = $parent->getUser();
1897					$authType = "zpcms_auth_album_" . $parent->getID();
1898					if (!empty($check_auth)) {
1899						break;
1900					}
1901					$parent = $parent->getParent();
1902				}
1903			}
1904		} else if (in_context(ZP_ZENPAGE_PAGE)) {
1905			$authType = "zpcms_auth_page_" . $_zp_current_zenpage_page->getID();
1906			$check_auth = $_zp_current_zenpage_page->getPassword();
1907			$check_user = $_zp_current_zenpage_page->getUser();
1908			if (empty($check_auth)) {
1909				$pageobj = $_zp_current_zenpage_page;
1910				while (empty($check_auth)) {
1911					$parentID = $pageobj->getParentID();
1912					if ($parentID == 0)
1913						break;
1914					$sql = 'SELECT `titlelink` FROM ' . prefix('pages') . ' WHERE `id`=' . $parentID;
1915					$result = query_single_row($sql);
1916					$pageobj = new ZenpagePage($result['titlelink']);
1917					$authType = "zpcms_auth_page_" . $pageobj->getID();
1918					$check_auth = $pageobj->getPassword();
1919					$check_user = $pageobj->getUser();
1920				}
1921			}
1922		} else if (in_context(ZP_ZENPAGE_NEWS_CATEGORY) || in_context(ZP_ZENPAGE_NEWS_ARTICLE)) {
1923			$check_auth_user = array();
1924			if (in_context(ZP_ZENPAGE_NEWS_CATEGORY)) {
1925				$checkcats = array($_zp_current_category);
1926			} else if (in_context(ZP_ZENPAGE_NEWS_ARTICLE)) {
1927				$checkcats = array();
1928				$cats = $_zp_current_zenpage_news->getCategories();
1929				foreach ($cats as $cat) {
1930					$checkcats[] = new ZenpageCategory($cat['titlelink']);
1931				}
1932			}
1933			if (!empty($checkcats)) {
1934				foreach ($checkcats as $obj) {
1935					$authType = "zpcms_auth_category_" . $obj->getID();
1936					$check_auth = $obj->getPassword();
1937					$check_user = $obj->getUser();
1938					if (empty($check_auth)) {
1939						$catobj = $obj;
1940						while (empty($check_auth)) {
1941							$parentID = $catobj->getParentID();
1942							if ($parentID == 0)
1943								break;
1944							$sql = 'SELECT `titlelink` FROM ' . prefix('news_categories') . ' WHERE `id`=' . $parentID;
1945							$result = query_single_row($sql);
1946							$catobj = new ZenpageCategory($result['titlelink']);
1947							$authType = "zpcms_auth_category_" . $catobj->getID();
1948							$check_auth = $catobj->getPassword();
1949							$check_user = $catobj->getUser();
1950						}
1951					}
1952					if (!empty($check_auth)) {
1953						//collect passwords from all categories
1954						$check_auth_user[] = array(
1955								'authtype' => $authType,
1956								'check_auth' => $check_auth,
1957								'check_user' => $check_user
1958						);
1959					}
1960				}
1961			}
1962		}
1963		if (empty($check_auth)) { // anything else is controlled by the gallery credentials
1964			$authType = 'zpcms_auth_gallery';
1965			$check_auth = $_zp_gallery->getPassword();
1966			$check_user = $_zp_gallery->getUser();
1967		}
1968	}
1969	if (in_context(ZP_ZENPAGE_NEWS_ARTICLE)) {
1970		//check every category with password individually
1971		foreach ($check_auth_user as $check) {
1972			zp_handle_password_single($check['authtype'], $check['check_auth'], $check['check_user']);
1973		}
1974	} else {
1975		zp_handle_password_single($authType, $check_auth, $check_user);
1976	}
1977}
1978
1979/**
1980 * Handles a passwort
1981 *
1982 * @param string $authType override of authorization type
1983 * @param string $check_auth Password
1984 * @param string $check_user User
1985 * @return bool
1986 */
1987function zp_handle_password_single($authType = NULL, $check_auth = NULL, $check_user = NULL) {
1988	// Handle the login form.
1989	if (DEBUG_LOGIN)
1990		debugLog("zp_handle_password: \$authType=$authType; \$check_auth=$check_auth; \$check_user=$check_user; ");
1991
1992	if (isset($_POST['password']) && isset($_POST['pass'])) { // process login form
1993		if (isset($_POST['user'])) {
1994			$post_user = sanitize($_POST['user']);
1995		} else {
1996			$post_user = '';
1997		}
1998		$post_pass = $_POST['pass']; // We should not sanitize the password
1999
2000		foreach (Zenphoto_Authority::$hashList as $hash => $hi) {
2001			$auth = Zenphoto_Authority::passwordHash($post_user, $post_pass, $hi);
2002			$success = ($auth == $check_auth) && $post_user == $check_user;
2003			if (DEBUG_LOGIN)
2004				debugLog("zp_handle_password($success): \$post_user=$post_user; \$post_pass=$post_pass; \$check_auth=$check_auth; \$auth=$auth; \$hash=$hash;");
2005			if ($success) {
2006				break;
2007			}
2008		}
2009		$success = zp_apply_filter('guest_login_attempt', $success, $post_user, $post_pass, $authType);
2010		if ($success) {
2011			// Correct auth info. Set the cookie.
2012			if (DEBUG_LOGIN)
2013				debugLog("zp_handle_password: valid credentials");
2014			zp_setCookie($authType, $auth);
2015			if (isset($_POST['redirect'])) {
2016				$redirect_to = sanitizeRedirect($_POST['redirect']);
2017				if (!empty($redirect_to)) {
2018					redirectURL($redirect_to);
2019				}
2020			}
2021		} else {
2022			// Clear the cookie, just in case
2023			if (DEBUG_LOGIN)
2024				debugLog("zp_handle_password: invalid credentials");
2025			zp_clearCookie($authType);
2026			$_zp_login_error = true;
2027		}
2028		return;
2029	}
2030	if (empty($check_auth)) { //no password on record or admin logged in
2031		return;
2032	}
2033	if (($saved_auth = zp_getCookie($authType)) != '') {
2034		if ($saved_auth == $check_auth) {
2035			if (DEBUG_LOGIN)
2036				debugLog("zp_handle_password: valid cookie");
2037			return;
2038		} else {
2039			// Clear the cookie
2040			if (DEBUG_LOGIN)
2041				debugLog("zp_handle_password: invalid cookie");
2042			zp_clearCookie($authType);
2043		}
2044	}
2045}
2046
2047/**
2048 *
2049 * Gets an option directly from the database.
2050 * @param string $key
2051 */
2052function getOptionFromDB($key) {
2053	$sql = "SELECT `value` FROM " . prefix('options') . " WHERE `name`=" . db_quote($key) . " AND `ownerid`=0";
2054	$optionlist = query_single_row($sql, false);
2055	return @$optionlist['value'];
2056}
2057
2058/**
2059 * Set options local to theme and/or album
2060 *
2061 * @param string $key
2062 * @param string $value
2063 * @param object $album
2064 * @param string $theme default theme
2065 * @param bool $default set to true for setting default theme options (does not set the option if it already exists)
2066 */
2067function setThemeOption($key, $value, $album, $theme, $default = false) {
2068	global $_zp_gallery;
2069	if (is_null($album)) {
2070		$id = 0;
2071	} else {
2072		$id = $album->getID();
2073		$theme = $album->getAlbumTheme();
2074	}
2075	if (empty($theme)) {
2076		$theme = $_zp_gallery->getCurrentTheme();
2077	}
2078	$creator = THEMEFOLDER . '/' . $theme;
2079
2080	$sql = 'INSERT INTO ' . prefix('options') . ' (`name`,`ownerid`,`theme`,`creator`,`value`) VALUES (' . db_quote($key) . ',0,' . db_quote($theme) . ',' . db_quote($creator) . ',';
2081	$sqlu = ' ON DUPLICATE KEY UPDATE `value`=';
2082	if (is_null($value)) {
2083		$sql .= 'NULL';
2084		$sqlu .= 'NULL';
2085	} else {
2086		$sql .= db_quote($value);
2087		$sqlu .= db_quote($value);
2088	}
2089	$sql .= ') ';
2090	if (!$default) {
2091		$sql .= $sqlu;
2092	}
2093	$result = query($sql, false);
2094}
2095
2096/**
2097 * Replaces/renames an option. If the old option exits, it creates the new option with the old option's value as the default
2098 * unless the new option has already been set otherwise. Independently it always deletes the old option.
2099 *
2100 * @param string $oldkey Old option name
2101 * @param string $newkey New option name
2102 *
2103 * @since Zenphoto 1.5.1
2104 */
2105function replaceThemeOption($oldkey, $newkey) {
2106	$oldoption = getThemeOption($oldkey);
2107	if ($oldoption) {
2108		setThemeOptionDefault($newkey, $oldoption);
2109		purgeThemeOption($oldkey);
2110	}
2111}
2112
2113/**
2114 * Deletes an option from the database
2115 *
2116 * @global array $_zp_options
2117 * @param string $key
2118 *
2119 * @since Zenphoto 1.5.1
2120 */
2121function purgeThemeOption($key, $album = NULL, $theme = NULL) {
2122	global $_set_theme_album, $_zp_gallery;
2123	if (is_null($album)) {
2124		$album = $_set_theme_album;
2125	}
2126	if (is_null($album)) {
2127		$id = 0;
2128	} else {
2129		$id = $album->getID();
2130		$theme = $album->getAlbumTheme();
2131	}
2132	if (empty($theme)) {
2133		$theme = $_zp_gallery->getCurrentTheme();
2134	}
2135	$sql = 'DELETE FROM ' . prefix('options') . ' WHERE `name`=' . db_quote($key) . ' AND `ownerid`=' . $id . ' AND `theme`=' . db_quote($theme);
2136	query($sql, false);
2137}
2138
2139/**
2140 * Used to set default values for theme specific options
2141 *
2142 * @param string $key
2143 * @param mixed $value
2144 */
2145function setThemeOptionDefault($key, $value) {
2146	$bt = debug_backtrace();
2147	$b = array_shift($bt);
2148	$theme = basename(dirname($b['file']));
2149	setThemeOption($key, $value, NULL, $theme, true);
2150}
2151
2152/**
2153 * Returns the value of a theme option
2154 *
2155 * @param string $option option key
2156 * @param object $album
2157 * @param string $theme default theme name
2158 * @return mixed
2159 */
2160function getThemeOption($option, $album = NULL, $theme = NULL) {
2161	global $_set_theme_album, $_zp_gallery;
2162	if (is_null($album)) {
2163		$album = $_set_theme_album;
2164	}
2165	if (is_null($album)) {
2166		$id = 0;
2167	} else {
2168		$id = $album->getID();
2169		$theme = $album->getAlbumTheme();
2170	}
2171	if (empty($theme)) {
2172		$theme = $_zp_gallery->getCurrentTheme();
2173	}
2174
2175	// album-theme
2176	$sql = "SELECT `value` FROM " . prefix('options') . " WHERE `name`=" . db_quote($option) . " AND `ownerid`=" . $id . " AND `theme`=" . db_quote($theme);
2177	$db = query_single_row($sql);
2178	if (!$db) {
2179		// raw theme option
2180		$sql = "SELECT `value` FROM " . prefix('options') . " WHERE `name`=" . db_quote($option) . " AND `ownerid`=0 AND `theme`=" . db_quote($theme);
2181		$db = query_single_row($sql);
2182		if (!$db) {
2183			// raw album option
2184			$sql = "SELECT `value` FROM " . prefix('options') . " WHERE `name`=" . db_quote($option) . " AND `ownerid`=" . $id . " AND `theme`=NULL";
2185			$db = query_single_row($sql);
2186			if (!$db) {
2187				return getOption($option);
2188			}
2189		}
2190	}
2191	return $db['value'];
2192}
2193
2194/**
2195 * Returns true if all the right conditions are set to allow comments for the $type
2196 *
2197 * @param string $type Which comments
2198 * @return bool
2199 */
2200function commentsAllowed($type) {
2201	return getOption($type) && (!MEMBERS_ONLY_COMMENTS || zp_loggedin(ADMIN_RIGHTS | POST_COMMENT_RIGHTS));
2202}
2203
2204/**
2205 * Returns the viewer's IP address
2206 * Deals with transparent proxies
2207 *
2208 * @param bool $anonymize If null (default) the backend option setting is used. Override with anonymize levels
2209 * - 0 (No anonymizing)
2210 * - 1 (Last fourth anonymized)
2211 * - 2 (Last half anonymized)
2212 * - 3 (Last three fourths anonymized)
2213 * - 4 (Full anonymization, no IP stored)
2214 * @return string
2215 */
2216function getUserIP($anonymize = null) {
2217	if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2218		$ip = sanitize($_SERVER['HTTP_X_FORWARDED_FOR']);
2219		if (filter_var($ip, FILTER_VALIDATE_IP)) {
2220			return getAnonymIP($ip, $anonymize);
2221		}
2222	}
2223	$ip = sanitize($_SERVER['REMOTE_ADDR']);
2224	if (filter_var($ip, FILTER_VALIDATE_IP)) {
2225		return getAnonymIP($ip, $anonymize);
2226	}
2227	return NULL;
2228}
2229
2230/**
2231 * Anonymizing of IP addresses
2232 * @param bool $anonymize If null (default) the backend option setting is used. Override with anonymize levels
2233 * - 0 (No anonymizing)
2234 * - 1 (Last fourth anonymized)
2235 * - 2 (Last half anonymized)
2236 * - 3 (Last three fourths anonymized)
2237 * - 4 (Full anonymization, no IP stored)
2238 *
2239 * @return string
2240 */
2241function getAnonymIP($ip, $anonymize = null) {
2242	if (is_null($anonymize)) {
2243		$anonymize = getOption('anonymize_ip');
2244	}
2245	$is_ipv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
2246	switch ($anonymize) {
2247		case 0; // No anonymizing
2248			return $ip;
2249		default:
2250		case 1; // Last fourth anonymized
2251			if ($is_ipv6) {
2252				return preg_replace('~[0-9a-zA-Z]*:[0-9a-zA-Z]+$~', '0:0', $ip);
2253			} else {
2254				return preg_replace('~[0-9a-zA-Z]+$~', '0', $ip);
2255			}
2256		case 2: // Last half anonymized
2257			if ($is_ipv6) {
2258				return preg_replace('~[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]+$~', '0:0:0:0', $ip);
2259			} else {
2260				return preg_replace('~[0-9a-zA-Z]*.[0-9a-zA-Z]+$~', '0.0', $ip);
2261			}
2262		case 3: // Last three fourths anonymized
2263			if ($is_ipv6) {
2264				return preg_replace('~[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]*:[0-9a-zA-Z]+$~', '0:0:0:0:0:0', $ip);
2265			} else {
2266				return preg_replace('~[0-9a-zA-Z]*.[0-9a-zA-Z]*.[0-9a-zA-Z]+$~', '0.0.0', $ip);
2267			}
2268		case 4: // Full anonymization, no IP stored
2269			if ($is_ipv6) {
2270				return '0:0:0:0:0:0:0:0';
2271			} else {
2272				return '0.0.0.0';
2273			}
2274	}
2275}
2276
2277/**
2278 * Strips out and/or replaces characters from the string that are not "soe" friendly
2279 *
2280 * @param string $string
2281 * @return string
2282 */
2283function seoFriendly($string) {
2284	$string = trim(preg_replace('~\s+\.\s*~', '.', $string));
2285	if (zp_has_filter('seoFriendly')) {
2286		$string = zp_apply_filter('seoFriendly', $string);
2287	} else { // no filter, do basic cleanup
2288		$string = trim($string);
2289		$string = preg_replace("/\s+/", "-", $string);
2290		$string = preg_replace("/[^a-zA-Z0-9_.-]/", "-", $string);
2291		$string = str_replace(array('---', '--'), '-', $string);
2292	}
2293	return $string;
2294}
2295
2296/**
2297 *
2298 * emit the javascript seojs() function
2299 */
2300function seoFriendlyJS() {
2301	if (zp_has_filter('seoFriendly_js')) {
2302		echo zp_apply_filter('seoFriendly_js');
2303	} else {
2304		?>
2305		function seoFriendlyJS(fname) {
2306		fname=fname.trim();
2307		fname=fname.replace(/\s+\.\s*/,'.');
2308		fname = fname.replace(/\s+/g, '-');
2309		fname = fname.replace(/[^a-zA-Z0-9_.-]/g, '-');
2310		fname = fname.replace(/--*/g, '-');
2311		return fname;
2312		}
2313		<?php
2314	}
2315}
2316
2317/**
2318 * Returns true if there is an internet connection
2319 *
2320 * @param string $host optional host name to test
2321 *
2322 * @return bool
2323 */
2324function is_connected($host = 'www.zenphoto.org') {
2325	$err_no = $err_str = false;
2326	$connected = @fsockopen($host, 80, $errno, $errstr, 0.5);
2327	if ($connected) {
2328		fclose($connected);
2329		return true;
2330	}
2331	return false;
2332}
2333
2334/**
2335 * produce debugging information on 404 errors
2336 * @param string $album
2337 * @param string $image
2338 * @param string $theme
2339 */
2340function debug404($album, $image, $theme) {
2341	if (DEBUG_404) {
2342		$list = explode('/', $album);
2343		if (array_shift($list) == 'cache') {
2344			return;
2345		}
2346		$ignore = array('/favicon.ico', '/zp-data/tést.jpg');
2347		$target = getRequestURI();
2348		foreach ($ignore as $uri) {
2349			if ($target == $uri)
2350				return;
2351		}
2352		$server = array();
2353		foreach (array('REQUEST_URI', 'HTTP_REFERER', 'REMOTE_ADDR', 'REDIRECT_STATUS') as $key) {
2354			$server[$key] = @$_SERVER[$key];
2355		}
2356		$request = $_REQUEST;
2357		$request['theme'] = $theme;
2358		if (!empty($image)) {
2359			$request['image'] = $image;
2360		}
2361
2362		trigger_error(sprintf(gettext('Zenphoto processed a 404 error on %s. See the debug log for details.'), $target), E_USER_NOTICE);
2363		ob_start();
2364		var_dump($server);
2365		$server = preg_replace('~array\s*\(.*\)\s*~', '', html_decode(getBare(ob_get_contents())));
2366		ob_end_clean();
2367		ob_start();
2368		var_dump($request);
2369		$request['theme'] = $theme;
2370		if (!empty($image)) {
2371			$request['image'] = $image;
2372		}
2373		$request = preg_replace('~array\s*\(.*\)\s*~', '', html_decode(getBare(ob_get_contents())));
2374		ob_end_clean();
2375		debugLog("404 error details\n" . $server . $request);
2376	}
2377}
2378
2379/**
2380 * Checks for Cross Site Request Forgeries
2381 * @param string $action
2382 */
2383function XSRFdefender($action) {
2384	$token = getXSRFToken($action);
2385	if (!isset($_REQUEST['XSRFToken']) || $_REQUEST['XSRFToken'] != $token) {
2386		zp_apply_filter('admin_XSRF_access', false, $action);
2387		redirectURL(FULLWEBPATH . '/' . ZENFOLDER . '/admin.php?action=external&error&msg=' . sprintf(gettext('“%s” Cross Site Request Forgery blocked.'), $action), '302');
2388	}
2389	unset($_REQUEST['XSRFToken']);
2390	unset($_POST['XSRFToken']);
2391	unset($_GET['XSRFToken']);
2392}
2393
2394/**
2395 * returns an XSRF token
2396 * @param striong $action
2397 */
2398function getXSRFToken($action) {
2399	global $_zp_current_admin_obj;
2400	$admindata = '';
2401	if (!is_null($_zp_current_admin_obj)) {
2402		$admindata = $_zp_current_admin_obj->getData();
2403		unset($admindata['lastvisit']);
2404	}
2405	return sha1($action . prefix(ZENPHOTO_VERSION) . serialize($admindata) . session_id());
2406}
2407
2408/**
2409 * Emits a "hidden" input for the XSRF token
2410 * @param string $action
2411 */
2412function XSRFToken($action) {
2413	?>
2414	<input type="hidden" name="XSRFToken" id="XSRFToken" value="<?php echo getXSRFToken($action); ?>" />
2415	<?php
2416}
2417
2418/**
2419 * Starts a sechedule script run
2420 * @param string $script The script file to load
2421 * @param array $params "POST" parameters
2422 * @param bool $inline set to true to run the task "in-line". Set false run asynchronously
2423 */
2424function cron_starter($script, $params, $offsetPath, $inline = false) {
2425	global $_zp_authority, $_zp_loggedin, $_zp_current_admin_obj, $_zp_HTML_cache;
2426	$admin = $_zp_authority->getMasterUser();
2427
2428	if ($inline) {
2429		$_zp_current_admin_obj = $admin;
2430		$_zp_loggedin = $_zp_current_admin_obj->getRights();
2431		foreach ($params as $key => $value) {
2432			if ($key == 'XSRFTag') {
2433				$key = 'XSRFToken';
2434				$value = getXSRFToken($value);
2435			}
2436			$_POST[$key] = $_GET[$key] = $_REQUEST[$key] = $value;
2437		}
2438		require_once($script);
2439	} else {
2440		$auth = sha1($script . serialize($admin));
2441		$paramlist = 'link=' . $script;
2442		foreach ($params as $key => $value) {
2443			$paramlist .= '&' . $key . '=' . $value;
2444		}
2445		$paramlist .= '&auth=' . $auth . '&offsetPath=' . $offsetPath;
2446		$_zp_HTML_cache->abortHTMLCache();
2447		?>
2448		<script type="text/javascript">
2449			// <!-- <![CDATA[
2450			$.ajax({
2451				type: 'POST',
2452				cache: false,
2453				data: '<?php echo $paramlist; ?>',
2454				url: '<?php echo WEBPATH . '/' . ZENFOLDER; ?>/cron_runner.php'
2455			});
2456			// ]]> -->
2457		</script>
2458		<?php
2459	}
2460}
2461
2462/**
2463 *
2464 * Check if logged in (with specific rights)
2465 * Returns a true value if there is a user logged on with the required rights
2466 *
2467 * @param bit $rights rights required by the caller
2468 *
2469 * @return bool
2470 */
2471function zp_loggedin($rights = ALL_RIGHTS) {
2472	global $_zp_loggedin, $_zp_current_admin_obj;
2473	$loggedin = $_zp_loggedin & ($rights | ADMIN_RIGHTS);
2474	if ($loggedin && $_zp_current_admin_obj) {
2475		$_zp_current_admin_obj->updateLastVisit();
2476	}
2477	return $loggedin;
2478}
2479
2480/**
2481 * Provides an error protected read of image EXIF/IPTC data
2482 *
2483 * @param string $path image path
2484 * @return array
2485 *
2486 */
2487function read_exif_data_protected($path) {
2488	if (DEBUG_EXIF) {
2489		debugLog("Begin read_exif_data_protected($path)");
2490		$start = microtime(true);
2491	}
2492	try {
2493		$rslt = read_exif_data_raw($path, false);
2494	} catch (Exception $e) {
2495		debugLog("read_exif_data($path) exception: " . $e->getMessage());
2496		$rslt = array();
2497	}
2498	if (DEBUG_EXIF) {
2499		$time = microtime(true) - $start;
2500		debugLog(sprintf("End read_exif_data_protected($path) [%f]", $time));
2501	}
2502	return $rslt;
2503}
2504
2505/**
2506 *
2507 * fetches the path to the flag image
2508 * @param string $lang whose flag
2509 * @return string
2510 */
2511function getLanguageFlag($lang) {
2512	if (file_exists(SERVERPATH . '/' . USER_PLUGIN_FOLDER . '/locale/' . $lang . '/flag.png')) {
2513		$flag = WEBPATH . '/' . USER_PLUGIN_FOLDER . '/locale/' . $lang . '/flag.png';
2514	} else if (file_exists(SERVERPATH . '/' . ZENFOLDER . '/locale/' . $lang . '/flag.png')) {
2515		$flag = WEBPATH . '/' . ZENFOLDER . '/locale/' . $lang . '/flag.png';
2516	} else {
2517		$flag = WEBPATH . '/' . ZENFOLDER . '/locale/missing_flag.png';
2518	}
2519	return $flag;
2520}
2521
2522/**
2523 * Gets an item object by id
2524 *
2525 * @param string $table database table to search
2526 * @param int $id id of the item to get
2527 * @return mixed
2528 */
2529function getItemByID($table, $id) {
2530	if ($result = query_single_row('SELECT * FROM ' . prefix($table) . ' WHERE id =' . (int) $id)) {
2531		switch ($table) {
2532			case 'images':
2533				if ($alb = getItemByID('albums', $result['albumid'])) {
2534					return newImage($alb, $result['filename'], true);
2535				}
2536				break;
2537			case 'albums':
2538				return newAlbum($result['folder'], false, true);
2539			case 'news':
2540				return new ZenpageNews($result['titlelink']);
2541			case 'pages':
2542				return new ZenpagePage($result['titlelink']);
2543			case 'news_categories':
2544				return new ZenpageCategory($result['titlelink']);
2545		}
2546	}
2547	return NULL;
2548}
2549
2550/**
2551 * uses down and up arrow links to show and hide sections of HTML
2552 *
2553 * @param string $content the id of the html section to be revealed
2554 * @param bool $visible true if the content is initially visible
2555 */
2556function reveal($content, $visible = false) {
2557	?>
2558	<span id="<?php echo $content; ?>_reveal"<?php if ($visible) echo 'style="display:none;"'; ?> class="icons">
2559		<a href="javascript:reveal('<?php echo $content; ?>')" title="<?php echo gettext('Click to show content'); ?>">
2560			<img src="../../images/arrow_down.png" alt="" class="icon-position-top4" />
2561		</a>
2562	</span>
2563	<span id="<?php echo $content; ?>_hide"<?php if (!$visible) echo 'style="display:none;"'; ?> class="icons">
2564		<a href="javascript:reveal('<?php echo $content; ?>')" title="<?php echo gettext('Click to hide content'); ?>">
2565			<img src="../../images/arrow_up.png" alt="" class="icon-position-top4" />
2566		</a>
2567	</span>
2568	<?php
2569}
2570
2571/**
2572 * Deals with the [macro parameters] substitutions
2573 *
2574 * See the macroList plugin for details
2575 *
2576 * @param string $text
2577 * @return string
2578 */
2579function applyMacros($text) {
2580	$content_macros = getMacros();
2581	preg_match_all('/\[(\w+)(.*?)\]/i', $text, $instances);
2582	foreach ($instances[0] as $instance => $macro_instance) {
2583		$macroname = strtoupper($instances[1][$instance]);
2584		if (array_key_exists($macroname, $content_macros)) {
2585			$macro = $content_macros[$macroname];
2586			$p = $instances[2][$instance];
2587			$data = NULL;
2588			$class = $macro['class'];
2589			if ($p) {
2590				$p = trim(utf8::sanitize(str_replace("\xC2\xA0", ' ', strip_tags($p)))); //	remove hard spaces and invalid characters
2591				$p = preg_replace("~\s+=\s+(?=(?:[^\"]*+\"[^\"]*+\")*+[^\"]*+$)~", "=", $p); //	deblank assignment operator
2592				preg_match_all("~'[^'\"]++'|\"[^\"]++\"|[^\s]++~", $p, $l); //	parse the parameter list
2593				$parms = array();
2594				$k = 0;
2595				foreach ($l[0] as $s) {
2596					if ($s != ',') {
2597						$parms[$k++] = trim($s, '\'"'); //	remove any quote marks
2598					}
2599				}
2600			} else {
2601				$parms = array();
2602			}
2603			$parameters = array();
2604			if (!empty($macro['params'])) {
2605				$err = false;
2606				foreach ($macro['params'] as $key => $type) {
2607					$data = false;
2608					if (array_key_exists($key, $parms)) {
2609						switch (trim($type, '*')) {
2610							case 'int':
2611								if (is_numeric($parms[$key])) {
2612									$parameters[] = (int) $parms[$key];
2613								} else {
2614									$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> parameter %2$d should be a number.'), trim($macro_instance, '[]'), $key + 1) . '</span>';
2615									$class = 'error';
2616								}
2617								break;
2618							case 'string':
2619								if (is_string($parms[$key])) {
2620									$parameters[] = $parms[$key];
2621								} else {
2622									$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> parameter %2$d should be a string.'), trim($macro_instance, '[]'), $key + 1) . '</span>';
2623									$class = 'error';
2624								}
2625								break;
2626							case 'bool':
2627								switch (strtolower($parms[$key])) {
2628									case ("true"):
2629										$parameters[] = true;
2630										break;
2631									case ("false"):
2632										$parameters[] = false;
2633										break;
2634									default:
2635										$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> parameter %2$d should be <code>true</code> or <code>false</code>.'), trim($macro_instance, '[]'), $key + 1) . '</span>';
2636										$class = 'error';
2637										break;
2638								}
2639								break;
2640							case 'array':
2641								$l = array_slice($parms, $key);
2642								$parms = array();
2643								foreach ($l as $key => $p) {
2644									$x = explode('=', $p);
2645									if (count($x) == 2) {
2646										$parms[$x[0]] = $x[1];
2647									} else {
2648										$parms[$key] = $x[0];
2649									}
2650								}
2651								$parameters[] = $parms;
2652								break;
2653							default:
2654								$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> parameter %2$d is incorrectly defined.'), trim($macro_instance, '[]'), $key + 1) . '</span>';
2655								$class = 'error';
2656								break;
2657						}
2658					} else {
2659						if (strpos($type, '*') === false) {
2660							$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> parameter %2$d is missing.'), trim($macro_instance, '[]'), $key + 1) . '</span>';
2661							$class = 'error';
2662						}
2663						break;
2664					}
2665				}
2666			} else {
2667				if (!empty($p)) {
2668					$class = 'error';
2669					$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> macro does not take parameters'), trim($macro_instance, '[]')) . '</span>';
2670				}
2671			}
2672			switch ($class) {
2673				case 'error':
2674					break;
2675				case 'function';
2676				case 'procedure':
2677					if (is_callable($macro['value'])) {
2678						if ($class == 'function') {
2679							ob_start();
2680							$data = call_user_func_array($macro['value'], $parameters);
2681							if (empty($data)) {
2682								$data = ob_get_contents();
2683							}
2684							ob_end_clean();
2685						} else {
2686							ob_start();
2687							call_user_func_array($macro['value'], $parameters);
2688							$data = ob_get_contents();
2689							ob_end_clean();
2690						}
2691						if (empty($data)) {
2692							$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> retuned no data'), trim($macro_instance, '[]')) . '</span>';
2693						} else {
2694							$data = "\n<!--Begin " . $macroname . "-->\n" . $data . "\n<!--End " . $macroname . "-->\n";
2695						}
2696					} else {
2697						$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> <code>%2$s</code> is not callable'), trim($macro_instance, '[]'), $macro['value']) . '</span>';
2698					}
2699					break;
2700				case 'constant':
2701					$data = "\n<!--Begin " . $macroname . "-->\n" . $macro['value'] . "\n<!--End " . $macroname . "-->\n";
2702					break;
2703				case 'expression':
2704					$expression = '$data = ' . $macro['value'];
2705					$parms = array_reverse($parms, true);
2706					preg_match_all('/\$\d+/', $macro['value'], $replacements);
2707					foreach ($replacements as $rkey => $v) {
2708						if (empty($v))
2709							unset($replacements[$rkey]);
2710					}
2711					if (count($parms) == count($replacements)) {
2712
2713						foreach ($parms as $key => $value) {
2714							$key++;
2715							$expression = preg_replace('/\$' . $key . '/', db_quote($value), $expression);
2716						}
2717						eval($expression);
2718						if (!isset($data) || is_null($data)) {
2719							$data = '<span class="error">' . sprintf(gettext('<em>[%1$s]</em> retuned no data'), trim($macro_instance, '[]')) . '</span>';
2720						} else {
2721							$data = "\n<!--Begin " . $macroname . "-->\n" . $data . "\n<!--End " . $macroname . "-->\n";
2722						}
2723					} else {
2724						$data = '<span class="error">' . sprintf(ngettext('<em>[%1$s]</em> takes %2$d parameter', '<em>[%1$s]</em> takes %2$d parameters', count($replacements)), trim($macro_instance, '[]'), count($replacements)) . '</span>';
2725					}
2726					break;
2727			}
2728			$text = str_replace($macro_instance, $data, $text);
2729		}
2730	}
2731	return $text;
2732}
2733
2734function getMacros() {
2735	global $_zp_content_macros;
2736	if (is_null($_zp_content_macros)) {
2737		$_zp_content_macros = zp_apply_filter('content_macro', array());
2738	}
2739	return $_zp_content_macros;
2740}
2741
2742/**
2743 * generates a nested list of albums for the album tab sorting
2744 * Returns an array of "albums" each element contains:
2745 * 								'name' which is the folder name
2746 * 								'sort_order' which is an array of the sort order set
2747 *
2748 * @param $subalbum root level album (NULL is the gallery)
2749 * @param $levels how far to nest
2750 * @param $checkalbumrights TRUE (Default) for album rights for backend usage, FALSE to skip for frontend usage
2751 * @param $level internal for keeping the sort order elements
2752 * @return array
2753 */
2754function getNestedAlbumList($subalbum, $levels, $checkalbumrights = true, $level = array()) {
2755	global $_zp_gallery;
2756	$cur = count($level);
2757	$levels--; // make it 0 relative to sync with $cur
2758	if (is_null($subalbum)) {
2759		$albums = $_zp_gallery->getAlbums();
2760	} else {
2761		$albums = $subalbum->getAlbums();
2762	}
2763	$list = array();
2764	foreach ($albums as $analbum) {
2765		$albumobj = newAlbum($analbum);
2766		$accessallowed = true;
2767		if ($checkalbumrights) {
2768			$accessallowed = $albumobj->isMyItem(ALBUM_RIGHTS);
2769		}
2770		if (!is_null($subalbum) || $accessallowed) {
2771			$level[$cur] = sprintf('%03u', $albumobj->getSortOrder());
2772			$list[] = array('name' => $analbum, 'sort_order' => $level);
2773			if ($cur < $levels && ($albumobj->getNumAlbums()) && !$albumobj->isDynamic()) {
2774				$list = array_merge($list, getNestedAlbumList($albumobj, $levels + 1, $checkalbumrights, $level));
2775			}
2776		}
2777	}
2778	return $list;
2779}
2780
2781/**
2782 * initializes the $_zp_exifvars array display state
2783 *
2784 */
2785function setexifvars() {
2786	global $_zp_exifvars;
2787	/*
2788	 * Note: If fields are added or deleted, setup should be run or the new data won't be stored
2789	 * (but existing fields will still work; nothing breaks).
2790	 *
2791	 * This array should be ordered by logical associations as it will be the order that EXIF information
2792	 * is displayed
2793	 */
2794	$_zp_exifvars = array(
2795			// Database Field       		 => array('source', 'Metadata Key', 'ZP Display Text', Display?,	size (ignored!), enabled, type)
2796			'EXIFMake' => array('IFD0', 'Make', gettext('Camera Maker'), true, 52, true, 'string'),
2797			'EXIFModel' => array('IFD0', 'Model', gettext('Camera Model'), true, 52, true, 'string'),
2798			'EXIFDescription' => array('IFD0', 'ImageDescription', gettext('Image Title'), false, 52, true, 'string'),
2799			'IPTCObjectName' => array('IPTC', 'ObjectName', gettext('Object Name'), false, 256, true, 'string'),
2800			'IPTCImageHeadline' => array('IPTC', 'ImageHeadline', gettext('Image Headline'), false, 256, true, 'string'),
2801			'IPTCImageCaption' => array('IPTC', 'ImageCaption', gettext('Image Caption'), false, 2000, true, 'string'),
2802			'IPTCImageCaptionWriter' => array('IPTC', 'ImageCaptionWriter', gettext('Image Caption Writer'), false, 32, true, 'string'),
2803			'EXIFDateTime' => array('SubIFD', 'DateTime', gettext('Time Taken'), true, 52, true, 'time'),
2804			'EXIFDateTimeOriginal' => array('SubIFD', 'DateTimeOriginal', gettext('Original Time Taken'), true, 52, true, 'time'),
2805			'EXIFDateTimeDigitized' => array('SubIFD', 'DateTimeDigitized', gettext('Time Digitized'), true, 52, true, 'time'),
2806			'IPTCDateCreated' => array('IPTC', 'DateCreated', gettext('Date Created'), false, 8, true, 'time'),
2807			'IPTCTimeCreated' => array('IPTC', 'TimeCreated', gettext('Time Created'), false, 11, true, 'time'),
2808			'IPTCDigitizeDate' => array('IPTC', 'DigitizeDate', gettext('Digital Creation Date'), false, 8, true, 'time'),
2809			'IPTCDigitizeTime' => array('IPTC', 'DigitizeTime', gettext('Digital Creation Time'), false, 11, true, 'time'),
2810			'EXIFArtist' => array('IFD0', 'Artist', gettext('Artist'), false, 52, true, 'string'),
2811			'IPTCImageCredit' => array('IPTC', 'ImageCredit', gettext('Image Credit'), false, 32, true, 'string'),
2812			'IPTCByLine' => array('IPTC', 'ByLine', gettext('Byline'), false, 32, true, 'string'),
2813			'IPTCByLineTitle' => array('IPTC', 'ByLineTitle', gettext('Byline Title'), false, 32, true, 'string'),
2814			'IPTCSource' => array('IPTC', 'Source', gettext('Image Source'), false, 32, true, 'string'),
2815			'IPTCContact' => array('IPTC', 'Contact', gettext('Contact'), false, 128, true, 'string'),
2816			'EXIFCopyright' => array('IFD0', 'Copyright', gettext('Copyright Holder'), false, 128, true, 'string'),
2817			'IPTCCopyright' => array('IPTC', 'Copyright', gettext('Copyright Notice'), false, 128, true, 'string'),
2818			'IPTCKeywords' => array('IPTC', 'Keywords', gettext('Keywords'), false, 0, true, 'string'),
2819			'EXIFExposureTime' => array('SubIFD', 'ExposureTime', gettext('Shutter Speed'), true, 52, true, 'string'),
2820			'EXIFFNumber' => array('SubIFD', 'FNumber', gettext('Aperture'), true, 52, true, 'number'),
2821			'EXIFISOSpeedRatings' => array('SubIFD', 'ISOSpeedRatings', gettext('ISO Sensitivity'), true, 52, true, 'number'),
2822			'EXIFExposureBiasValue' => array('SubIFD', 'ExposureBiasValue', gettext('Exposure Compensation'), true, 52, true, 'string'),
2823			'EXIFMeteringMode' => array('SubIFD', 'MeteringMode', gettext('Metering Mode'), true, 52, true, 'string'),
2824			'EXIFFlash' => array('SubIFD', 'Flash', gettext('Flash Fired'), true, 52, true, 'string'),
2825			'EXIFImageWidth' => array('SubIFD', 'ExifImageWidth', gettext('Original Width'), false, 52, true, 'number'),
2826			'EXIFImageHeight' => array('SubIFD', 'ExifImageHeight', gettext('Original Height'), false, 52, true, 'number'),
2827			'EXIFOrientation' => array('IFD0', 'Orientation', gettext('Orientation'), false, 52, true, 'string'),
2828			'EXIFSoftware' => array('IFD0', 'Software', gettext('Software'), false, 999, true, 'string'),
2829			'EXIFContrast' => array('SubIFD', 'Contrast', gettext('Contrast Setting'), false, 52, true, 'string'),
2830			'EXIFSharpness' => array('SubIFD', 'Sharpness', gettext('Sharpness Setting'), false, 52, true, 'string'),
2831			'EXIFSaturation' => array('SubIFD', 'Saturation', gettext('Saturation Setting'), false, 52, true, 'string'),
2832			'EXIFWhiteBalance' => array('SubIFD', 'WhiteBalance', gettext('White Balance'), false, 52, true, 'string'),
2833			'EXIFSubjectDistance' => array('SubIFD', 'SubjectDistance', gettext('Subject Distance'), false, 52, true, 'number'),
2834			'EXIFFocalLength' => array('SubIFD', 'FocalLength', gettext('Focal Length'), true, 52, true, 'number'),
2835			'EXIFLensType' => array('SubIFD', 'LensType', gettext('Lens Type'), false, 52, true, 'string'),
2836			'EXIFLensInfo' => array('SubIFD', 'LensInfo', gettext('Lens Info'), false, 52, true, 'string'),
2837			'EXIFFocalLengthIn35mmFilm' => array('SubIFD', 'FocalLengthIn35mmFilm', gettext('35mm Focal Length Equivalent'), false, 52, true, 'string'),
2838			'IPTCCity' => array('IPTC', 'City', gettext('City'), false, 32, true, 'string'),
2839			'IPTCSubLocation' => array('IPTC', 'SubLocation', gettext('Sub-location'), false, 32, true, 'string'),
2840			'IPTCState' => array('IPTC', 'State', gettext('Province/State'), false, 32, true, 'string'),
2841			'IPTCLocationCode' => array('IPTC', 'LocationCode', gettext('Country/Primary Location Code'), false, 3, true, 'string'),
2842			'IPTCLocationName' => array('IPTC', 'LocationName', gettext('Country/Primary Location Name'), false, 64, true, 'string'),
2843			'IPTCContentLocationCode' => array('IPTC', 'ContentLocationCode', gettext('Content Location Code'), false, 3, true, 'string'),
2844			'IPTCContentLocationName' => array('IPTC', 'ContentLocationName', gettext('Content Location Name'), false, 64, true, 'string'),
2845			'EXIFGPSLatitude' => array('GPS', 'Latitude', gettext('Latitude'), false, 52, true, 'number'),
2846			'EXIFGPSLatitudeRef' => array('GPS', 'Latitude Reference', gettext('Latitude Reference'), false, 52, true, 'string'),
2847			'EXIFGPSLongitude' => array('GPS', 'Longitude', gettext('Longitude'), false, 52, true, 'number'),
2848			'EXIFGPSLongitudeRef' => array('GPS', 'Longitude Reference', gettext('Longitude Reference'), false, 52, true, 'string'),
2849			'EXIFGPSAltitude' => array('GPS', 'Altitude', gettext('Altitude'), false, 52, true, 'number'),
2850			'EXIFGPSAltitudeRef' => array('GPS', 'Altitude Reference', gettext('Altitude Reference'), false, 52, true, 'string'),
2851			'IPTCOriginatingProgram' => array('IPTC', 'OriginatingProgram', gettext('Originating Program'), false, 32, true, 'string'),
2852			'IPTCProgramVersion' => array('IPTC', 'ProgramVersion', gettext('Program Version'), false, 10, true, 'string'),
2853			'VideoFormat' => array('VIDEO', 'fileformat', gettext('Video File Format'), false, 32, true, 'string'),
2854			'VideoSize' => array('VIDEO', 'filesize', gettext('Video File Size'), false, 32, true, 'number'),
2855			'VideoArtist' => array('VIDEO', 'artist', gettext('Video Artist'), false, 256, true, 'string'),
2856			'VideoTitle' => array('VIDEO', 'title', gettext('Video Title'), false, 256, true, 'string'),
2857			'VideoBitrate' => array('VIDEO', 'bitrate', gettext('Bitrate'), false, 32, true, 'number'),
2858			'VideoBitrate_mode' => array('VIDEO', 'bitrate_mode', gettext('Bitrate_Mode'), false, 32, true, 'string'),
2859			'VideoBits_per_sample' => array('VIDEO', 'bits_per_sample', gettext('Bits per sample'), false, 32, true, 'number'),
2860			'VideoCodec' => array('VIDEO', 'codec', gettext('Codec'), false, 32, true, 'string'),
2861			'VideoCompression_ratio' => array('VIDEO', 'compression_ratio', gettext('Compression Ratio'), false, 32, true, 'number'),
2862			'VideoDataformat' => array('VIDEO', 'dataformat', gettext('Video Dataformat'), false, 32, true, 'string'),
2863			'VideoEncoder' => array('VIDEO', 'encoder', gettext('File Encoder'), false, 10, true, 'string'),
2864			'VideoSamplerate' => array('VIDEO', 'Samplerate', gettext('Sample rate'), false, 32, true, 'number'),
2865			'VideoChannelmode' => array('VIDEO', 'channelmode', gettext('Channel mode'), false, 32, true, 'string'),
2866			'VideoFormat' => array('VIDEO', 'format', gettext('Format'), false, 10, true, 'string'),
2867			'VideoChannels' => array('VIDEO', 'channels', gettext('Channels'), false, 10, true, 'number'),
2868			'VideoFramerate' => array('VIDEO', 'framerate', gettext('Frame rate'), false, 32, true, 'number'),
2869			'VideoResolution_x' => array('VIDEO', 'resolution_x', gettext('X Resolution'), false, 32, true, 'number'),
2870			'VideoResolution_y' => array('VIDEO', 'resolution_y', gettext('Y Resolution'), false, 32, true, 'number'),
2871			'VideoAspect_ratio' => array('VIDEO', 'pixel_aspect_ratio', gettext('Aspect ratio'), false, 32, true, 'number'),
2872			'VideoPlaytime' => array('VIDEO', 'playtime_string', gettext('Play Time'), false, 10, true, 'number'),
2873			'XMPrating' => array('XMP', 'rating', gettext('XMP Rating'), false, 10, true, 'string'),
2874	);
2875	foreach ($_zp_exifvars as $key => $item) {
2876		if (!is_null($disable = getOption($key . '-disabled'))) {
2877			$_zp_exifvars[$key][5] = !$disable;
2878		}
2879		$_zp_exifvars[$key][3] = getOption($key);
2880	}
2881}
2882
2883/**
2884 *
2885 * Returns true if the install is not a "clone"
2886 */
2887function hasPrimaryScripts() {
2888	if (!defined('PRIMARY_INSTALLATION')) {
2889		if (function_exists('readlink') && ($zen = str_replace('\\', '/', @readlink(SERVERPATH . '/' . ZENFOLDER)))) {
2890			// no error reading the link info
2891			$os = strtoupper(PHP_OS);
2892			$sp = SERVERPATH;
2893			if (substr($os, 0, 3) == 'WIN' || $os == 'DARWIN') { // canse insensitive file systems
2894				$sp = strtolower($sp);
2895				$zen = strtolower($zen);
2896			}
2897			define('PRIMARY_INSTALLATION', $sp == dirname($zen));
2898		} else {
2899			define('PRIMARY_INSTALLATION', true);
2900		}
2901	}
2902	return PRIMARY_INSTALLATION;
2903}
2904
2905/**
2906 *
2907 * Recursively clears and removes a folder
2908 * @param string $path
2909 * @return boolean
2910 */
2911function removeDir($path, $within = false) {
2912	if (($dir = @opendir($path)) !== false) {
2913		while (($file = readdir($dir)) !== false) {
2914			if ($file != '.' && $file != '..') {
2915				if ((is_dir($path . '/' . $file))) {
2916					if (!removeDir($path . '/' . $file)) {
2917						return false;
2918					}
2919				} else {
2920					@chmod($path . $file, 0777);
2921					if (!@unlink($path . '/' . $file)) {
2922						return false;
2923					}
2924				}
2925			}
2926		}
2927		closedir($dir);
2928		if (!$within) {
2929			@chmod($path, 0777);
2930			if (!@rmdir($path)) {
2931				return false;
2932			}
2933		}
2934		return true;
2935	}
2936	return false;
2937}
2938
2939/**
2940 * inserts location independent WEB path tags in place of site path tags
2941 * @param string $text
2942 */
2943function tagURLs($text) {
2944	if (is_string($text) && preg_match('/^a:[0-9]+:{/', $text)) { //	serialized array
2945		$text = getSerializedArray($text);
2946		$serial = true;
2947	} else {
2948		$serial = false;
2949	}
2950	if (is_array($text)) {
2951		foreach ($text as $key => $textelement) {
2952			$text[$key] = tagURLs($textelement);
2953		}
2954		if ($serial) {
2955			$text = serialize($text);
2956		}
2957	} else {
2958		$text = str_replace(WEBPATH, '{*WEBPATH*}', str_replace(FULLWEBPATH, '{*FULLWEBPATH*}', $text));
2959	}
2960	return $text;
2961}
2962
2963/**
2964 * reverses tagURLs()
2965 * @param string $text
2966 * @return string
2967 */
2968function unTagURLs($text) {
2969	if (is_string($text) && preg_match('/^a:[0-9]+:{/', $text)) { //	serialized array
2970		$text = getSerializedArray($text);
2971		$serial = true;
2972	} else {
2973		$serial = false;
2974	}
2975	if (is_array($text)) {
2976		foreach ($text as $key => $textelement) {
2977			$text[$key] = unTagURLs($textelement);
2978		}
2979		if ($serial) {
2980			$text = serialize($text);
2981		}
2982	} else {
2983		$text = str_replace('{*WEBPATH*}', WEBPATH, str_replace('{*FULLWEBPATH*}', FULLWEBPATH, $text));
2984	}
2985	return $text;
2986}
2987
2988/**
2989 * Searches out i.php image links and replaces them with cache links if image is cached
2990 * @param string $text
2991 * @return string
2992 */
2993function updateImageProcessorLink($text) {
2994	if (is_string($text) && preg_match('/^a:[0-9]+:{/', $text)) { //	serialized array
2995		$text = getSerializedArray($text);
2996		$serial = true;
2997	} else {
2998		$serial = false;
2999	}
3000	if (is_array($text)) {
3001		foreach ($text as $key => $textelement) {
3002			$text[$key] = updateImageProcessorLink($textelement);
3003		}
3004		if ($serial) {
3005			$text = serialize($text);
3006		}
3007	} else {
3008		preg_match_all('|<\s*img.*?\ssrc\s*=\s*"([^"]*)?|', $text, $matches);
3009		foreach ($matches[1] as $key => $match) {
3010			preg_match('|.*i\.php\?(.*)|', $match, $imgproc);
3011			if ($imgproc) {
3012				$match = preg_split('~\&[amp;]*~', $imgproc[1]);
3013				$set = array();
3014				foreach ($match as $v) {
3015					$s = explode('=', $v);
3016					$set[$s[0]] = $s[1];
3017				}
3018				$args = getImageArgs($set);
3019				$imageuri = getImageURI($args, urldecode($set['a']), urldecode($set['i']), NULL);
3020				if (strpos($imageuri, 'i.php') === false) {
3021					$text = str_replace($matches[1][$key], $imageuri, $text);
3022				}
3023			}
3024		}
3025	}
3026	return $text;
3027}
3028
3029function pluginDebug($extension, $priority, $start) {
3030	list($usec, $sec) = explode(" ", microtime());
3031	$end = (float) $usec + (float) $sec;
3032	$class = array();
3033	if ($priority & CLASS_PLUGIN) {
3034		$class[] = 'CLASS';
3035	}
3036	if ($priority & ADMIN_PLUGIN) {
3037		$class[] = 'ADMIN';
3038	}
3039	if ($priority & FEATURE_PLUGIN) {
3040		$class[] = 'FEATURE';
3041	}
3042	if ($priority & THEME_PLUGIN) {
3043		$class[] = 'THEME';
3044	}
3045	if (empty($class))
3046		$class[] = 'theme';
3047	debugLog(sprintf('    ' . $extension . '(%s:%u)=>%.4fs', implode('|', $class), $priority & PLUGIN_PRIORITY, $end - $start));
3048}
3049
3050/**
3051 * Removes a trailing slash from a string if one exists, otherwise just returns the string
3052 * Used primarily within date and tag searches and news date archive results
3053 *
3054 * @param string $string
3055 * @return string
3056 * @since 1.4.12
3057 */
3058function removeTrailingSlash($string) {
3059	if (substr($string, -1) == '/') {
3060		$length = strlen($string) - 1;
3061		return substr($string, 0, $length);
3062	}
3063	return $string;
3064}
3065
3066/**
3067 * Returns an array the data privacy policy page and the data usage confirmation text as defined on Options > Security
3068 * array(
3069 * 	'notice' => '<The defined text>',
3070 * 	'url' => '<url to the define page either custom page url or Zenpage page>',
3071 * 	'linktext' => '<The defined text>'
3072 * )
3073 *
3074 * @since Zenphoto 1.5
3075 *
3076 * @return array
3077 */
3078function getDataUsageNotice() {
3079	$array = array('notice' => '', 'url' => '', 'linktext' => '');
3080	$array['linktext'] = get_language_string(getOption('dataprivacy_policy_customlinktext'));
3081	$array['notice'] = get_language_string(getOption('dataprivacy_policy_notice'));
3082	$custompage = trim(getOption('dataprivacy_policy_custompage'));
3083	$zenpage_page = '';
3084	if (empty($array['notice'])) {
3085		$array['notice'] = gettext('By using this form you agree with the storage and handling of your data by this website.');
3086	}
3087	if (extensionEnabled('zenpage') && ZP_PAGES_ENABLED) {
3088		$zenpage_page = getOption('dataprivacy_policy_zenpage');
3089		if ($zenpage_page == 'none') {
3090			$zenpage_page = '';
3091		}
3092	}
3093	if (!empty($custompage)) {
3094		$array['url'] = $custompage;
3095	} else if (!empty($zenpage_page)) {
3096		$obj = new ZenpagePage($zenpage_page);
3097		$array['url'] = $obj->getLink();
3098	}
3099	if (empty($array['linktext'])) {
3100		$array['linktext'] = gettext('More info on our data privacy policy.');
3101	}
3102	return $array;
3103}
3104
3105/**
3106 * Prints the data privacy policy page and the data usage confirmation text as defined on Options > Security
3107 * If there is no page defined it only prints the default text.
3108 *
3109 * @since Zenphoto 1.5
3110 */
3111function printDataUsageNotice() {
3112	$data = getDataUsageNotice();
3113	echo $data['notice'];
3114	if (!empty($data['url'])) {
3115		printLinkHTML($data['url'], ' ' . $data['linktext'], $data['linktext'], null, null);
3116	}
3117}
3118
3119/**
3120 * Returns an array with predefined info about general cookies set by the system and/or plugins
3121 *
3122 * @since ZenphotoCMS 1.5.8
3123 *
3124 * @param string $section Name of the section to get: 'authenticaion', 'search', 'admin', 'cookie', 'various' or null (default) for the full array
3125 * @return array
3126 */
3127function getCookieInfoData($section = null) {
3128	$info = array(
3129			'authentication' => array(
3130					'sectiontitle' => gettext('Authentication'),
3131					'sectiondesc' => gettext('Cookies set if logging in as an admin or as one of the various guest user types.'),
3132					'cookies' => array(
3133							'zpcms_auth_user' => gettext('Stores the zenphoto user login credentials.'),
3134							'zpcms_auth_gallery' => gettext('Stores guest user gallery access credentias.'),
3135							'zpcms_auth_search' => gettext('Stores guest user search access credentials'),
3136							'zpcms_auth_image_itemid' => gettext('Stores guest user <em>image item</em> access credentials. <em>itemid</em> refers to the ID of the image.'),
3137							'zpcms_auth_album_itemid' => gettext('Stores guest user <em>album item</em> access credentials. <em>itemid</em> refers to the ID of the album.'),
3138							'zpcms_auth_category_itemid' => gettext('Stores guest user <em>category item</em> access credentials. <em>itemid</em> refers to the ID of the category.'),
3139							'zpcms_auth_page_itemid' => gettext('Stores guest user <em>page item</em> access credentials. <em>itemid</em> refers to the ID of the zenpage page.'),
3140							'zpcms_auth_download' => gettext('Stores guest user access used by the <em>downloadlist</em> plugin.')
3141					),
3142			),
3143			'search' => array(
3144					'sectiontitle' => gettext('Search context (frontend)'),
3145					'sectiondesc' => gettext('These cookies help keep the search result context while browsing results'),
3146					'cookies' => array(
3147							'zpcms_search_params' => gettext('Stores search parameters of the most recent search.'),
3148							'zpcms_search_lastalbum' => gettext('Stores the last album in search context.'),
3149							'zpcms_search_parent' => gettext('Stores the previous page within search context (either the main search results or an album result).')
3150					),
3151			),
3152			'admin' => array(
3153					'sectiontitle' => gettext('Administration'),
3154					'sectiondesc' => gettext('These are set on the backend to help editing.'),
3155					'cookies' => array(
3156							'zpcms_admin_gallery_nesting' => gettext('Stores the setting for the nested album list display on the backend.'),
3157							'zpcms_admin_subalbum_nesting' => gettext('Stores the setting for the nested subalbum list display on the backend.'),
3158							'zpcms_admin_imagestab_imagecount' => gettext('Stores the image count on the backend images pages.'),
3159							'zpcms_admin_uploadtype' => gettext('Stores the upload method on the backend.')
3160					),
3161			),
3162			'cookie' => array(
3163					'sectiontitle' => gettext('Cookie related'),
3164					'sectiondesc' => '',
3165					'cookies' => array(
3166							'zpcms_setup_testcookie' => gettext('Used by setup to test if cookies are operational on the installation. May store the Zenphoto version number of the last unsuccessful run.'),
3167							'zpcms_cookie_path' => gettext('Stores the path for cookies.')
3168					),
3169			),
3170			'various' => array(
3171					'sectiontitle' => gettext('Various'),
3172					'sectiondesc' => gettext('Various cookies set by plugins, themes or otherwise'),
3173					'cookies' => array(
3174							'zcms_ssl' => gettext('Stores the HTTPS/SSL setting.'),
3175							'zpcms_locale' => gettext('Stores the language selection set by the <em>dynamic_locale</em> plugin.'),
3176							'zpcms_mobiletheme' => gettext('Stores if the mobile theme is defined - used by the <em>mobileTheme</em> plugin.'),
3177							'zpcms_themeswitcher_theme' => gettext('Stores the current theme selected by the <em>themeSwitcher</em> plugin.'),
3178							'zpcms_comment' => gettext('Stores information from the comment form POST for re-populaton of the form in the <em>comment_form</em> plugin.')
3179					)
3180			)
3181	);
3182	if (is_null($section) && array_key_exists($section, $info)) {
3183		return $info[$section];
3184	} else {
3185		return $info;
3186	}
3187}
3188
3189/**
3190 * Returns a definition list with predefined info about general cookies set by the system and/or plugins as a string
3191 *
3192 * @since ZenphotoCMS 1.5.8
3193 *
3194 * @param string $section Name of the section to get: 'authenticaion', 'search', 'admin', 'cookie', 'various' or null (default) for the full array
3195 * @param string $sectionheadline Add h2 to h6 to print as the section headline, h2 default.
3196 * @return string
3197 */
3198function getCookieInfoHTML($section = null, $sectionheadline = 'h2') {
3199	$cookies = getCookieInfoData($section);
3200	$html = '';
3201	if ($cookies) {
3202		foreach ($cookies as $section) {
3203			if (!in_array($sectionheadline, array('h2', 'h3', 'h4', 'h5', 'h6'))) {
3204				$sectionheadline = 'h2';
3205			}
3206			$html .= '<' . $sectionheadline . '>' . $section['sectiontitle'] . '</' . $sectionheadline . '>';
3207			$html .= '<p>' . $section['sectiondesc'] . '</p>';
3208			if ($section['cookies']) {
3209				$html .= '<dl>';
3210				foreach ($section['cookies'] as $key => $val) {
3211					$html .= '<dt>' . $key . '</dt>';
3212					$html .= '<dd>' . $val . '</dd>';
3213				}
3214				$html .= '</dl>';
3215			}
3216		}
3217	}
3218	return $html;
3219}
3220
3221/**
3222 * Prints a definition list with predefined info about general cookies set by the system and/or plugins
3223 *
3224 * @since ZenphotoCMS 1.5.8
3225 *
3226 * @param string $section Name of the section to get: 'authenticaion', 'search', 'admin', 'cookie', 'various' or null (default) for the full array
3227 * @param string $sectionheadline Add h2 to h6 to print as the section headline, h2 default.
3228 */
3229function printCookieInfo($section = null, $sectionheadline = 'h2') {
3230	echo getCookieInfoHTML($section, $sectionheadline);
3231}
3232
3233/**
3234 * Registers the content macro(s)
3235 *
3236 * @param array $macros Passes through the array of already registered
3237 * @return array
3238 */
3239function getCookieInfoMacro($macros) {
3240	$macros['COOKIEINFO'] = array(
3241			'class' => 'function',
3242			'params' => array('string*', 'string*'),
3243			'value' => 'getCookieInfoHTML',
3244			'owner' => 'core',
3245			'desc' => gettext('Set %1 to the section to get, set %2 to the h2-h6 for the headline element to use.')
3246	);
3247	return $macros;
3248}
3249
3250/**
3251 * Standins for when no captcha is enabled
3252 */
3253class _zp_captcha {
3254
3255	var $name = NULL; // "captcha" name if no captcha plugin loaded
3256
3257	function getCaptcha($prompt) {
3258		return array('input' => NULL, 'html' => '<p class="errorbox">' . gettext('No captcha handler is enabled.') . '</p>', 'hidden' => '');
3259	}
3260
3261	function checkCaptcha($s1, $s2) {
3262		return false;
3263	}
3264
3265}
3266
3267/**
3268 * stand-in for when there is no HTML cache plugin enabled
3269 */
3270class _zp_HTML_cache {
3271
3272	function disable() {
3273
3274	}
3275
3276	function startHTMLCache() {
3277
3278	}
3279
3280	function abortHTMLCache() {
3281
3282	}
3283
3284	function endHTMLCache() {
3285
3286	}
3287
3288	function clearHtmlCache() {
3289
3290	}
3291
3292}
3293
3294setexifvars();
3295
3296/**
3297 * Prints the lang="" attribute for the main <html> element.
3298 *
3299 * @since ZenphotoCMS 1.5.7
3300 *
3301 * @param string $locale Default null so the current locale is used. Or a locale like "en_US" which will get the underscores replaced by hyphens to be valid
3302 */
3303function printLangAttribute($locale = null) {
3304	echo ' lang="' . getLangAttributeLocale($locale) . '"';
3305}
3306