1<?php
2
3/**
4 * functions common to both the Zenphoto core and setup's basic environment
5 *
6 * @package core
7 * @subpackage functions\functions-common
8 */
9
10/**
11 *
12 * Traps errors and insures thy are logged.
13 * @param int $errno
14 * @param string $errstr
15 * @param string $errfile
16 * @param string $errline
17 * @return void|boolean
18 */
19function zpErrorHandler($errno, $errstr = '', $errfile = '', $errline = '') {
20	// check if function has been called by an exception
21	if (func_num_args() == 5) {
22		// called by trigger_error()
23		list($errno, $errstr, $errfile, $errline) = func_get_args();
24	} else {
25		// caught exception
26		$exc = func_get_arg(0);
27		$errno = $exc->getCode();
28		$errstr = $exc->getMessage();
29		$errfile = $exc->getFile();
30		$errline = $exc->getLine();
31	}
32	// if error has been supressed with an @
33	if (error_reporting() == 0 && !in_array($errno, array(E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE))) {
34		return;
35	}
36	$errorType = array(E_ERROR				 => gettext('ERROR'),
37					E_WARNING			 => gettext('WARNING'),
38					E_NOTICE			 => gettext('NOTICE'),
39					E_USER_ERROR	 => gettext('USER ERROR'),
40					E_USER_WARNING => gettext('USER WARNING'),
41					E_USER_NOTICE	 => gettext('USER NOTICE'),
42					E_STRICT			 => gettext('STRICT NOTICE')
43	);
44
45	// create error message
46
47	if (array_key_exists($errno, $errorType)) {
48		$err = $errorType[$errno];
49	} else {
50		$err = gettext("EXCEPTION ($errno)");
51		$errno = E_ERROR;
52	}
53	$msg = sprintf(gettext('%1$s: %2$s in %3$s on line %4$s'), $err, $errstr, $errfile, $errline);
54	debugLogBacktrace($msg, 1);
55	return false;
56}
57
58/**
59 * Converts a file system filename to UTF-8 for zenphoto internal storage
60 *
61 * @param string $filename the file name to convert
62 * @return string
63 */
64function filesystemToInternal($filename) {
65	global $_zp_UTF8;
66	return str_replace('\\', '/', $_zp_UTF8->convert($filename, FILESYSTEM_CHARSET, LOCAL_CHARSET));
67}
68
69/**
70 * Converts a Zenphoto Internal filename string to one compatible with the file system
71 *
72 * @param string $filename the file name to convert
73 * @return string
74 */
75function internalToFilesystem($filename) {
76	global $_zp_UTF8;
77	return $_zp_UTF8->convert($filename, LOCAL_CHARSET, FILESYSTEM_CHARSET);
78}
79
80/**
81 * Takes user input meant to be used within a path to a file or folder and
82 * removes anything that could be insecure or malicious, or result in duplicate
83 * representations for the same physical file.
84 *
85 * This function is used primarily for album names.
86 * NOTE: The initial and trailing slashes are removed!!!
87 *
88 * Returns the sanitized path
89 *
90 * @param string $filename is the path text to filter.
91 * @return string
92 */
93function sanitize_path($filename) {
94	$filename = strip_tags(str_replace('\\', '/', $filename));
95	$filename = preg_replace(array('/x00/', '/\/\/+/', '/\/\.\./', '/\/\./', '/:/', '/</', '/>/', '/\?/', '/\*/', '/\"/', '/\|/', '/\/+$/', '/^\/+/'), '', $filename);
96	return $filename;
97}
98
99/**
100 * Checks if the input is numeric, rounds if so, otherwise returns false.
101 *
102 * @param mixed $num the number to be sanitized
103 * @return int
104 */
105function sanitize_numeric($num) {
106	if (is_numeric($num)) {
107		return round($num);
108	} else {
109		return false;
110	}
111}
112
113/**
114 * removes script tags
115 *
116 * @param string $text
117 * @return string
118 */
119function sanitize_script($text) {
120	return preg_replace('!<script.*>.*</script>!ixs', '', $text);
121}
122
123/** Make strings generally clean.  Takes an input string and cleans out
124 * null-bytes, and optionally use KSES
125 * library to prevent XSS attacks and other malicious user input.
126 * @param string $input_string is a string that needs cleaning.
127 * @param string $sanitize_level is a number between 0 and 3 that describes the
128 * type of sanitizing to perform on $input_string.
129 *   0 - Basic sanitation. Only strips null bytes. Not recommended for submitted form data.
130 *   1 - User specified. (User defined code is allowed. Used for descriptions and comments.)
131 *   2 - Text style/formatting. (Text style codes allowed. Used for titles.)
132 *   3 - Full sanitation. (Default. No code allowed. Used for text only fields)
133 * @return string the sanitized string.
134 */
135function sanitize($input_string, $sanitize_level = 3) {
136	if (is_array($input_string)) {
137		$output_string = array();
138		foreach ($input_string as $output_key => $output_value) {
139			$output_string[$output_key] = sanitize($output_value, $sanitize_level);
140		}
141	} else {
142		$output_string = sanitize_string($input_string, $sanitize_level);
143	}
144	return $output_string;
145}
146
147/**
148 * Internal "helper" function to apply the tag removal
149 *
150 * @param string $input_string
151 * @param array $allowed_tags
152 * @return string
153 */
154function ksesProcess($input_string, $allowed_tags) {
155	if (function_exists('kses')) {
156		return kses($input_string, $allowed_tags);
157	} else {
158		return getBare($input_string);
159	}
160}
161
162/**
163 * Cleans tags and some content.
164 * @param type $content
165 * @return type
166 */
167function getBare($content) {
168  $content = preg_replace('~<script.*?/script>~is', '', $content);
169  $content = preg_replace('~<style.*?/style>~is', '', $content);
170  $content = preg_replace('~<!--.*?-->~is', '', $content);
171  $content = strip_tags($content);
172  $content = str_replace('&nbsp;', ' ', $content);
173  return $content;
174}
175
176/** returns a sanitized string for the sanitize function
177 * @param string $input_string
178 * @param string $sanitize_level See sanitize()
179 * @return string the sanitized string.
180 */
181function sanitize_string($input, $sanitize_level) {
182	if (is_string($input)) {
183		$input = str_replace(chr(0), " ", $input);
184		switch ($sanitize_level) {
185			case 0:
186				return $input;
187			case 2:
188				// Strips non-style tags.
189				$input = sanitize_script($input);
190				return ksesProcess($input, getAllowedTags('style_tags'));
191			case 3:
192				// Full sanitation.  Strips all code.
193				return getBare($input);
194
195			case 1:
196				// Text formatting sanititation.
197				$input = sanitize_script($input);
198				return ksesProcess($input, getAllowedTags('allowed_tags'));
199			case 4:
200			default:
201				// for internal use to eliminate security injections
202				return sanitize_script($input);
203		}
204	}
205	return $input;
206}
207
208///// database helper functions
209
210/**
211 * Prefix a table name with a user-defined string to avoid conflicts.
212 * This MUST be used in all database queries.
213 * @param string $tablename name of the table
214 * @return prefixed table name
215 * @since 0.6
216 */
217function prefix($tablename = NULL) {
218	if(defined('DATABASE_PREFIX')) {
219		$prefix = DATABASE_PREFIX;
220	} else{
221		$prefix = 'zp_'; // use default in case this constant is not set in setup primitive environments
222	}
223	return '`' . $prefix . $tablename . '`';
224}
225
226/**
227 * Constructs a WHERE clause ("WHERE uniqueid1='uniquevalue1' AND uniqueid2='uniquevalue2' ...")
228 *  from an array (map) of variables and their values which identifies a unique record
229 *  in the database table.
230 * @param string $unique_set what to add to the WHERE clause
231 * @return contructed WHERE cleause
232 * @since 0.6
233 */
234function getWhereClause($unique_set) {
235	if (empty($unique_set))
236		return ' ';
237	$where = ' WHERE';
238	foreach ($unique_set as $var => $value) {
239		$where .= ' `' . $var . '` = ' . db_quote($value) . ' AND';
240	}
241	return substr($where, 0, -4);
242}
243
244/**
245 * Constructs a SET clause ("SET uniqueid1='uniquevalue1', uniqueid2='uniquevalue2' ...")
246 *  from an array (map) of variables and their values which identifies a unique record
247 *  in the database table. Used to 'move' records. Note: does not check anything.
248 * @param string $new_unique_set what to add to the SET clause
249 * @return contructed SET cleause
250 * @since 0.6
251 */
252function getSetClause($new_unique_set) {
253	$i = 0;
254	$set = ' SET';
255	foreach ($new_unique_set as $var => $value) {
256		$set .= ' `' . $var . '`=';
257		if (is_null($value)) {
258			$set .= 'NULL';
259		} else {
260			$set .= db_quote($value) . ',';
261		}
262	}
263	return substr($set, 0, -1);
264}
265
266/*
267 * returns the connected database name
268 */
269
270function db_name() {
271	global $_zp_conf_vars;
272	return $_zp_conf_vars['mysql_database'];
273}
274
275function db_count($table, $clause = NULL, $field = "*") {
276	$sql = 'SELECT COUNT(' . $field . ') FROM ' . prefix($table) . ' ' . $clause;
277	$result = query_single_row($sql);
278	if ($result) {
279		return array_shift($result);
280	} else {
281		return 0;
282	}
283}
284
285/**
286 * triggers an error
287 *
288 * @param string $message
289 * @param int $type the PHP error type to trigger; default to E_USER_ERROR
290 */
291function zp_error($message, $fatal = E_USER_ERROR) {
292	// Print the error message, to be convenient.
293	printf(html_encode($message));
294	trigger_error($message, $fatal);
295}
296
297function html_decode($string) {
298	return html_entity_decode($string, ENT_QUOTES, 'UTF-8');
299}
300
301/**
302 * encodes a pre-sanitized string to be used in an HTML text-only field (value, alt, title, etc.)
303 *
304 * @param string $str
305 * @return string
306 */
307function html_encode($str) {
308	return htmlspecialchars($str, ENT_FLAGS, LOCAL_CHARSET);
309}
310
311/**
312 * HTML encodes the non-metatag part of the string.
313 *
314 * @param string $original string to be encoded
315 * @param bool $allowScript set to false to prevent pass-through of script tags.
316 * @return string
317 */
318function html_encodeTagged($original, $allowScript = true) {
319	$tags = array();
320	$str = $original;
321	//javascript
322	if ($allowScript) {
323		preg_match_all('!<script.*>.*</script>!ixs', $str, $matches);
324		foreach (array_unique($matches[0]) as $key => $tag) {
325			$tags[2]['%' . $key . '$j'] = $tag;
326			$str = str_replace($tag, '%' . $key . '$j', $str);
327		}
328	} else {
329		$str = preg_replace('|<a(.*)href(.*)=(.*)javascript|ixs', '%$x', $str);
330		$tags[2]['%$x'] = '&lt;a href=<strike>javascript</strike>';
331		$str = preg_replace('|<(.*)onclick|ixs', '%$c', $str);
332		$tags[2]['%$c'] = '&lt;<strike>onclick</strike>';
333	}
334	//strip html comments
335	$str = preg_replace('~<!--.*?-->~is', '', $str);
336	// markup
337	preg_match_all("/<\/?\w+((\s+(\w|\w[\w-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)\/?>/i", $str, $matches);
338	foreach (array_unique($matches[0]) as $key => $tag) {
339		$tags[2]['%' . $key . '$s'] = $tag;
340		$str = str_replace($tag, '%' . $key . '$s', $str);
341	}
342	$str = htmLawed($str);
343	//entities
344	preg_match_all('/(&[a-z0-9#]+;)/i', $str, $matches);
345	foreach (array_unique($matches[0]) as $key => $entity) {
346		$tags[3]['%' . $key . '$e'] = $entity;
347		$str = str_replace($entity, '%' . $key . '$e', $str);
348	}
349	$str = htmlspecialchars($str, ENT_FLAGS, LOCAL_CHARSET);
350	foreach (array_reverse($tags, true) as $taglist) {
351		$str = strtr($str, $taglist);
352	}
353	if ($str != $original) {
354		$str = tidyHTML($str);
355	}
356	return $str;
357}
358
359/**
360 * Convenience wrapper of html_encode(pathurlencode($url))
361 * Primarily intended for use with img src URLs
362 *
363 * @since ZenphotoCMS 1.5.8
364 *
365 * @param string $url
366 * @return string
367 */
368function html_pathurlencode($url) {
369	return html_encode(pathurlencode($url));
370}
371
372/**
373 * Makes directory recursively, returns TRUE if exists or was created sucessfuly.
374 * Note: PHP5 includes a recursive parameter to mkdir, but it apparently does not
375 * 				does not traverse symlinks!
376 * @param string $pathname The directory path to be created.
377 * @return boolean TRUE if exists or made or FALSE on failure.
378 */
379function mkdir_recursive($pathname, $mode) {
380	if (!is_dir(dirname($pathname))) {
381		mkdir_recursive(dirname($pathname), $mode);
382	}
383	return is_dir($pathname) || @mkdir($pathname, $mode);
384}
385
386/**
387 * Logs the calling stack
388 *
389 * @param string $message Message to prefix the backtrace
390 */
391function debugLogBacktrace($message, $omit = 0) {
392	$output = trim($message) . "\n";
393	// Get a backtrace.
394	$bt = debug_backtrace();
395	while ($omit >= 0) {
396		array_shift($bt); // Get rid of debug_backtrace, callers in the backtrace.
397		$omit--;
398	}
399	$prefix = '  ';
400	$line = '';
401	$caller = '';
402	foreach ($bt as $b) {
403		$caller = (isset($b['class']) ? $b['class'] : '') . (isset($b['type']) ? $b['type'] : '') . $b['function'];
404		if (!empty($line)) { // skip first output to match up functions with line where they are used.
405			$prefix .= '  ';
406			$output .= 'from ' . $caller . ' (' . $line . ")\n" . $prefix;
407		} else {
408			$output .= '  ' . $caller . " called ";
409		}
410		$date = false;
411		if (isset($b['file']) && isset($b['line'])) {
412			$line = basename($b['file']) . ' [' . $b['line'] . "]";
413		} else {
414			$line = 'unknown';
415		}
416	}
417	if (!empty($line)) {
418		$output .= 'from ' . $line;
419	}
420	debugLog($output);
421}
422
423/**
424 * Records a Var to the debug log
425 *
426 * @param string $message message to insert in log [optional]
427 * @param mixed $var the variable to record
428 */
429function debugLogVar($message) {
430	$args = func_get_args();
431	if (count($args) == 1) {
432		$var = $message;
433		$message = '';
434	} else {
435		$message .= ' ';
436		$var = $args[1];
437	}
438	ob_start();
439	var_dump($var);
440	$str = ob_get_contents();
441	ob_end_clean();
442	debugLog(trim($message) . "\r" . html_decode(getBare($str)));
443}
444
445/**
446 * Returns the value of a cookie from either the cookies or from $_SESSION[]
447 *
448 * @param string $name the name of the cookie
449 */
450function zp_getCookie($name) {
451  if (isset($_COOKIE[$name])) {
452    $cookiev = sanitize($_COOKIE[$name]);
453  } else {
454    $cookiev = '';
455  }
456  if (DEBUG_LOGIN) {
457    if (isset($_SESSION[$name])) {
458      $sessionv = sanitize($_SESSION[$name]);
459    } else {
460      $sessionv = '';
461    }
462    debugLog(zp_getCookie($name) . '=::' . 'album_session=' . GALLERY_SESSION . "; SESSION[" . session_id() . "]=" . sanitize($sessionv) . ", COOKIE=" . sanitize($cookiev));
463  }
464  if (!empty($cookiev) && (defined('GALLERY_SESSION') && !GALLERY_SESSION)) {
465    return zp_cookieEncode($cookiev);
466  }
467  if (isset($_SESSION[$name])) {
468    return sanitize($_SESSION[$name]);
469  }
470  return NULL;
471}
472
473/**
474 *
475 * Encodes a cookie value tying it to the user IP
476 * @param $value
477 */
478function zp_cookieEncode($value) {
479	if (IP_TIED_COOKIES) {
480		return rc4(getUserIP() . HASH_SEED, $value);
481	} else {
482		return $value;
483	}
484}
485
486/**
487 * Sets a cookie both in the browser cookies and in $_SESSION[]
488 *
489 * @param string $name The 'cookie' name
490 * @param string $value The value to be stored
491 * @param timestamp $time The time delta until the cookie expires
492 * @param string $path The path on the server in which the cookie will be available on
493 * @param bool $secure true if secure cookie
494 * @param bool $httponly true if access to this cookie should only be allowed via http (e.g. no access to JS etc.). Requires browser support though.
495 */
496function zp_setCookie($name, $value, $time = NULL, $path = NULL, $secure = false, $httponly = false) {
497  if (empty($value)) {
498    $cookiev = '';
499  } else {
500    $cookiev = zp_cookieEncode(sanitize($value));
501  }
502  if (is_null($time)) {
503    $time = COOKIE_PERSISTENCE;
504  }
505  if (is_null($path)) {
506    $path = WEBPATH;
507  }
508  if (substr($path, -1, 1) != '/')
509    $path .= '/';
510  if (DEBUG_LOGIN) {
511    debugLog("zp_setCookie($name, $value, $time, $path)::album_session=" . GALLERY_SESSION . "; SESSION=" . session_id());
512  }
513  if (($time < 0) || !GALLERY_SESSION) {
514		if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
515			$options = array(
516					'expires' => (time() + $time),
517					'path' => $path,
518					'secure' => $secure,
519					'httponly' => $httponly,
520					'samesite' => 'Lax'
521			);
522			setcookie($name, $cookiev, $options);
523		} else {
524			setcookie($name, $cookiev, time() + $time, $path, '', $secure, $httponly);
525		}
526	}
527	if ($time < 0) {
528    if (isset($_SESSION))
529      unset($_SESSION[$name]);
530    if (isset($_COOKIE))
531      unset($_COOKIE[$name]);
532  } else {
533    $_SESSION[$name] = sanitize($value);
534    $_COOKIE[$name] = sanitize($cookiev);
535  }
536}
537
538/**
539 * Clears a cookie
540 * @param string $name The 'cookie' name
541 * @param string $path The path on the server in which the cookie will be available on
542 * @param bool $secure true if secure cookie
543 * @param bool $httponly true if access to this cookie should only be allowed via http (e.g. no access to JS etc.). Requires browser support though.
544 */
545function zp_clearCookie($name, $path = NULl, $secure = false, $httponly = false) {
546	zp_setCookie($name, '', -368000, $path, $secure, $httponly);
547}
548
549/**
550 * if $string is an serialzied array it is unserialized otherwise an appropriate array is returned
551 *
552 * @param string $string
553 *
554 * @return array
555 */
556function getSerializedArray($string) {
557	if (is_array($string)) {
558		return $string;
559	}
560	if (preg_match('/^a:[0-9]+:{/', $string)) {
561		$r = @unserialize($string);
562		if ($r) {
563			return $r;
564		} else {
565			return array();
566		}
567	} else if (strlen($string) == 0 && !is_bool($string)) {
568		return array();
569	} else {
570		return array($string);
571	}
572}
573
574?>
575