<?php

	// set default values for newer constants that might not be defined in older constants.php files
	if (!defined("SITE_IMAGE_DIRECTORIES")) { define($ns."SITE_IMAGE_DIRECTORIES", SITE_SUBDIRECTORY."/images"); }
	if (!defined("MULTIPLE_SPACES")) { define($ns."MULTIPLE_SPACES", 1); }
	if (!defined("COLLAPSABLE_GROUPS")) { define($ns."COLLAPSABLE_GROUPS", 1); }
	if (!defined("CONTENT_FILE_EXTENSIONS")) { define($ns."CONTENT_FILE_EXTENSIONS", "html,htm"); }
	if (!defined("DREAMWEAVER_CONTENT_SUBDIRECTORIES")) { define($ns."DREAMWEAVER_CONTENT_SUBDIRECTORIES", 1); }


	function require_user($user) {
	
		// make sure we have a user ID in the session; if not, redirect to the login page
	
		if ($user == "") {
			GLOBAL $login_page;
			redirect($login_page);
		}
	}


	function require_role($role, $required_role) {
	
		// make sure we have the correct in the session; if not, set feedback accordingly and redirect to the previous page
	
		if ($role < $required_role) {
			set_feedback(0, "Your current role does not allow you to perform that action.");
			redirect();
		}
	}


	function initialize($variable_name, $input_method, $sanitize_type, $maximum_size=32000) {
	
		// $variable_name = the name of the variable to initialize
		// $input_method = get, post, or session
		// $sanitize_type = none, integer, decimal, date_fields, date, time_fields, time, text, html, filename, or sql
		
		// requires send_email and get_URL functions
		
		// 09/10/04 - Initial development; request access only
		// 10/03/04 - Added sanitization routines
		// 10/17/04 - Fixed a bug that prevented the sanitization routines from actually running; included input value in alert message
		// 01/11/05 - Tweaked text sanitization so that it only modifes <.*>, not just < or >; improved formatting of alert message; saved integer conversion until the end of the script to avoid type mismatches
		// 12/22/05 - Changed integer checking to use a regular expression rather than the intval function so that leading zeroes don't trigger an alert message
		// 01/13/06 - Moved final integer type change to before email alert so email alert is accurate
		// 05/19/06 - Added the ability to initialize session variables, mainly for the convenience of avoiding unset variables, although this does add another level of security
		// 05/26/06 - Moved addslashes to after string comparison to avoid triggering sanitization alerts
		// 05/29/06 - Tweaked previous change to support strings or arrays
		// 08/19/06 - Fixed support for integers in sessions by forcing session variables to strings for comparison, and fixed support for arrays of integers by forcing each element of the array, not the entire array, to an integer
		// 02/05/07 - Changed webmaster email variable from a global to a constant
		// 05/11/07 - Changed array conversion to support associative arrays; added array support to integer type; skipped all but the initial value set if sanitize type is "none"
		// 05/21/08 - Moved string forcing of session variables before the original value is saved, to avoid false positives on session integers
		// 06/26/09 - Allowed skipping the maximum size check by setting that argument to 0
		// 07/24/09 - Changed $message to $body internally (which has no effect on the code calling this function)
		// 10/21/09 - Removed flattening of session arrays to strings -- that wasn't necessary
		// 06/21/10 - Imploded array values in notification message so we can get more info in the case of a mismatch
		// 03/03/11 - Added decimal sanitize type
		// 03/08/11 - Added date and date_fields sanitize types
		// 03/08/11 - Added time and time_fields sanitize types
		// 03/30/11 - Removed strict type comparison of sanitized value to avoid false positives with session variables
		// 11/14/11 - Added the user agent and IP address to the notification message
		// 01/18/12 - Added the referrer to the notification message
		// 01/18/12 - Changed all < > characters to HTML entities in the text sanitization type, rather than just matching pairs of characters
		// 06/29/12 - Added duration_fields sanitize type
		
		// NOTE: this version is customized to use the "editor" session variable

		$alert_email = WEBMASTER_EMAIL;
		
		// allow developers to use mixed case for these
		$input_method = strtoupper($input_method);
		$sanitize_type = strtolower($sanitize_type);
		
		// get the value from the requested global array, or set it to "" if it doesn't exist there
		// (apparently we can't access the global variable dynamically: ${"_".strtoupper($input_method)."[\"".$variable_name."\"]"} )
		// if we're requesting the date_fields sanitize type, group the date fields together into one field, then proceed with the date sanitize type
		switch ($input_method) {
		
			case "GET":
		
				if (($sanitize_type == "date_fields")&&(isset($_GET[$variable_name."_year"]))&&(isset($_GET[$variable_name."_month"]))&&(isset($_GET[$variable_name."_day"]))) {
					$variable_value = combine_date_elements($_GET[$variable_name."_year"], $_GET[$variable_name."_month"], $_GET[$variable_name."_day"]);
					$sanitize_type = "date";
				} else if (($sanitize_type == "time_fields")&&(isset($_GET[$variable_name."_hours"]))&&(isset($_GET[$variable_name."_minutes"]))&&(isset($_GET[$variable_name."_seconds"]))&&(isset($_GET[$variable_name."_ampm"]))) {
					$variable_value = combine_time_elements($_GET[$variable_name."_hours"], $_GET[$variable_name."_minutes"], $_GET[$variable_name."_seconds"], $_GET[$variable_name."_ampm"]);
					$sanitize_type = "time";
				} else if (($sanitize_type == "duration_fields")&&(isset($_GET[$variable_name."_minutes"]))&&((isset($_GET[$variable_name."_hours"]))||(isset($_GET[$variable_name."_seconds"])))) {
					$hours = (isset($_GET[$variable_name."_hours"])) ? $_GET[$variable_name."_hours"] : 0 ;
					$minutes = $_GET[$variable_name."_minutes"];
					$seconds = (isset($_GET[$variable_name."_seconds"])) ? $_GET[$variable_name."_seconds"] : 0 ;
					$variable_value = combine_duration_elements($hours, $minutes, $seconds);
					$sanitize_type = "integer";
				} else {
					if (isset($_GET[$variable_name])) {
						$variable_value = $_GET[$variable_name];
					} else {
						$variable_value = "";
					}
				}
				break;
	
			case "POST":
		
				if (($sanitize_type == "date_fields")&&(isset($_POST[$variable_name."_year"]))&&(isset($_POST[$variable_name."_month"]))&&(isset($_POST[$variable_name."_day"]))) {
					$variable_value = combine_date_elements($_POST[$variable_name."_year"], $_POST[$variable_name."_month"], $_POST[$variable_name."_day"]);
					$sanitize_type = "date";
				} else if (($sanitize_type == "time_fields")&&(isset($_POST[$variable_name."_hours"]))&&(isset($_POST[$variable_name."_minutes"]))&&(isset($_POST[$variable_name."_seconds"]))&&(isset($_POST[$variable_name."_ampm"]))) {
					$variable_value = combine_time_elements($_POST[$variable_name."_hours"], $_POST[$variable_name."_minutes"], $_POST[$variable_name."_seconds"], $_POST[$variable_name."_ampm"]);
					$sanitize_type = "time";
				} else if (($sanitize_type == "duration_fields")&&(isset($_POST[$variable_name."_minutes"]))&&((isset($_POST[$variable_name."_hours"]))||(isset($_POST[$variable_name."_seconds"])))) {
					$hours = (isset($_POST[$variable_name."_hours"])) ? $_POST[$variable_name."_hours"] : 0 ;
					$minutes = $_POST[$variable_name."_minutes"];
					$seconds = (isset($_POST[$variable_name."_seconds"])) ? $_POST[$variable_name."_seconds"] : 0 ;
					$variable_value = combine_duration_elements($hours, $minutes, $seconds);
					$sanitize_type = "integer";
				} else {
					if (isset($_POST[$variable_name])) {
						$variable_value = $_POST[$variable_name];
					} else {
						$variable_value = "";
					}
				}
				break;

			case "SESSION":
		
				if (($sanitize_type == "date_fields")&&(isset($_SESSION["editor"][$variable_name."_year"]))&&(isset($_SESSION["editor"][$variable_name."_month"]))&&(isset($_SESSION["editor"][$variable_name."_day"]))) {
					$variable_value = combine_date_elements($_SESSION["editor"][$variable_name."_year"], $_SESSION["editor"][$variable_name."_month"], $_SESSION["editor"][$variable_name."_day"]);
					$sanitize_type = "date";
				} else if (($sanitize_type == "time_fields")&&(isset($_SESSION["editor"][$variable_name."_hours"]))&&(isset($_SESSION["editor"][$variable_name."_minutes"]))&&(isset($_SESSION["editor"][$variable_name."_seconds"]))&&(isset($_SESSION["editor"][$variable_name."_ampm"]))) {
					$variable_value = combine_time_elements($_SESSION["editor"][$variable_name."_hours"], $_SESSION["editor"][$variable_name."_minutes"], $_SESSION["editor"][$variable_name."_seconds"], $_SESSION["editor"][$variable_name."_ampm"]);
					$sanitize_type = "time";
				} else if (($sanitize_type == "duration_fields")&&(isset($_SESSION["editor"][$variable_name."_minutes"]))&&((isset($_SESSION["editor"][$variable_name."_hours"]))||(isset($_SESSION["editor"][$variable_name."_seconds"])))) {
					$hours = (isset($_SESSION["editor"][$variable_name."_hours"])) ? $_SESSION["editor"][$variable_name."_hours"] : 0 ;
					$minutes = $_SESSION["editor"][$variable_name."_minutes"];
					$seconds = (isset($_SESSION["editor"][$variable_name."_seconds"])) ? $_SESSION["editor"][$variable_name."_seconds"] : 0 ;
					$variable_value = combine_duration_elements($hours, $minutes, $seconds);
					$sanitize_type = "integer";
				} else {
					if (isset($_SESSION["editor"][$variable_name])) {
						$variable_value = $_SESSION["editor"][$variable_name];
					} else {
						$variable_value = "";
					}
				}
				break;

		}
		
		if ($sanitize_type != "none") {


			// save the variable value before sanitizing so we can see if we ran into anything suspicious
			$variable_value_original = $variable_value;
			
			// in order to handle arrays seamlessly, we'll treat every variable as an array, then process every element
			if (is_array($variable_value)) {
				$is_really_array = 1;
			} else {
				$variable_value = Array($variable_value);
				$is_really_array = 0;
			}
			
			foreach ($variable_value as $key => $value) {
			
				// truncate the value if it's too large
				if ($maximum_size) {
					if ($variable_value[$key] != "") { // the substr function returns 0 if the string is empty, which makes the comparison too complicated
						$variable_value[$key] = substr($variable_value[$key], 0, $maximum_size);
					}
				}
						
				// sanitize the value
				switch ($sanitize_type) {
				
					case "integer":
					
						// remove all but numeric characters to prevent SQL injection attacks
						// but check first for a negative sign to support negative integers
						$negative_sign = (substr($variable_value[$key], 0, 1) == "-") ? "-" : "" ;
						$variable_value[$key] = preg_replace("/\D/", "", $variable_value[$key]);
						$variable_value[$key] = $negative_sign.$variable_value[$key];
						break;
						
					case "decimal":
					
						// remove all but numeric characters and decimal points to prevent SQL injection attacks
						// but check first for a negative sign to support negative decimals
						$negative_sign = (substr($variable_value[$key], 0, 1) == "-") ? "-" : "" ;
						$variable_value[$key] = preg_replace("/[^\d\.,]/", "", $variable_value[$key]);
						$variable_value[$key] = $negative_sign.$variable_value[$key];
						break;
						
					case "date":
					
						// remove all but YYYY-MM-DD characters to prevent SQL injection attacks
						$variable_value[$key] = preg_replace("/(\d{4}-\d{2}-\d{2})/", "$1", $variable_value[$key]);
						break;
						
					case "time":
					
						// remove all but 00:00:00 characters to prevent SQL injection attacks
						$variable_value[$key] = preg_replace("/(\d{2}:\d{2}:\d{2})/", "$1", $variable_value[$key]);
						break;
						
					case "text":
					
						// change < and > to HTML entities to prevent cross-site scripting attacks
						//$variable_value[$key] = preg_replace("/<([^>]*)>/", "&lt;$1&gt;", $variable_value[$key]);
						$variable_value[$key] = str_replace("<", "&lt;", $variable_value[$key]);
						$variable_value[$key] = str_replace(">", "&gt;", $variable_value[$key]);
						break;
						
					case "html":
					
						break;
						
					case "filename":
					
						// strip special characters and directory commands to prevent command execution, directory traversing, and references to remote files
						$variable_value[$key] = preg_replace("/[^\w-\.\/\?\&\=\%]/", "", $variable_value[$key]);
						$variable_value[$key] = preg_replace("/\.\.\//", "", $variable_value[$key]);
						$variable_value[$key] = preg_replace("/:\/\//", "", $variable_value[$key]);
						break;
						
					case "sql":
					
						// remove all but alphanumeric characters to prevent SQL injection attacks
						$variable_value[$key] = preg_replace("/\W/", "", $variable_value[$key]);
						break;
						
				}
			
			}
			
			// convert variables that weren't arrays to begin with back to strings or integers
			if (!$is_really_array) {
				$variable_value = $variable_value[0];
			}
			
			// compare the new value to the original and if it's different, send an alert
			// we don't want a strict comparisons here because we might set a session with a number value and that would trigger a false mismatch with the sanitized value
			// everything coming from the get or post arrays will be a string anyway so this shouldn't have any disadvantages
			$send_notification = ($variable_value != $variable_value_original) ? 1 : 0 ;
			
			// make sure quotes are escaped to prevent SQL injection attacks
			// this isn't needed as long as we're using db_normalize_value when building queries
			///*
			if ($is_really_array) {
				foreach ($variable_value as $key => $value) {
					$variable_value[$key] = addslashes(stripslashes($variable_value[$key]));
				}
			} else {
				$variable_value = addslashes(stripslashes($variable_value));
			}
			//*/
	
			// change the value to a number rather than a string for the integer or decimal sanitization type
			if (($sanitize_type == "integer")||($sanitize_type == "decimal")) {
				if ($is_really_array) {
					foreach ($variable_value as $key => $value) {
						$variable_value[$key] = ($sanitize_type == "integer") ? intval($variable_value[$key]) : floatval($variable_value[$key]) ;
					}
				} else {
					$variable_value = ($sanitize_type == "integer") ? intval($variable_value) : floatval($variable_value) ;
				}
			}
	
			if ($send_notification) {
				$subject = "Sanitization alert";
				$body = "Some user-submitted data has failed sanitization. We've modified the data to make it safe, but please review this incident to determine whether it shows malicious intent.\n\n";
				// we could make this message easier to read by using HTML formatting (bold headings), but then any HTML we passed in the value fields would render, which we don't want
				if (is_array($variable_value_original)) { $variable_value_original = implode_assoc("=", ", ", $variable_value_original); }
				if (is_array($variable_value)) { $variable_value = implode_assoc("=", ", ", $variable_value); }
				$body .= "
Script:
".get_URL()."

Input method:
$input_method

Variable name:
$variable_name

Attempted value:
$variable_value_original

Modified value:
$variable_value

Sanitization type:
$sanitize_type

IP address:
".$_SERVER["REMOTE_ADDR"]."

User agent:
".$_SERVER["HTTP_USER_AGENT"]."

Referrer:
".$_SERVER["HTTP_REFERER"]."
";
				send_email($alert_email, "", $alert_email, "", $alert_email, $subject, $body, 0); 
			}
		
		}

		return $variable_value;
	
	}


	function unregister_globals() {
		// thanks to http://us.php.net/manual/en/security.globals.php
		if (ini_get("register_globals")) {
			$sources = array("_REQUEST", "_SESSION", "_SERVER", "_ENV", "_FILES");
			foreach ($sources as $source) {
				foreach ($GLOBALS[$source] as $key => $value) {
					if ((isset($GLOBALS[$key])&&($GLOBALS[$key] === $value))) {
						unset($GLOBALS[$key]);
					}
				}
			}
		}
	}
	
	
	function implode_assoc($inner_glue, $outer_glue, $array) {
		$output = array();
		if (count($array) > 0) {
			// thanks to http://www.php.net/manual/en/function.implode.php
			foreach($array as $key => $value) {
				$output[] = $key.$inner_glue.$value;
			}
			$output = implode($outer_glue, $output);
		} else {
			$output = "";
		}
		return $output;
	}
	
	
	function explode_assoc($inner_glue, $outer_glue, $string) {
		$output = array();
		if ($string != "") {
			$array = explode($outer_glue, $string);
			foreach($array as $pair) {
				if (strpos($pair, $inner_glue) !== false) {
					list($key, $value) = explode($inner_glue, $pair);
					if ($key) {
						$output["$key"] = $value;
					}
				}
			}
		}
		return $output;
	}
	
	
	function ajax_serialize($input) {
		// convert an associative array into an encoded string for posting via an HTTP request
		$output = array();
		foreach ($input as $key=>$value) {
			$output[] = $key."=".rawurlencode($value);
		}
		$output = implode("&", $output);
		return $output;
	}
	

	function ajax_unserialize($input) {
		// convert an encoded string into an associative array for retrieving from an HTTP response
		$input = explode("&", $input);
		$output = array();
		for ($n=0; $n < count($input); $n++) {
			$pair = explode("=", $input[$n]);
			$key = $pair[0];
			$value = rawurldecode($pair[1]);
			$output[$key] = $value;
		}
		return $output;
	}
	
	
    function set_feedback($success, $text) {
        $text = ($success) ? $text : "ERROR: $text" ; // we don't need this in this site if the feedback styles have icons in them
        $style = ($success) ? "feedback_positive" : "feedback_negative" ;
        $html = "<p class=\"$style\">$text</p>\n\n";
        
        $feedback = stripslashes(initialize("feedback", "session", "html"));
		$_SESSION["editor"]["feedback"] = $feedback.$html;
    }
    
    
    function get_feedback() {
        $feedback = stripslashes(initialize("feedback", "session", "html"));
        $_SESSION["editor"]["feedback"] = "";
        return $feedback;
    }


    function check_feedback() {
        $feedback = stripslashes(initialize("feedback", "session", "html"));
        if ($feedback) {
        	if (strpos($feedback, "feedback_negative") === false) {
        		$result = "positive";
        	} else {
        		$result = "negative";
        	}
        } else {
        	$result = "";
        }
        return $result;
    }
    
    
    function show_feedback_this_page() {
    	// reload the page to show the feedback we've set, but use a flag to avoid an infinite loop
    	if (isset($_SESSION["editor"]["show_feedback_redirect"])) {
    		clear_feedback(); // this prevents showing the feedback again on the next page
    		unset($_SESSION["editor"]["show_feedback_redirect"]);
    		return;
    	}
    	
    	if (check_feedback()) {
			$_SESSION["editor"]["show_feedback_redirect"] = 1;
			redirect($_SERVER["REQUEST_URI"]);
		}
    }


    function clear_feedback() {
        $_SESSION["editor"]["feedback"] = "";
    }


	function get_value($table, $input, $output, $key, $active=0) {
	
		// returns the value of a single table cell
		// 01/26/03 - added optional active argument; if true, will only return a value if its row is active
		// 02/16/03 - adjusted to use db.php
		// 06/05/03 - adjusted to use MYSQL_ASSOC
		// 06/09/09 - add addslashes() to $key to support apostrophes without magic quotes
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
	
		$key = addslashes($key);
		$query = "SELECT `$output` FROM `$table` WHERE `$input`='$key'";
		if ($active) { $query .= " AND active=1"; }
		$result = db_do_query($query);
		if ($result != 0) {
			$row = db_get_row($result, 0);
			return $row[$output];
		} else {
			return "";
		}
		
	}
	
	
	function set_value($table, $field, $value, $key_field, $key) {
	
		// sets the value of a single table cell
		
		$value = addslashes($value);
		$key = addslashes($key);
	
		$query = "UPDATE `$table` SET `$field`='$value' WHERE `$key_field`='$key'";
		$result = db_do_query($query);
		return $result;
		
	}
	
	
	function get_row_count($table) {
	
		// returns the number of rows in a table
		// we might not need this since input and key are now optional in get_value_count
	
		$result = db_do_query("SELECT ID FROM `$table`");
		if ($result != 0) {
			return db_num_rows($result);
		} else {
			return "";
		}
		
	}
	
	
	function get_value_count($table, $input, $key) {
	
		// returns the number of rows that match the value
	
		// 08/26/03 - made input and key optional by adding the criteria check
		// 06/09/09 - add addslashes() to $key to support apostrophes without magic quotes
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words

		$key = addslashes($key);
		$criteria = ((!$input)||(!$key)) ? "ID>0" : "`$input`='$key'" ;

		$result = db_do_query("SELECT ID FROM `$table` WHERE $criteria");
		if ($result != 0) {
			return db_num_rows($result);
		} else {
			return "";
		}
		
	}
	
	
	function get_value_total($table, $input, $output, $key) {
	
		// returns the total of all the values in a single table column, assuming they are numbers
	
		$key = addslashes($key);
		$criteria = ((!$input)||(!$key)) ? "1=1" : "`$input`='$key'" ;
		$result = db_do_query("SELECT SUM(`$output`) AS value_total FROM `$table` WHERE $criteria");
		$row = db_get_row($result, 0);
		$value_total = $row["value_total"];
		return $value_total;
		
	}
	
	
	function get_value_list($table, $input, $output, $key, $delimiter, $sort="", $inverse=0) {
	
		// returns the value of several cells in a single table column, separated by $delimiter
		
		// 06/17/03 - reversed order of instr parameters (BTW, if using postgres you'd need to change that function to strpos)
		// 08/26/03 - made input and key optional by adding the criteria check
		// 11/22/03 - ordered results by ID, do an exact match rather than a list intersect
		// 05/20/08 - added sort argument, returned to list intersect using FIND_IN_SET function
		// 12/15/08 - added inverse argument to match one value against multiple values in a record, rather than multiple values against one value in a record
		// 06/09/09 - add addslashes() to $value to support apostrophes without magic quotes
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 07/28/09 - tweaked backticks on sort values in case $sort is something like "date DESC"; changed default sorts to $input and $output rather than hard-coded ID and name
		// 08/29/09 - added a backup sort value in case $sort and $input are both blank

		$key = addslashes($key);
		$delimiter = ($delimiter) ? $delimiter : ", ";
		$match = ($inverse) ? "FIND_IN_SET('$key', `$input`)" : "FIND_IN_SET(`$input`, '$key')" ;
		$criteria = ((!$input)||(!$key)) ? "ID>0" : $match ;
	
		if ($sort) {
			$sort_value = ($sort == 1) ? $output : $sort ;
		} else {
			if ($input) {
				$sort_value = $input;
			} else {
				$sort_value = "ID";
			}
		}
		$sort_value = preg_replace("/(\w+)(.*)/", "`$1`$2", $sort_value); // add backticks to first word of sort value
		$sort = "ORDER by $sort_value" ;
	
		$result = db_do_query("SELECT `$output` FROM `$table` WHERE $criteria $sort");
		$count = ($result != 0) ? db_num_rows($result) : 0 ;
	
		$value_list = "";
		for ($i=0; $i < $count; $i++) {
			$row = db_get_row($result, $i);
			$this_value = $row[$output];
			$value_list .= $this_value.$delimiter;
		}
		$value_list = substr($value_list, 0, -(strlen($delimiter)));
		
		return $value_list;
		
	}
	
	
	function make_date_menus($label, $form, $field, $class="", $selected="") {
	
		// creates a set of popup menus, which makes it easier to capture SQL-standard dates
		// this function requires date_month, date_day, and date_year tables in your database, which you can quickly add by importing the contents of date_tables.sql
		// it also requires the select_date() JavaScript function, from shared.js
		// you can also add the date collection routine from tidbits.php to your submit script
	
		// $label - a small text label that appears above the menus
		// $form - the name of the HTML form that these menus appear on
		// $field - the name of the database field that this date will be submitted into
		// $class - set the CSS class of the menu; "" disables this option
		// $selected - an SQL-standard date to set the menus to initially; "" disables this option
	
		// 02/16/03 - doesn't try to set if the default date is $no_date (useful in postgresql); we now return a value containing the menu, rather than printing the menu directly
		// 06/23/03 - renamed selectDate to select_date
		// 06/25/03 - renamed default_date to selected and added a conditional to set blank selected values
		// 09/09/03 - replaced font tags with body style
		// 09/11/03 - set a minimum and maximum $no_date, for sorting in two directions
		// 03/02/04 - added a Clear button to the top of the menus
		// 11/14/07 - added quotes around HTML attributes
		// 10/14/08 - reworked to use make_menu_simple instead of make_menu, so that we no longer need the date tables in the database
		// 11/19/08 - if there is no label, don't show the shortcut buttons, either
		// 07/22/09 - padded month and day values to two digits; removed global variables and just made settings local; avoided using a table if there is no label
		// 07/24/09 - compiled elements using an output array rather than a string
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 08/04/09 - added class argument to pass through to the make_menu_simple function
		// 08/10/09 - replaced table layout with span layout to allow inline placement
		// 01/21/10 - added an $end_year variable in addition to $start_year
		
		$no_date_min = "0000-00-00";
		$no_date_max = "9999-12-31";
		$start_year = 2009;
		$end_year = date("Y") + 6;
		$month_names = Array("01"=>"Jan", "02"=>"Feb", "03"=>"Mar", "04"=>"Apr", "05"=>"May", "06"=>"Jun", "07"=>"Jul", "08"=>"Aug", "09"=>"Sep", "10"=>"Oct", "11"=>"Nov", "12"=>"Dec");
		
		if (($selected == $no_date_min)||($selected == $no_date_max)) { $selected = ""; }
		
		if ($selected) {
			$temp = explode("-", $selected);
			$selected_year = $temp[0];
			$selected_month = $temp[1];
			$selected_day = $temp[2];
		} else {
			$selected_year = "";
			$selected_month = "";
			$selected_day = "";
		}
	
		$output = array();
		$output[] = "<span style=\"position: relative;\">";
		if ($label) {
			$output[] = "<span style=\"margin-left: 2px;\">$label</span>";
			$output[] = "<span style=\"position: absolute; right: 6px; top: 0px;\">";
			$output[] = "	<a href=\"JavaScript:select_date('$form.$field','')\">Today</a>&nbsp;";
			$output[] = "	<a href=\"JavaScript:select_date('$form.$field','-1')\">Clear</a>";
			$output[] = "</span>";
			$output[] = "<br />";
		}
		$month_menu = make_menu_simple("Month...", $field."_month", 1, 12, 1, 2, 0, $class, $selected_month);
		//$month_menu = preg_replace("/>(\d+)</", ">".$month_names["$1"]."<", $month_menu); // change month numbers to month names -- too bad this doesn't work; it seems that the variable index is read before the preg replacement is performed
		foreach ($month_names as $month=>$name) {
			$month_menu = str_replace(">$month<", ">$name<", $month_menu); // change month numbers to month names
		}
		$output[] = $month_menu;
		$output[] = make_menu_simple("Day...", $field."_day", 1, 31, 1, 2, 0, $class, $selected_day);
		$output[] = make_menu_simple("Year...", $field."_year", $start_year, $end_year, 1, 0, 0, $class, $selected_year);
		$output[] = "</span>";
		$output = implode("\n", $output);
		
		return $output;
	
	}
	
	
	function combine_date_elements($year, $month, $day) {
	
		// helps us collect the values submitted by the date menus for insertion into a single database field
		// if any of the individual menus are unset, we'll put the empty date value into the database
	
		if (($year)&&($month)&&($day)) {
			$date = "$year-$month-$day";
		} else {
			$date = "0000-00-00";
		}
		
		return $date;
	
	}
	
	
	function make_time_menus($label, $form, $field, $increment, $class="", $selected="") {
	
		// creates a set of popup menus, which makes it easier to capture SQL-standard times
		// this function requires the select_time() JavaScript function, from shared.js
		// you can also add the time collection routine from tidbits.php to your submit script
	
		// $label - a small text label that appears above the menus
		// $form - the name of the HTML form that these menus appear on
		// $field - the name of the database field that this time will be submitted into
		// $increment - increment to be used between minutes
		// $class - set the CSS class of the menu; "" disables this option
		// $selected - an SQL-standard time to set the menus to initially; "" disables this option
	
		// 06/24/03 - initial development
		// 06/25/03 - renamed default_time to selected and added a conditional to set blank selected values
		// 09/09/03 - replaced font tags with body style
		// 07/26/04 - added increment
		// 09/02/04 - added class=form to the am/pm menu
		// 11/14/07 - added quotes around HTML attributes
		// 12/07/07 - removed "if $selected = '00:00:00'" so we could load midnight selections (but now we have no way to show as unset)
		// 06/04/08 - added missing $increment value to function declaration
		// 11/19/08 - if there is no label, don't show the shortcut buttons, either; added Clear button to match make_date_menus
		// 07/22/09 - changed default value from -1 to 0; avoided using a table if there is no label
		// 07/24/09 - compiled elements using an output array rather than a string
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 08/04/09 - added class argument to pass through to the make_menu_simple function
		// 08/10/09 - replaced table layout with span layout to allow inline placement
		// 09/15/09 - added class tag to am/pm menu
		
		$use_seconds = 0;
		
		if (!$increment) { $increment == 1; }
		
		if ($selected) {
			$temp = explode(":", $selected);
			$selected_hours = $temp[0];
			$selected_minutes = $temp[1];
			if ($use_seconds) {
				$selected_seconds = $temp[2];
			}
			if ($selected_hours >= 12) {
				$selected_ampm = "pm";
				if ($selected_hours > 12) {
					$selected_hours = $selected_hours - 12;
				}
			} else {
				$selected_ampm = "am";
				if ($selected_hours == 0) {
					$selected_hours = 12;
				}
			}
		} else {
			$selected_hours = "";
			$selected_minutes = "";
			$selected_seconds = "";
			$selected_ampm = "";
		}
		
		$output = array();
		$output[] = "<span style=\"position: relative;\">";
		if ($label) {
			$output[] = "<span style=\"position: absolute; left: 2px; top: -18px;\">$label</span>";
			$output[] = "<span style=\"position: absolute; right: 6px; top: -18px;\">";
			$output[] = "	<a href=\"JavaScript:select_time('$form.$field','',$increment)\">Now</a>&nbsp;";
			$output[] = "	<a href=\"JavaScript:select_time('$form.$field','-1',$increment)\">Clear</a>";
			$output[] = "</span>";
		}
		$output[] = make_menu_simple("Hours...", $field."_hours", 1, 12, 1, 2, 0, $class, $selected_hours);
		$output[] = " : ";
		$output[] = make_menu_simple("Minutes...", $field."_minutes", 0, 59, $increment, 2, 0, $class, $selected_minutes);
		if ($use_seconds) {
			$output[] = " : ";
			$output[] = make_menu_simple("Seconds...", $field."_seconds", 0, 59, 1, 2, 0, $class, $selected_seconds);
		} else {
			$output[] = "<input type=\"hidden\" name=\"".$field."_seconds\" value=\"00\">\n";
		}
		$am_selected = ($selected_ampm == "am") ? "selected " : "" ;
		$pm_selected  = ($selected_ampm == "pm") ? "selected " : "" ;
		$class = ($class) ? "class=\"$class\"" : "" ;
		$output[] = "<select name=\"".$field."_ampm\" $class>\n";
		$output[] = "<option value=\"0\">am/pm...</option>\n";
		$output[] = "<option value=\"am\" $am_selected >am</option>\n";
		$output[] = "<option value=\"pm\" $pm_selected >pm</option>\n";
		$output[] = "</select>\n";
		$output[] = "</span>";
		$output = implode("\n", $output);
		
		return $output;
	
	}
	
	
	function combine_time_elements($hours, $minutes, $seconds, $ampm) {
	
		// helps us collect the values submitted by the time menus for insertion into a single database field
		// if any of the individual menus are unset, we'll put the empty date value into the database
	
		if (($hours)&&($minutes)&&($seconds)&&($ampm)) {
			if (($ampm == "pm")&&($hours < 12)) { $hours = $hours + 12; }
			if (($ampm == "am")&&($hours == 12)) { $hours = 0; }
			$time = "$hours:$minutes:$seconds";
		} else {
			$time = "00:00:00";
		}
		
		return $time;
	
	}
	
	
	function make_menu($title, $table, $name, $value, $text, $filter=0, $sort=0, $multiple=0, $class="", $selected=0) { 
	
		// creates an HTML popup menu (<select>) based on the contents of a table

		// $title - the value to set for the first option, which shows by default in the menu; use "Tablename..." in most cases (recommended)
		// $table - the database table to select from
		// $name - the name to set for the HTML select tag (usually the same as the table name)
		// $value - the database field to use for the option values (usually ID)
		// $text - the database field to use for the option text (usually name)
		// $filter - a database field and values to use to filter the output (e.g., status=2,3 or format=jpg,gif); 0 uses the default (active=1)
		// $sort - the database field to sort the output by; 1 sorts by $text; 0 uses the default ($value)
		// $multiple - if 1, adds the "multiple" attribute to the menu; 0 disables this option
		// $class - set the CSS class of the menu; "" disables this option
		// $selected - the options to show as selected initially (refers to position in the database table, not position in the menu); "all" selects all items in a multiple menu; 0 disables this option

		// 04/12/01 - allows inactive but selected values to appear; also limit=0 does no filtering (show all items), limit='' does total filtering (show no items)
		// 03/28/02 - added $multiple option; submits $table[] if used, $table if not
		// 05/08/02 - fixed two little bugs introduced in the last version
		// 01/26/03 - removed "WHERE active=1" from the query, so we can display inactive but selected or filtered records
		// 02/16/03 - we now return a value containing the menu, rather than printing the menu directly
		// 06/18/03 - changed the for loop to go from 0 to <$count
		// 07/24/03 - added the $select="all" option for multiple selection lists
		// 08/12/04 - added "WHERE active=1 OR active=0" for compatibility with the account_filter function in our web apps
		// 05/26/05 - added strict comparison operators to make the $selected and $filter options work with either 0 or "0"
		// 09/22/05 - added double quotes around HTML attributes
		// 07/19/07 - moved the limit functionality into the query, so we can no longer show an inactive item by including it in the $filter value; we can still show an inactive item by including it in the $selected value
		// 07/03/09 - removed hard-coded selection of ID field, and added $text argument instead of a hard-coded name field, to use with tables that don't have an ID or name field; changed $limit to $filter, which takes a name/value pair rather than just a list to use with tables that don't have an active field
		// 			  NOTE: this version is not backwards compatible because of a reordering of the arguments and removal of some defaults
		// 07/22/09 - changed default value from -1 to 0
		// 07/24/09 - compiled elements using an output array rather than a string
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 07/28/09 - tweaked backticks on sort values in case $sort is something like "date DESC"; changed default sorts to $input and $output rather than hard-coded ID and name
		// 08/31/09 - for multiple-select menus, don't output the title if the title isn't set; this avoids an extra line break above the menu

		$output = array();

		if ($filter) {
			list($filter_key, $filter_value) = explode("=", $filter);
		} else {
			$filter_key = "active";
			$filter_value = "1";
		}

		if ($sort) {
			$sort_value = ($sort == 1) ? $text : $sort ;
		} else {
			$sort_value = $value;
		}
		$sort_value = preg_replace("/(\w+)(.*)/", "`$1`$2", $sort_value); // add backticks to first word of sort value
		$sort = "ORDER by $sort_value" ;
	
		if ($multiple) {
			$multiple = "multiple";
			$name = $name."[]";
			if ($title) {
				$output[] = "$title<br>\n";
			}
		} else {
			$multiple = "";
		}
		
		$class = ($class) ? "class=\"$class\"" : "" ;
	
		if ($selected) {
			if (is_array($selected)) { $selected = implode(",", $selected); }
			$selected_select = "(FIND_IN_SET(`$value`, '$selected') OR '$selected'='all') AS selected";
			$selected_filter = "FIND_IN_SET(`$value`, '$selected')";
		} else {
			$selected_select = "0 AS selected";
			$selected_filter = "0";
		}
		
		$query = "
			SELECT `$value`, `$text`, $selected_select
			FROM `$table`
			WHERE FIND_IN_SET(`$filter_key`, '$filter_value') OR $selected_filter
			GROUP BY `$value`
			$sort
		";
		$result = db_do_query($query);
		$count = db_num_rows($result);

		$options = array();
		$default_selected = "selected";
		for ($i=0; $i<$count; $i++) {
			$row = db_get_row($result, $i);
			if ($row["selected"]) {
				$this_selected = "selected";
				$default_selected = "";
			} else {
				$this_selected = "";
			}
			$options[] = "  <option value=\"".$row[$value]."\" ".$this_selected.">".$row[$text]."</option>";
		}

		$output[] = "<select name=\"$name\" $multiple $class>";
		if (!$multiple) { $output[] = "	 <option value=\"0\" $default_selected>$title</option>"; }
		$output = array_merge($output, $options);
		$output[] = "</select>";

		$output = implode("\n", $output);
		
		return $output;
	
	}
	
	
	function make_menu_simple($title, $name, $start, $end, $increment=1, $digits=0, $multiple=0, $class="", $selected=0) { 
	
		// creates an HTML popup menu (<select>) with numeric values from $start to $end

		// $title - the value to set for the first option, which shows by default in the menu
		// $name - the name to set for the HTML select tag
		// $start - the number to use as the first value
		// $end - the number to use as the last value
		// $increment - number at which to increment the values; use 1 to increment by whole numbers (default)
		// $digits - adds leading zeros to numbers less than this length; use 0 to leave values untouched (default)
		// $multiple - if 1, adds the "multiple" attribute to the menu; 0 disables this option
		// $class - set the CSS class of the menu; "" disables this option
		// $selected - display the menu with this option selected by default; use 0 to show unselected (default)
	
		// 09/24/02 - initial development
		// 09/26/02 - we now return a value containing the menu, rather than printing the menu directly
		// 11/16/02 - added digits argument to add leading zeros
		// 02/16/03 - we now return a value containing the menu, rather than printing the menu directly
		// 06/25/03 - added another condition to ensure that nothing would be selected if $selected is ""
		// 02/15/04 - write options into an array and sort backwards if $start is greater than $end
		// 07/26/04 - added increment - NOTE: this adds an argument, so all function calls must be updated
		// 09/22/05 - added double quotes around HTML attributes
		// 07/22/09 - changed default value from -1 to 0
		// 07/30/09 - added multiple and class arguments to match make_menu

		$increment = (!$increment) ? 1 : $increment;
		$selected = (is_array($selected)) ? implode(",", $selected) : $selected ;
		
		// it would have been much neater to just dynamically write the for statement below, but we couldn't figure out a way to do that
		if ($start > $end) {
			$start_temp = $start;
			$end_temp = $end;
			$start = $end_temp;
			$end = $start_temp;
			$reverse = 1;
		} else {
			$reverse = 0;
		}

		if ($multiple) {
			$multiple = "multiple";
			$name = $name."[]";
			$output[] = "$title<br>\n";
		} else {
			$multiple = "";
		}
		
		$class = ($class) ? "class=\"$class\"" : "" ;
	
		$options = array();
		for ($i=$start; $i <= $end; $i++) {
			if ($i % $increment == 0){
				$this_number = strval($i);
				while (strlen($this_number) < $digits) { $this_number = "0".$this_number; }
				$this_selected = ((($i == $selected)||($this_number == $selected))&&($selected != "")) ? " selected" : "" ; // this handles $select values that are single items or lists (when $multiple=1)
				$options[] = "  <option value=\"".$this_number."\" ".$this_selected.">".$this_number."</option>";
			}
		}
		if ($reverse) { $options = array_reverse($options); }

		$output = array();
		$output[] = "<select name=\"$name\" $multiple $class>";
		if (!$multiple) { $output[] = "	 <option value=\"0\">$title</option>"; }
		$output = array_merge($output, $options);
		$output[] = "</select>";
		$output = implode("\n", $output);
		
		return $output;
	
	}
	
	
    function make_checkboxes($table, $name, $value, $text, $filter=0, $sort=0, $delimiter=0, $selected=0) { 
	
		// creates a series of HTML checkboxes (<input type="checkbox">) based on the contents of a table
	
		// $table - the database table to select from
		// $name - the name to set for the HTML input tag (usually the table name)
		// $value - the database field to use for the input values (usually ID)
		// $text - the database field to use for the input text (usually name)
		// $filter - a database field and values to use to filter the output (e.g., status=2,3 or format=jpg,gif); 0 uses the default (active=1)
		// $sort - the database field to sort the output by; 1 sorts by $text; 0 uses the default ($value)
		// $delimiter - the string to put in between the checkboxes; 0 uses the default (<br />)
		// $selected - the checkboxes to show as selected initially; "all" selects all items; use 0 to show all unselected (default)

		// 02/16/03 - added delimiter argument; we now return a value containing the menu, rather than printing the menu directly
		// 09/09/03 - removed class=form from the input tag since we removed it from make_radio_buttons
		// 05/24/05 - fixed $limit so it filters by ID rather than by table sequence
		// 05/26/05 - added strict comparison operators to make the $selected and $limit options work with either 0 or "0"
		// 09/22/05 - added double quotes around HTML attributes
		// 07/19/07 - moved the limit functionality into the query for better performance; moved the active check OUT of the query to allow displaying inactive but selected items; collapsed the this_checked setting to a single line; updated singleton tags for HTML 4.0 compliance
		// 07/03/09 - removed hard-coded selection of ID field, and added $text argument instead of a hard-coded name field, to use with tables that don't have an ID or name field; changed $limit to $filter, which takes a name/value pair rather than just a list to use with tables that don't have an active field
		// 			  NOTE: this version is not backwards compatible because of a reordering of the arguments and removal of some defaults
		// 07/24/09 - compiled elements using an output array rather than a string so delimiter would only be in the middle
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 07/28/09 - tweaked backticks on sort values in case $sort is something like "date DESC"; changed default sorts to $input and $output rather than hard-coded ID and name
		
		if ($filter) {
			list($filter_key, $filter_value) = explode("=", $filter);
		} else {
			$filter_key = "active";
			$filter_value = "1";
		}
		
		if ($sort) {
			$sort_value = ($sort == 1) ? $text : $sort ;
		} else {
			$sort_value = $value;
		}
		$sort_value = preg_replace("/(\w+)(.*)/", "`$1`$2", $sort_value); // add backticks to first word of sort value
		$sort = "ORDER by $sort_value" ;
	
		$delimiter = ($delimiter !== 0) ? $delimiter : "<br />" ;
	
		$selected = (is_array($selected)) ? implode(",", $selected) : $selected ;
	
		$query = "
			SELECT `$value`, `$text`, (FIND_IN_SET(`$value`, '$selected') OR '$selected'='all') AS selected
			FROM `$table`
			WHERE FIND_IN_SET(`$filter_key`, '$filter_value') OR FIND_IN_SET(`$value`, '$selected')
			$sort
		";
		$result = db_do_query($query);
		$count = db_num_rows($result);
		
		$output = array();
		for ($i=0; $i<$count; $i++) {
			$row = db_get_row($result, $i);
			$this_selected = ($row["selected"]) ? "checked" : "" ;
			$output[] = "<input type=\"checkbox\" name=\"".$name."[]\" value=\"".$row[$value]."\" ".$this_selected." /> ".$row[$text];
		}
		$output = implode($delimiter."\n", $output);
		
		return $output;
	
	}
    
    
    function make_radio_buttons($table, $name, $value, $text, $filter=0, $sort=0, $delimiter=0, $selected=0) { 
	
		// creates a series of HTML radio buttons (<input type="radio">) based on the contents of a table
	
		// $table - the database table to select from
		// $name - the name to set for the HTML input tag (usually the table name)
		// $value - the database field to use for the input values (usually ID)
		// $text - the database field to use for the input text (usually name)
		// $filter - a database field and values to use to filter the output (e.g., status=2,3 or format=jpg,gif); 0 uses the default (active=1)
		// $value - set the value of each radio button to something other than the row ID; use 0 to set values to IDs (default)
		// $sort - the database field to sort the output by; 1 sorts by $text; 0 uses the default ($value)
		// $delimiter - the string to put in between the radio buttons; 0 uses the default (<br />)
		// $selected - the radio button to show as selected initially (refers to position in the database table, not position in the menu); use 0 to show all unselected (default)

		// 02/16/03 - added delimiter argument; we now return a value containing the menu, rather than printing the menu directly
		// 09/09/03 - removed class=form from the input tag; that created weird alignments on Windows
		// 03/10/05 - removed the square brackets from the element names, since radio buttons don't need to send arrays
		// 05/24/05 - fixed $limit so it filters by ID rather than by table sequence
		// 05/26/05 - added strict comparison operators to make the $selected and $limit options work with either 0 or "0"
		// 08/17/05 - changed default sorting to ID, not ID DESC; restored delimiter argument -- what happened to that?
		// 09/22/05 - added double quotes around HTML attributes
		// 07/19/07 - added missing $delimiter argument to argument list
		// 07/19/07 - moved the limit functionality into the query for better performance; moved the active check OUT of the query to allow displaying inactive but selected items; collapsed the this_checked setting to a single line; updated singleton tags for HTML 4.0 compliance
		// 07/03/09 - removed hard-coded selection of ID field, and added $text argument instead of a hard-coded name field, to use with tables that don't have an ID or name field; changed $limit to $filter, which takes a name/value pair rather than just a list to use with tables that don't have an active field
		// 			  NOTE: this version is not backwards compatible because of a reordering of the arguments and removal of some defaults
		// 07/24/09 - compiled elements using an output array rather than a string so delimiter would only be in the middle
		// 07/26/09 - added backticks around table and field names to avoid problems with reserved words
		// 07/28/09 - tweaked backticks on sort values in case $sort is something like "date DESC"; changed default sorts to $input and $output rather than hard-coded ID and name
		// 07/12/10 - fixed a bug when using something other than "name" as the display field
		
		if ($filter) {
			list($filter_key, $filter_value) = explode("=", $filter);
		} else {
			$filter_key = "active";
			$filter_value = "1";
		}
	
		if ($sort) {
			$sort_value = ($sort == 1) ? $text : $sort ;
		} else {
			$sort_value = $value;
		}
		$sort_value = preg_replace("/(\w+)(.*)/", "`$1`$2", $sort_value); // add backticks to first word of sort value
		$sort = "ORDER by $sort_value" ;
	
		$delimiter = ($delimiter !== 0) ? $delimiter : "<br />" ;
		
		$selected = intval($selected);

		$query = "
			SELECT `$value`, `$text`, (`$value`=$selected) AS selected
			FROM `$table`
			WHERE FIND_IN_SET(`$filter_key`, '$filter_value') OR `$value`=$selected
			$sort
		";
		$result = db_do_query($query);
		$count = db_num_rows($result);
		
		$output = array();
		for ($i=0; $i<$count; $i++) {
			$row = db_get_row($result, $i);
			$this_selected = ($row["selected"]) ? "checked" : "" ;
			$output[] = "<input type=\"radio\" name=\"".$name."\" value=\"".$row[$value]."\" ".$this_selected." /> ".$row[$text];
		}
		$output = implode($delimiter."\n", $output);
		
		return $output;
	
	}
	
	
	function make_rows($content, $delimiter, $row_length, $minimum_length=0) {
	
		// take the output from make_checkboxes or make_radio_buttons (or any other list) and split it into the requested number of rows
		// this assumes we have a style defined for reset
		// we only need $delimiter if we're passing in a string
		// we always return a string
		
		if (is_array($content)) {
			$content_array = $content;
		} else {
			$content_array = explode($delimiter, $content);
		}

		$content_count = count($content_array);
		
		if ($row_length > $minimum_length) {
		
			$rows_count = ceil($content_count / $row_length);
		
			for ($i=0; $i<$rows_count; $i++) {
				$this_content_count = min($row_length, count($content_array));
				$rows[$i] = array_splice($content_array, 0, $this_content_count);
				$rows[$i] = implode($delimiter, $rows[$i]);
				$rows[$i] .= "<div class=\"reset\"></div>"; // okay, how else can we break the line without inserting a block-level element?
			}
			
			$content = implode("\n\n", $rows);
			//$content .= "<div class=\"reset\"></div>\n\n"; // actually, we can't nest this inside of a <p> tag (and span doesn't have the desired effect), so let's handle this outside the function
		
		}
	
		return $content;
		
	}
	
	
	function prepare_filters($user_variable, $user, $page, $names) {
	
		// gets the requested filter values from the database and updates them if they have changed
		// this assumes we have database tables called user_filter and user_sort, each with ID, user_variable, user, page, name, and value fields

		// NOTE: in the code library, the user_variable and table columns are called user_type
		// we could update this to match, but then all previous users would have to update their table columns
		// so we'll leave it as is for now
		
		/*

		CREATE TABLE `user_filter` (
		  `ID` int(11) NOT NULL auto_increment,
		  `user_variable` tinytext NOT NULL,
		  `user` int(11) NOT NULL default '0',
		  `page` tinytext NOT NULL,
		  `name` tinytext NOT NULL,
		  `value` tinytext NOT NULL,
		  PRIMARY KEY  (`ID`)
		);

		CREATE TABLE `user_sort` (
		  `ID` int(11) NOT NULL auto_increment,
		  `user_variable` tinytext NOT NULL,
		  `user` int(11) NOT NULL default '0',
		  `page` tinytext NOT NULL,
		  `value` tinytext NOT NULL,
		  PRIMARY KEY  (`ID`)
		);

		*/
		
		// $user_variable - usually this is "user," but we can separate it into "administrator," "member," etc. to prevent conflicts between different types of user IDs
		// $user - the user ID to save the filters under
		// $page - the root of the page name calling this function
		// $names - a comma-delimited list of filter names
		
		// 2009-08-11 - updated to take care of checkboxes that are submitted as arrays
		
		$names = explode(",", $names);

		GLOBAL $filters_submitted;
		foreach ($names as $name) {
			GLOBAL ${"filter_".$name};
		}
		GLOBAL $sort;
	
		// get the values that are currently saved in the database
		
		$query = "SELECT name, value FROM user_filter WHERE user_variable='$user_variable' AND user=$user AND page='$page'";
		$result = db_do_query($query);
		$count = db_num_rows($result);
		for ($i=0; $i < $count; $i++) {
			$row = db_get_row($result, $i);
			extract($row);
			${"filter_".$name."_saved"} = $value;
		}

		$query = "SELECT value FROM user_sort WHERE user_variable='$user_variable' AND user=$user AND page='$page'";
		$result = db_do_query($query);
		$row = db_get_row($result, 0);
		$sort_saved = $row["value"];
		
		// update the saved values if we've input something different

		foreach ($names as $name) {
			if (is_array($name)) { $name = implode(",", $name); }
			${"filter_".$name."_saved"} = (isset(${"filter_".$name."_saved"})) ? ${"filter_".$name."_saved"} : "" ;
			if (($filters_submitted)&&(${"filter_".$name} != ${"filter_".$name."_saved"})) {
				$query = "DELETE FROM user_filter WHERE user_variable='$user_variable' AND user=$user AND page='$page' AND name='$name'";
				db_do_query($query);
				$query = "INSERT INTO user_filter (user_variable, user, page, name, value) VALUES ('$user_variable', $user, '$page', '$name', '".${"filter_".$name}."')";
				db_do_query($query);
			} else {
				${"filter_".$name} = ${"filter_".$name."_saved"};
			}
		}

		if (($filters_submitted)&&($sort != $sort_saved)) {
			$query = "DELETE FROM user_sort WHERE user_variable='$user_variable' AND user=$user AND page='$page'";
			db_do_query($query);
			$query = "INSERT INTO user_sort (user_variable, user, page, value) VALUES ('$user_variable', $user, '$page', '$sort')";
			db_do_query($query);
		} else {
			$sort = $sort_saved;
		}

	}
	

	function show_content($content, $label) {
	
		// if we delineate particular content with HTML comment tags, we can quickly remove eveything but it with this function
		
		$content = preg_replace("/.*<!--\s*$label\s*-->(.*?)<!--\s*$label\s*-->.*/s", "$1", $content, 1);
		return $content;
	
	}


	function hide_content($content, $label) {
	
		// if we delineate optional content with HTML comment tags, we can quickly remove it with this function
		
		$content = preg_replace("/<!--\s*$label\s*-->.*?<!--\s*$label\s*-->/s", "", $content);
		return $content;
	
	}


	function send_email($to_email, $to_name, $from_email, $from_name, $bounce_email, $subject, $body, $template, $custom_headers="") {
	
		// sends plain text mail, HTML-formatted mail, or HTML mail merged with an HTML template
	
		if ($template == "0") {  // plain text
			
			$headers = "Content-Type: text/plain; charset=us-ascii";
			
		} else if ($template == "1") {  // HTML-formatted
				
			$headers = "Content-Type: text/html; charset=iso-8859-15";
			$body = "
				<html>
					<head>
						<link href=\"".SITE_PROTOCOL."://".SITE_DOMAIN."/shared/styles.css\" rel=\"stylesheet\" type=\"text/css\">
						<link href=\"".SITE_PROTOCOL."://".SITE_DOMAIN."/shared/elements.css\" rel=\"stylesheet\" type=\"text/css\">
					</head>
					<body>
						$body
					</body>
				</html>
			";
			$body = str_replace("\t", "", $body);  // remove tabs, which look funny in some email programs
		
		} else {  // HTML-formatted with a MIME template
		
			// $template should be a PHP file that defines additional MIME headers and the MIME parts,
			// with "$subject" and "$body" inserted where you want the subject and body text to appear
		
			include($template);
			//$body = chunk_split($MIME_body, 72, "\r\n"); // hmm, why doesn't this work?
			//$body = wordwrap($MIME_body, 72, "\n"); // don't we need this anymore?
	
			$headers = $MIME_headers;
			$body = $MIME_body;
					
		}
		
		$to = (($to_name)&&($to_name != " ")) ? "\"$to_name\" <$to_email>" : "$to_email" ;
		$from = (($from_name)&&($from_name != " ")) ? "\"$from_name\" <$from_email>" : "$from_email" ;
		
		if ($custom_headers) { $custom_headers = "\n".$custom_headers; }
	
		$result = mail($to, $subject, $body, "From: $from\nErrors-To: $bounce_email\nReturn-Path: $bounce_email\n".$headers.$custom_headers, "-f $bounce_email");
		
		return $result;
		
	}
	
	
	function make_pagination_links($start, $results_per_page, $total) {
	
		// adds a "start" argument to the links to show a subset of search results
		// returns an empty string if there are fewer results than the amount allowed per page
		// you can use LIMIT $start, $results_per_page to limit the query
		// you can use SQL_CALC_FOUND_ROWS to get the total, regardless of the limit, from the query
	
		// 05/21/08 - initial development
		// 08/11/08 - added pagination_previous, pagination_next and pagination_numbers classes; this lets us control spacing and set images or hide the previous/next text from the style sheets
		// 08/25/08 - updated to support links with either ? or &
		// 09/10/08 - added $max_page_links to limit the number of page links displayed at one time
		// 09/12/08 - added rounding to avoid fractional page numbers
		// 11/20/08 - changed links to call a JavaScript function rather than do direct navigation
		// 07/16/09 - renamed page function to click_pagination_link() to avoid a conflict with a variable named page
		
		$max_page_links = 15;
		
		if ($total > $results_per_page) {
			$pagination_links = Array();
			if ($total > $results_per_page) {
				$previous = $start - $results_per_page;
				$next = $start + $results_per_page;
				$this_page = $start / $results_per_page;
				$pages = ceil($total / $results_per_page);
				if ($pages > $max_page_links) {
					$first_page = max(0, ($this_page - floor(($max_page_links / 2))));
					$display_pages = $max_page_links;
				} else {
					$first_page = 0;
					$display_pages = $pages;
				}
				if ($this_page > 0) {
					$pagination_links[] = "<a href=\"JavaScript:click_pagination_link('$previous');\" class=\"pagination_previous\">Previous</a>";
				} else {
					$pagination_links[] = "<span class=\"pagination_previous\">Previous</span>";
				}
				for ($i=$first_page; $i<($first_page + $display_pages); $i++) {
					$display_number = $i + 1; // everything in this section is 0-indexed, except the page numbers we show the users
					$this_start = $i * $results_per_page;
					if ($i != $this_page) {
						$pagination_links[] = "<a href=\"JavaScript:click_pagination_link('$this_start');\" class=\"pagination_numbers\">$display_number</a>";
					} else {
						$pagination_links[] = "<span class=\"pagination_numbers\" id=\"active\">$display_number</span>";
					}
				}
				if ($this_page < ($pages - 1)) {
					$pagination_links[] = "<a href=\"JavaScript:click_pagination_link('$next');\" class=\"pagination_next\">Next</a>";
				} else {
					$pagination_links[] = "<span class=\"pagination_next\">Next</span>";
				}
			}
			$pagination_links = implode("&nbsp;\n", $pagination_links);
		} else {
			$pagination_links = "";
		}
		return $pagination_links;

	}
	
	
	function find_desired_set($result, $count) {
	
		// moves forward in a query to find the result set that contains a particular record
		// requires that the result contains an ID field

		GLOBAL $start, $show, $desired_set;
		
		if ($show) {
			for ($i=0; $i < $count; $i++) {
				$row = db_get_row($result, $i);
				if ($row["ID"] == $show) {
					$desired_set = 1;
					break;
				}
			}
			if (!$desired_set) {
				if ($count) {
					$start = $start + ADMIN_RECORDS_PER_PAGE;
				} else {
					// we must have gone all the way through without finding the record, so just forget about it
					$start = 0;
					$show = 0;
				}
			}
		} else {
			$desired_set = 1;
		}
	
	}
	
	
	function build_excerpt($ID, $text, $max_length) {
	
		// instead of showing the entire text, show a portion of it with a "more" link
		// we use onclick with return false to prevent Firefox and IE from loading a blank page after running the commands
	
		if (strlen($text) > $max_length) {
			$text_more = $text;
			$text_less = substr($text, 0, $max_length)."...";
			$link_more = "(<a href=\"\" onclick=\"document.getElementById('excerpt_less_$ID').style.display = 'none'; document.getElementById('excerpt_more_$ID').style.display = 'inline'; return false;\">more</a>)";
			$link_less = "(<a href=\"\" onclick=\"document.getElementById('excerpt_more_$ID').style.display = 'none'; document.getElementById('excerpt_less_$ID').style.display = 'inline'; return false;\">less</a>)";
			$text = "<span style=\"display: none;\" id=\"excerpt_more_$ID\">$text_more $link_less</span>";
			$text .= "<span style=\"display: inline;\" id=\"excerpt_less_$ID\">$text_less $link_more</span>";
		}

		return $text;

	}


	function convert_characters($text, $style="html") {

		// convert special characters to HTML entities or low-ASCII equivalents
		
		// these aren't high-ASCII, but they're not allowed in XML so let's convert them first
		//if ($style == "html") {
		//	$text = str_replace("&", "&amp;", $text); // ampersand
		//	$text = str_replace("<", "&lt;", $text); // greater-than sign
		//	$text = str_replace(">", "&gt;", $text); // less-than sign
		//}
		
		// now, change all high-ASCII characters to numeric definitions
		$text_temp = "";
		for ($i=0; $i < strlen($text); $i++) {
			$decimal = ord($text[$i]);
			if ($decimal > 127) {
				$text_temp .= "&#".$decimal.";";
			} else {
				$text_temp .= $text[$i];
			}
		}
		$text = $text_temp;
		
		// finally, convert known definitions to HTML or text
		if ($style == "html") {
			$text = str_replace("&#133;", "&hellip;", $text); // ellipses
			$text = str_replace("&#145;", "&lsquo;", $text); // left single quote
			$text = str_replace("&#146;", "&rsquo;", $text); // right single quote
			$text = str_replace("&#147;", "&ldquo;", $text); // left double quote
			$text = str_replace("&#148;", "&rdquo;", $text); // right double quote
			$text = str_replace("&#149;", "&bull;", $text); // normal bullet
			$text = str_replace("&#150;", "&ndash;", $text); // n-dash
			$text = str_replace("&#151;", "&mdash;", $text); // m-dash
			$text = str_replace("&#169;", "&copy;", $text); // copyright symbol
			$text = str_replace("&#223;", "&bull;", $text); // MS Word bullet
		} else {
			$text = str_replace("&#133;", "...", $text); // ellipses
			$text = str_replace("&#145;", "'", $text); // left single quote
			$text = str_replace("&#146;", "'", $text); // right single quote
			$text = str_replace("&#147;", "\"", $text); // left double quote
			$text = str_replace("&#148;", "\"", $text); // right double quote
			$text = str_replace("&#149;", " - ", $text); // normal bullet
			$text = str_replace("&#150;", " - ", $text); // n-dash
			$text = str_replace("&#151;", " -- ", $text); // m-dash
			$text = str_replace("&#169;", "(c)", $text); // copyright symbol
			$text = str_replace("&#223;", " - ", $text); // MS Word bullet
		}
		
		return $text;
		
	}
	
	
	function timestamp($language, $universal=0) {
	
		// return the current time in the user's time zone, or in universal time, in a variety of formats
	
		$language = strtolower($language);
		
		// get the user's time zone from the session
		$user_timezone = $GLOBALS["timezone"];
		
		// get the server timezone from PHP, removing the effect of daylight savings time, because we don't know the user's DST setting
		// this means during DST our stored times will be an hour off from true UTC, but they will be accurate when we convert them back for display
		$server_timezone = (date("Z") / 60 / 60);
		if (date("I")) { $server_timezone--; }

		if ($universal) {
			// this returns the current time in Greenwich
			$timezone_correction = $server_timezone;
		} else {
			// this returns the current time in the user's location
			$timezone_correction = $server_timezone - $user_timezone;
		}
		
		if ($language == "php") {
			return date("Y-m-d H:i:s", time() - ($timezone_correction * 3600));
		} else if ($language == "sql") {
			return "DATE_SUB(NOW(), INTERVAL $timezone_correction HOUR)";
		} else if ($language == "unix") {
			return (time() - ($timezone_correction * 3600));
		}
		
	}
	
	
	function save_file($name, $content, $backup=0) {
		// if we're in demo mode, don't save anything
		if ($GLOBALS["demo"]) {
			return 1;
		}
		
		// find out if the file has been locked by Dreamweaver
		// (for page definitions, it would be better to do this when loading the file!)
		$lock_name = check_dreamweaver_lock($name);
		if ($lock_name) {
			return "<p class=\"feedback_negative\">Error: The file $name is currently checked out by Contribute or Dreamweaver.</p>";
		}
	
		// if requested, read the existing file and back it up
		if ($backup) {
			$result = copy_file($name, $name.".backup");
			if (!$result) {
				return "<p class=\"feedback_negative\">Error: Could not back up the file $name.</p>";
			}
		}
		
		// $name can be an absolute path, or a relative path to the current working directory
		@$my_file = fopen($name, "w");
		if ($my_file) {
			fwrite($my_file, $content);
			fclose($my_file);
			$result = 1;
		} else {
			$result = 0;
		}

		return $result;
	}


	function copy_file($name_original, $name_new) {
		// copy a file, but avoid an error if it doesn't exist
		$success = 0;
		if ($name_original) {
			if (file_exists($name_original)) {
				$success = copy($name_original, $name_new);
			}
		}
		return $success;
	}


	function rename_file($name_old, $name_new) {
		// rename a file, but avoid an error if it doesn't exist
		$success = 0;
		if ($name_old) {
			if (file_exists($name_old)) {
				$success = rename($name_old, $name_new);
			} else {
				$success = 1;
			}
		}
		return $success;
	}


	function delete_file($file_location) {
		// delete a file, but avoid an error if it doesn't exist
		$success = 0;
		if ($file_location) {
			if (file_exists($file_location)) {
				$success = unlink($file_location);
			} else {
				$success = 1;
			}
		}
		return $success;
	}


	function list_directory($directory, $types="directories", $recursive=0, $original_directory="") {
		
		// get a list of the directories and or files and turn the list into an array 
		// $directory - starting directory from which to search
		// $types - can be "directories," "files," or "all"
		// $recursive - determines whether subdirectories are searched
		// $original_directory - allows the function to return subdirectories but not the original directory as part of the file paths; leave this argument at the end and don't set it when calling the function
		
		// 2004-03-12 - initial development
		// 2006-10-02 - included subdirectories in file names, and searched directories recursively even when only files are returned
		// 2010-11-23 - fixed a missing rename item (was directory_list)
		
		if ($dir = opendir($directory)) {
			$file_list = array();
			while (($file = readdir($dir)) !== false) {
				// ignore the . and .. file references
				if ($file != "." && $file != "..") {
					// don't include the original directory in file names, but include any subdirectories
					if ($original_directory == "") { $original_directory = $directory; }
					$file_display = substr($directory."/".$file, (strlen($original_directory) + 1));
				
					// check whether this is a file or directory
					if (is_dir($directory."/".$file)) {
						// log the directory name if requested
						if (($types == "directories") || ($types == "all")) {
							$file_list[] = $file_display;
						}
						
						// if we're in a directory and we want recursive behavior, call this function again
						if ($recursive) {
							$file_list = array_merge($file_list, list_directory($directory."/".$file, $types, $recursive, $original_directory));
						}
					} else {
						// log the file name if requested
						if (($types == "files") || ($types == "all")) {
							$file_list[] = $file_display;
						}
					}
					
				}
			}
			closedir($dir);
			return $file_list;
		} else {
			return false;
		}
		
	}


	function get_position($string, $array) {
	
		// returns the index of an array element, something that ought to be built into PHP but isn't!
		// returns "" if not present
	
		$index = implode(",", array_keys($array, $string));
		return $index;
		
	}
	
	
	function get_URL() {
	
		// parses some environmental variables to build a complete URL for the current page
	
		$temp = explode("/", $_SERVER["SERVER_PROTOCOL"]);
		$URL = strtolower($temp[0])."://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"];
		
		return $URL;
		
	}
	
	
	function redirect($page="", $secure=0, $window="window") {
		// makes a URL absolute if it's relative (for standards compliance), then loads it
		// if no arguments are provided, returns to the previous page
		if (($page == "")&&(isset($_SERVER["HTTP_REFERER"]))&&($_SERVER["HTTP_REFERER"] != "")) {
			$URL = $_SERVER["HTTP_REFERER"];
		} else if (strpos($page, "://") !== false) {
			$URL = $page;
		} else {
			$page = preg_replace("/^[\.\\/]+(.*)$/", "$1", $page); // remove a leading slash or ../ if it exists
			$URL = SITE_PROTOCOL."://".SITE_DOMAIN."/".$page;
		}
		if (headers_sent()) {
			// this is clunky, but it does the job if some output has already occurred
			print "<script language=\"JavaScript\"> $window.location = \"$URL\"; </script>";
		} else if (isset($_SERVER["HTTP_AJAX_REQUEST"])) {
			// if this is an AJAX request, pass the redirect value to the AJAX script know so it can redirect the entire page
			header("Ajax-Redirect: $URL");
		} else {
			// this is what we'll get most of the time
			header("Location: $URL");
		}
		exit;
	}


	function build_page_browser() {
	
		$output = array();
	
		// look for open directories and a current pagename saved in a cookie
		GLOBAL $open_directories, $current_page;
		if (isset($_COOKIE["page_browser"])&&($_COOKIE["page_browser"])) {
			$cookie_string = urldecode($_COOKIE["page_browser"]);
			$cookie_array = explode_assoc("=", "&", $cookie_string);
			$open_directories = (isset($cookie_array["open_directories"])&&($cookie_array["open_directories"])) ? explode(",", $cookie_array["open_directories"]) : array() ;
			$current_page = (isset($cookie_array["current_page"])&&($cookie_array["current_page"])) ? $cookie_array["current_page"] : "" ;
		} else {
			$open_directories = array();
			$current_page = "";
		}
		
		switch(CONTENT_TYPE) {
		
			case "contemplate":
				$output[] = get_pages();
				break;
				
			case "dreamweaver":
				$output[] = get_pages();
				break;
			
		}
		
		$output[] = "<a href=\"JavaScript:build_page_browser();\" class=\"link_refresh\">Refresh</a>";
		$output = implode("\n", $output);
		return $output;
	
	}
	
	
	function get_pages() { // if we need to get pages only for a specific directory, we can add an argument here, and pass in DREAMWEAVER_CONTENT_DIRECTORIES to get them all
		$output = array();
		
		if (CONTENT_TYPE == "contemplate") {
	
			// get ready to use Contemplate's functions
			load_contemplate_assembler();
				
			// this loads the page definitions file (we're assuming there is only one)
			$page_definitions = get_page_definitions();
			
			// this gets an array of page names using the assembler function
			$page_names_grouped = get_page_names($page_definitions, 1);
			
			// this formats each page name into a link
			foreach ($page_names_grouped as $group_name=>$page_names) {
				if ((count($page_names) > 0)&&(count($page_names_grouped) > 1)) {
					$output[] = "<p class=\"content_browser_group not_collapsable\">$group_name</p>";
				}

				foreach($page_names as $page_name) {
					$output[] = "<a href=\"JavaScript:click_page('$page_name');\" id=\"page_$page_name\" class=\"page_browser_page\">$page_name</a><br />";
				}
			}
			
		}
		
		if (CONTENT_TYPE == "dreamweaver") {
		
			GLOBAL $open_directories;
			if (!isset($open_directories)) { $open_directories = array(); }
			$directories = explode(",", DREAMWEAVER_CONTENT_DIRECTORIES);
			$content_file_extensions = explode(",", CONTENT_FILE_EXTENSIONS);

			foreach($directories as $directory) {
				$page_names = list_directory(SITE_DIRECTORY."/$directory", "files", 0);

				if ((count($page_names) > 0)&&(count($directories) > 1)) {
					$output[] = "<p class=\"content_browser_group not_collapsable\">$directory</p>";
				}
			
				sort($page_names);
				foreach($page_names as $page_name) {
					$file_extension = preg_replace("/^\w+\.(\w+?)$/", "$1", $page_name);
					if (in_array($file_extension, $content_file_extensions)) {
						if (strpos($page_name, ".LCK") === false) { // don't show Dreamweaver lock files; is this needed since we're filtering by file extension now?
							$directory_and_page_name = "$directory/$page_name";
							$directory_and_page_name_stripped = preg_replace("/\W/", "_", $directory_and_page_name);
							$locked = (check_dreamweaver_lock($directory_and_page_name));
							if ($locked) {
								$output[] = "<span class=\"page_browser_page file_locked\">$page_name</span>";
							} else {
								$output[] = "<a href=\"JavaScript:click_page('$directory_and_page_name');\" id=\"page_$directory_and_page_name_stripped\" class=\"page_browser_page\">$page_name</a><br />";
							}
						}
					}
				}
			}

		}

		$output = implode("\n", $output);
		return $output;
	}
	
	
	function build_content_browser() {
	
		$output = array();
	
		// look for open directories and files saved in a cookie
		GLOBAL $open_groups, $open_paths;
		if (isset($_COOKIE["content_browser"])&&($_COOKIE["content_browser"])) {
			$cookie_string = urldecode($_COOKIE["content_browser"]);
			$cookie_array = explode_assoc("=", "&", $cookie_string);
			$open_groups = (isset($cookie_array["open_groups"])&&($cookie_array["open_groups"])) ? explode(",", $cookie_array["open_groups"]) : array() ;
			$open_paths = (isset($cookie_array["open_paths"])&&($cookie_array["open_paths"])) ? explode(",", $cookie_array["open_paths"]) : array() ;
		} else {
			$open_groups = array();
			$open_paths = array();
		}
		
		if (CONTENT_TYPE == "contemplate") {
			$version_check = check_contemplate_version();
			if ($version_check !== 1) {
				return $version_check;
			}
		}
		
		$groups = get_default_groups();
		$output[] = get_path_links($groups);
		$output[] = "<a href=\"JavaScript:build_content_browser();\" class=\"link_refresh\">Refresh</a>";
		$output = implode("\n", $output);

		return $output;
	
	}
	
	
	function get_default_groups() {
		switch(CONTENT_TYPE) {
			case "contemplate":
				$groups = CONTEMPLATE_CONTENT_DIRECTORIES;
				load_contemplate_assembler();
				$recursion = READ_FILES_RECURSIVELY;
				break;
			case "database":
				$groups = DATABASE_TABLES;
				$recursion = false;
				break;
			case "dreamweaver":
				$groups = DREAMWEAVER_CONTENT_DIRECTORIES;
				$recursion = DREAMWEAVER_CONTENT_SUBDIRECTORIES;
				break;
		}
		$groups = explode(",", $groups);

		// add any subdirectories as additional groups, so they get new headings in the file browser
		if ($recursion) {
			foreach ($groups as $group) {
				$subgroups = list_directory(SITE_DIRECTORY."/$group", "directories", 1);
				foreach ($subgroups as $subgroup) {
					if ($group) {
						$groups[] = "$group/$subgroup";
					} else {
						$groups[] = "$subgroup";
					}
				}
			}
		}

		sort($groups);
		$groups = implode(",", $groups);
		return $groups;
	}
	
	
	function get_paths($group) {
		$paths = array();

		$groups = explode(",", $group);
		
		foreach ($groups as $group) {
			if (CONTENT_TYPE == "database") {
				$group_paths = array();
				db_connect_content();
				$query = "SELECT DISTINCT `".DATABASE_ID1_COLUMN."` FROM `".$group."`";
				$result = db_do_query($query);
				$count = db_num_rows($result);
				for ($i=0; $i<$count; $i++) {
					$row = db_get_row($result, $i);
					$group_paths[] = $row[DATABASE_ID1_COLUMN];
				}
				db_connect_editor();
			} else {
				$group_paths = list_directory(SITE_DIRECTORY."/$group", "files", 0); // we'll add subdirectories in get_default_groups, so we don't need to use recursion here
			}
			sort($group_paths);
			foreach ($group_paths as $group_path) {
				$paths[] = $group."/".$group_path;
			}
		}

		return $paths;
	}
	
	
	function get_path_links($group) {
		// this function is also called from the content edit page, so we're keeping it separate from the build_content_browser function
		$output = array();

		$groups = explode(",", $group);

		GLOBAL $open_groups, $open_paths, $access;
		if (!isset($open_groups)) { $open_groups = array(); }
		if (!isset($open_paths)) { $open_paths = array(); }
		
		$content_file_extensions = explode(",", CONTENT_FILE_EXTENSIONS);
		
		foreach($groups as $group) {
			$paths = get_paths($group);
			$group_output = array();
			foreach($paths as $path) {
				if ((!count($access))||(in_array($path, $access))) {
					$simple_path = basename($path);
					if ( ((CONTENT_TYPE == "database")&&(strpos($path, "/") === false)) || ((CONTENT_TYPE != "database")&&(is_dir(SITE_DIRECTORY."/$path"))) ) {
						// directory
						$collapsable = (COLLAPSABLE_GROUPS);
						$expanded = (in_array("$path", $open_groups)||(!$collapsable));
						
						$collapsable_class = ($collapsable) ? "" : "not_collapsable" ;
						$expanded_class = ($expanded) ? "expanded" : "collapsed" ;
		
						if ($collapsable) {
							$group_output[] = "<a href=\"JavaScript:click_group('$path');\" id=\"group_$path\" class=\"content_browser_group $collapsable_class $expanded_class\">$simple_path</a><br />";
						} else {
							$group_output[] = "<p class=\"content_browser_group $collapsable_class $expanded_class\">$simple_path</p>";
						}
						$group_output[] = "<div id=\"paths_$path\" class=\"content_browser_paths $collapsable_class $expanded_class\">";
						if ($expanded) {
							$group_output[] = get_path_links($path);
						}
						$group_output[] = "</div>";
					} else {
						// file
						$file_extension = preg_replace("/^\w+\.(\w+?)$/", "$1", $simple_path);
						if ( (CONTENT_TYPE == "database") || ((strpos($path, ".LCK") === false)&&(in_array($file_extension, $content_file_extensions))) ) { // don't show Dreamweaver lock files or files with non-matching file extensions (neither of which applies to database files)
							$locked = (check_dreamweaver_lock($path));
							$collapsable = (COLLAPSABLE_GROUPS);
							$expanded = (in_array($path, $open_paths)||(!$collapsable));
							
							$locked_class = ($locked) ? "file_locked" : "" ;
							$collapsable_class = ($collapsable) ? "" : "not_collapsable" ;
							$expanded_class = ($expanded) ? "expanded" : "collapsed" ;
							
							if ($collapsable) {
								$group_output[] = "<a href=\"JavaScript:click_path('$path');\" id=\"path_$path\" class=\"content_browser_group $locked_class $collapsable_class $expanded_class\">$simple_path</a><br />";
							} else {
								$group_output[] = "<p class=\"content_browser_group $locked_class $collapsable_class $expanded_class\">$simple_path</p>";
							}
							$group_output[] = "<div id=\"paths_$path\" class=\"content_browser_paths $collapsable_class $expanded_class\">";
							if ($expanded) {
								$group_output[] = get_fields($path);
							}
							$group_output[] = "</div>";
						}
					}
				}
			}
			if (count($group_output) > 0) {
				if (count($groups) > 1) {
					// group heading
					$output[] = "<p class=\"content_browser_group not_collapsable\">$group</p>";
				}
				$output = array_merge($output, $group_output);
			}
		}
			
		$output = implode("\n", $output);
		return $output;
	}
	
	
	function get_fields($path) {
		// first, get a list of the currently open (in progress, in review or scheduled) fields from our database
		$timezone = initialize("timezone", "session", "text");
		$fields_open = array();
		$query = "
			SELECT
				field,
				status.ID AS status_ID,
				status.name AS status,
				user.name AS editor,
				DATE_ADD(date, INTERVAL $timezone HOUR) AS last_saved,
				DATE_ADD(schedule, INTERVAL $timezone HOUR) AS schedule
			FROM content
			LEFT JOIN status ON status.ID=content.status
			LEFT JOIN user ON user.ID=content.user
			INNER JOIN (SELECT MAX(ID) AS ID FROM content GROUP BY path, field) AS content_latest ON content_latest.ID=content.ID
			WHERE path='$path'
			AND FIND_IN_SET(content.status, '1,2,3')
			ORDER BY last_saved
		";
		$result = db_do_query($query);
		$count = db_num_rows($result);
		for ($i=0; $i<$count; $i++) {
			$row = db_get_row($result, $i);
			$fields_open[$row["field"]] = $row;
		}
		
		if (CONTENT_TYPE == "contemplate") {

			// get ready to use Contemplate's functions
			load_contemplate_assembler();
			
			// get the content field names and build a link for each one
				$source = get_file("../".$path, "filesystem", 0);
			$file_type = (strpos($path, "xml") !== false) ? "xml" : "html" ;
			$fields = get_field_names($source, $file_type);
			//sort($fields);
		
		} else if (CONTENT_TYPE == "database") {
		
			list($table, $ID1) = explode("/", $path);
		
			$fields = array();
			db_connect_content();
			$query = "SELECT `".DATABASE_ID2_COLUMN."` FROM `".$table."` WHERE `".DATABASE_ID1_COLUMN."`='".$ID1."'";
			$result = db_do_query($query);
			$count = db_num_rows($result);
			for ($i=0; $i<$count; $i++) {
				$row = db_get_row($result, $i);
				$fields[] = $row[DATABASE_ID2_COLUMN];
			}
			db_connect_editor();
		
		} else if (CONTENT_TYPE == "dreamweaver") {
		
			$file = file_get_contents("../".$path);
			$match = preg_match_all("/<!--\s*InstanceBeginEditable\s*name=\"(\w+)\"\s*-->.*?<!--\s*InstanceEndEditable\s*-->/s", $file, $matches);
			if ($match) {
				$fields = $matches[1];
			} else {
				$fields = array();
			}
		
		}
		
		$output = array();
		foreach($fields as $field) {
			if (in_array($field, array_keys($fields_open))) {
				// if this field is currently open, use a different style and add a hover box
				$hover = "";
				if ($fields_open[$field]["status_ID"] == 3) {
					$hover .= "Status: ".$fields_open[$field]["status"]." for ".$fields_open[$field]["schedule"]."\n";
				} else {
					$hover .= "Status: ".$fields_open[$field]["status"]."\n";
				}
				$hover .= "Editor: ".$fields_open[$field]["editor"]."\n";
				$hover .= "Last saved: ".$fields_open[$field]["last_saved"];
				$output[] = "<a href=\"JavaScript:click_field('$path', '$field');\" class=\"field_open\" title=\"$hover\">$field</a>";
				unset($fields_open[$field]);
			} else {
				$output[] = "<a href=\"JavaScript:click_field('$path', '$field');\">$field</a>";
			}
		}
		foreach($fields_open as $field_open) {
			// if there are any remaining open fields, they probably aren't in the content path yet, so show a link for those, too
			$hover = "";
			if ($field_open["status_ID"] == 3) {
				$hover .= "Status: ".$field_open["status"]." for ".$field_open["schedule"]."\n";
			} else {
				$hover .= "Status: ".$field_open["status"]."\n";
			}
			$hover .= "Editor: ".$field_open["editor"]."\n";
			$hover .= "Last saved: ".$field_open["last_saved"];
			$output[] = "<a href=\"JavaScript:click_field('$path', '".$field_open["field"]."');\" class=\"field_open\" title=\"$hover\">".$field_open["field"]."</a>";
		}
		if (CONTENT_TYPE != "dreamweaver") {
			// we don't allow adding new fields to Dreamweaver files because that would have to be done at the template level
			$output[] = "<a href=\"JavaScript:click_field('$path', '');\" class=\"link_new\">new field</a>";
		}
		$output = implode("<br />\n", $output);

		return $output;
	}
	
	
	function manage_page($page, $action, $new_name="", $new_definition="") {

		if (CONTENT_TYPE == "contemplate") {

			// get ready to use Contemplate's functions
			load_contemplate_assembler();
				
			// this loads the page definitions
			$page_definitions = get_page_definitions();

			// change the page definitions file
			switch ($action) {
				case "save": $page_definitions = update_page_definition($page, $page_definitions, $new_definition); break;
				case "copy": $page_definitions = copy_page_definition($page, $new_name, $page_definitions); break;
				case "rename": $page_definitions = rename_page_definition($page, $new_name, $page_definitions); break;
				case "delete": $page_definitions = delete_page_definition($page, $page_definitions); break;
			}
			
			// save the page definitions file with a backup
			// this only works with a single page definitions file; Contemplate supports multiple files, but it's an undocumented feature
			$result = save_file(CONTEMPLATE_DIRECTORY."/".READ_PAGE_DEFINITIONS_FROM, $page_definitions, 1);

		}
		
		if (CONTENT_TYPE == "dreamweaver") {

			// change the content file
			switch ($action) {
				case "copy": $result = copy_file(SITE_DIRECTORY."/".$page, SITE_DIRECTORY."/".$new_name); break;
				case "rename": $result = rename_file(SITE_DIRECTORY."/".$page, SITE_DIRECTORY."/".$new_name); break;
				case "delete": $result = delete_file(SITE_DIRECTORY."/".$page); break;
			}
			
			// build a result message based on the true/false result from these functions
			$result = ($result) ? 1 : "<p class=\"feedback_negative\">ERROR: Unable to $action the file.</p>" ;
			
		}
		
		if ($result === 1) {
			$action_past_tense = ($action == "copy") ? "copied" : $action."d" ;
			$result = "<p class=\"feedback_positive\">The page has been $action_past_tense.</p>";
		}
		
		return $result;
			
	}
	
	
	function get_contemplate_templates() {
	
		// we assume that templates are only stored in the templates folder
		// in fact, they could be stored elsewhere, but then we wouldn't be able to distinguish between templates and content files when displaying the editing menus
	
		// get ready to use Contemplate's functions
		load_contemplate_assembler();			
		
		$templates = get_recursive_listing("../templates", READ_FILES_RECURSIVELY, "");
		
		return $templates;
	
	}
	
	
	function get_content_files() {
	
		if (CONTENT_TYPE == "contemplate") {

			// get ready to use Contemplate's functions
			load_contemplate_assembler();			
			
			$content_files = Array();
			$content_directory_only = 1; // we won't show the directory if all the files are in the content directory

			$read_files_from = READ_FILES_FROM;
			$read_files_from = str_replace(" ", "", $read_files_from);
			$read_files_from = explode(",", $read_files_from);
			
			for ($j=0; $j<count($read_files_from); $j++) {
				$this_directory = $read_files_from[$j];
				if ((!strstr($this_directory, "../contemplate/"))&&(!strstr($this_directory, "../templates/"))) {
					//if (!strstr($this_directory, "../content/")) { $content_directory_only = 0; }
					$this_files = get_recursive_listing($this_directory, READ_FILES_RECURSIVELY, "");
					//$this_directory = str_replace("../", "", $this_directory);
					for ($k=0; $k<count($this_files); $k++) {
						$content_files[] = $this_directory.$this_files[$k];
					}
				}
			}
			
			//if ($content_directory_only) {
				for ($i=0; $i<count($content_files); $i++) {
					$content_files[$i] = str_replace("../content/", "", $content_files[$i]);
				}
			//}
		
			return $content_files;
	
		}

	}
	
	
	function get_content($path, $field, $rollback=0, $last_published=0) {
		// since we're generating the status menus here, figure out which statuses are available to this user
		$role = initialize("role", "session", "integer");
		$available_statuses = ($role > 1) ? "1,2,3,4" : "1,2" ;
		
		if (CONTENT_TYPE == "contemplate") {
		
			// get ready to use Contemplate's functions
			load_contemplate_assembler();
				
			// get the content field; we're going to need this to look for a background color, even if latest content is in the database
			$source = get_file("../".$path, "filesystem", 0);
			$file_type = (strpos($path, "xml") !== false) ? "xml" : "html" ;
			
		}
		
		// first, see if we're requesting a specific version of the content from the database
		if ($rollback) {
			$query = "SELECT content FROM content WHERE ID='$rollback'";
			$result = db_do_query($query);
			$count = db_num_rows($result);
			if ($count) {
				$row = db_get_row($result, 0);
				extract($row);
				$status_menu = make_menu("Status...", "status", "status", "ID", "name", "ID=".$available_statuses, 0, 0, "", 0);
				$date_menu = make_date_menus("", "content_edit", "schedule", 0);
				$time_menu = make_time_menus("", "content_edit", "schedule", 15, 0);
				$ID = $rollback;
			}
		}
		
		// next, see if an unpublished version of this field is in our database; if so, return that content instead of the live content
		if ((!isset($content))&&(!$last_published)) {
			$timezone = initialize("timezone", "session", "text");
			$query = "SELECT content.ID, content, status, COALESCE(DATE_ADD(schedule, INTERVAL $timezone HOUR), '0000-00-00 00:00:00') AS schedule FROM content WHERE path='$path' AND field='$field' ORDER BY date DESC LIMIT 1";
			$result = db_do_query($query);
			$count = db_num_rows($result);
			if ($count) {
				$row = db_get_row($result, 0);
				if ($row["status"] < 4) {
					extract($row);
					$status_menu = make_menu("Status...", "status", "status", "ID", "name", "ID=".$available_statuses, 0, 0, "", $status);
					list($schedule_date, $schedule_time) = explode(" ", $schedule);
					if ($schedule_date == "0000-00-00") { $schedule_time = ""; }
					$date_menu = make_date_menus("", "content_edit", "schedule", $schedule_date);
					$time_menu = make_time_menus("", "content_edit", "schedule", 15, $schedule_time);
				}
			}
		}
		
		// otherwise, get the content out of the site storage
		if (!isset($content)) {

			if (CONTENT_TYPE == "contemplate") {
			
				if ($field) {
					$content = get_field($source, $field, $file_type);
				} else {
					// if no field was specified, we're probably creating a new field
					$content = "";
				}
	
			} else if (CONTENT_TYPE == "database") {
			
				if ($field) {
					list($table, $ID1) = explode("/", $path);

					db_connect_content();
					$query = "SELECT `".DATABASE_CONTENT_COLUMN."` FROM `".$table."` WHERE `".DATABASE_ID1_COLUMN."`='".$ID1."' AND `".DATABASE_ID2_COLUMN."`='".$field."'";
					$result = db_do_query($query);
					$row = db_get_row($result, 0);
					$content = $row[DATABASE_CONTENT_COLUMN];
					db_connect_editor();
				} else {
					// if no field is found, we're probably creating a new field
					$content = "";
				}
			
			} else if (CONTENT_TYPE == "dreamweaver") {
			
				$file = file_get_contents("../".$path);
				$match = preg_match("/<!--\s*InstanceBeginEditable\s*name=\"$field\"\s*-->(.*?)<!--\s*InstanceEndEditable\s*-->/s", $file, $matches);
				if ($match) {
					$content = $matches[1];
				} else {
					$content = "";
				}
	
			}
		
			$status_menu = make_menu("Status...", "status", "status", "ID", "name", "ID=".$available_statuses, 0, 0, "", 0);
			$date_menu = make_date_menus("", "content_edit", "schedule", 0);
			$time_menu = make_time_menus("", "content_edit", "schedule", 15, 0);
			$ID = "";
		}
		
		// look for a background color for this field
		if (CONTENT_TYPE == "contemplate") {
			$bg_color = get_field_bg_color($source, $field, $file_type);
		} else {
			$bg_color = "";
		}
		
		// remove slashes if not using magic quotes
		if (CONTENT_TYPE == "database") {
			if (!ini_get("magic_quotes_runtime")) {
				$content = stripslashes($content);
			}
		}

		// clean up high-ASCII characters, which will break the decodeURIComponent function in ajax_unserialize (and display badly in some browsers)
		$content = convert_characters($content);

		// set up the schedule menu for display
		$schedule_menu = "<div class=\"stack_left\">$date_menu</div><div class=\"stack_left\">$time_menu</div>";
		$schedule_menu = str_replace("&nbsp;", "", $schedule_menu);
		
		// get the name of the person using the file if applicable
		$lock_name = check_dreamweaver_lock($path);

		$response = compact("content", "bg_color", "status_menu", "schedule_menu", "lock_name", "ID");
		$response = ajax_serialize($response);
		return $response;
	}
	
	
	function save_content($path, $field, $content) {
		// clean up high-ASCII characters, which will break the decodeURIComponent function in ajax_unserialize (and display badly in some browsers)
		$content = convert_characters($content);

		if (CONTENT_TYPE == "contemplate") {

			// get ready to use Contemplate's functions
			load_contemplate_assembler();
			
			// get the content file
			$source = get_file("../".$path, "filesystem", 0);
			$file_type = (strpos($path, "xml") !== false) ? "xml" : "html" ;
			
			// find out if this field is in the file yet
			$fields = get_field_names($source, $file_type);
			$action = (in_array($field, $fields)) ? "update" : "add" ;
	
			if ($action == "add") {
				// copy the last row of the file, add our new content, and append it to the file
				$last_field = $fields[(count($fields) - 1)];
				$last_row = get_row($source, $last_field, $file_type);
				$new_row = update_row($last_row, $file_type, $field, $content);
				$new_source = str_replace($last_row, $last_row."\n".$new_row, $source);
			} else if ($action == "update") {
				// update the existing row in place
				$original_row = get_row($source, $field, $file_type);
				$new_row = update_row($original_row, $file_type, $field, $content);
				$new_source = str_replace($original_row, $new_row, $source);
			}
	
			// save the new row to the file
			$result = save_file("../".$path, $new_source);
		
		} else if (CONTENT_TYPE == "database") {

			list($table, $ID1) = explode("/", $path);

			db_connect_content();

			// find out if this record is in the database yet
			$query = "SELECT `".DATABASE_CONTENT_COLUMN."` FROM `".$table."` WHERE `".DATABASE_ID1_COLUMN."`='".$ID1."' AND `".DATABASE_ID2_COLUMN."`='".$field."'";
			$result = db_do_query($query);
			$count = db_num_rows($result);
			$action = ($count) ? "update" : "add" ;
	
			if ($action == "add") {
				$query = "INSERT INTO `".$table."` (`".DATABASE_ID1_COLUMN."`, `".DATABASE_ID2_COLUMN."`, `".DATABASE_CONTENT_COLUMN."`) VALUES ('".$ID1."', '".$field."', '".$content."')";
			} else if ($action == "update") {
				$query = "UPDATE `".$table."` SET `".DATABASE_CONTENT_COLUMN."`='".$content."' WHERE `".DATABASE_ID1_COLUMN."`='".$ID1."' AND `".DATABASE_ID2_COLUMN."`='".$field."'";
			}
			$result = db_do_query($query);

			db_connect_editor();

		} else if (CONTENT_TYPE == "dreamweaver") {
		
			// update the content in the file
			$file = file_get_contents("../".$path);
			if (preg_match("/(.*<!--\s*InstanceBeginEditable\s*name=\"$field\"\s*-->)(.*?)(<!--\s*InstanceEndEditable\s*-->.*)/s", $file, $matches)) {
				// we use preg_match here because with preg_replace, any $dd variables in $content are interpreted as backreferences
				$new_file = $matches[1].$content.$matches[3];
			} else {
				$new_file = "";
			}

			// save the new content to the file
			if ($new_file) {
				$result = save_file("../".$path, $new_file);
			} else {
				$result = "";
			}

		}
		
		return $result;
	}
	
	
	function delete_content($path, $field, $content) {

		if (CONTENT_TYPE == "contemplate") {

			// get ready to use Contemplate's functions
			load_contemplate_assembler();
			
			// get the content file
			$source = get_file("../".$path, "filesystem", 0);
			$file_type = (strpos($path, "xml") !== false) ? "xml" : "html" ;
			
			// remove the existing row from the file
			// this could just be a str_replace, but we want to get the trailing white space, too
			$original_row = get_row($source, $field, $file_type);
			$new_source = preg_replace("/(.*)".preg_quote($original_row, "/")."\s*[\n\r]*(.*)/s", "$1$2", $source);
	
			// save the new row to the file
			$result = save_file("../".$path, $new_source);
		
		} else if (CONTENT_TYPE == "database") {

			list($table, $ID1) = explode("/", $path);

			db_connect_content();
			$query = "DELETE FROM `".$table."` WHERE `".DATABASE_ID1_COLUMN."`='".$ID1."' AND `".DATABASE_ID2_COLUMN."`='".$field."'";
			$result = db_do_query($query);
			db_connect_editor();

		}

		return $result;

	}
	
	
	function check_contemplate_version() {
		// return 1 if the version is compatible, an error message if not
		
		$required_version = "2.0"; // we assume that any sub-version will be compatible
		$assembler_text = file_get_contents(CONTEMPLATE_DIRECTORY."/assembler.php");
		
		if (preg_match("/version (\d+\.\d+)/", $assembler_text, $matches)) {
			$installed_version = $matches[1];
		} else {
			$installed_version = 0;
		}

		if ($installed_version < $required_version) {
			return "<p class=\"feedback_negative\">Error: This version of ".EDITOR_NAME." requires version $required_version or newer of Contemplate.<br /><br />If you want to use a different content type, you can specify that in your config/constants.php file.</p>";
		} else {
			return 1;
		}
	}
	

	function check_dreamweaver_lock($path) {
		// if we see a Dreameaver lock file and none of the fields in this file are open by us, then the file is locked by Dreamweaver
		// return the name of the person who has it locked, or an empty string if it is not locked
		$lock_name = "";
		if ((USE_LCK_FILES)&&(CONTENT_TYPE != "database")) {
			if (file_exists(SITE_DIRECTORY."/".$path.".LCK")) {
				$query = "
					SELECT content.ID
					FROM content
					INNER JOIN (SELECT MAX(ID) AS ID FROM content GROUP BY path, field) AS content_latest ON content_latest.ID=content.ID
					WHERE path='$path'
					AND FIND_IN_SET(content.status, '1,2,3')
				";
				$result = db_do_query($query);
				$count = db_num_rows($result);
				if (!$count) {
					$lock_file = file_get_contents(SITE_DIRECTORY."/".$path.".LCK");
					list($lock_name, $lock_email) = explode("||", $lock_file);
				}
			}
		}
		return $lock_name;
	}
	
	
	function set_dreamweaver_lock($path) {
		// if we edit a file, create a Dreamweaver lock file to warn Dreamweaver, Contribute or other users not to edit it
		// return 1 if the file was created or already existed; return 0 if it should have been created but we were not able to create
		$result = 1;
		if ((USE_LCK_FILES)&&(CONTENT_TYPE != "database")) {
			if (!file_exists(SITE_DIRECTORY."/".$path.".LCK")) {
				$lock_name = initialize("username", "session", "text");
				$lock_email = get_value("user", "name", "email", $lock_name);
				$lock_file = "$lock_name||$lock_email";
				$file = fopen(SITE_DIRECTORY."/".$path.".LCK", "w");
				if ($file) {
					fwrite($file, $lock_file);
					fclose($file);
				} else {
					$result = 0;
				}
			}
		}
		return $result;
	}
	
	
	function clear_dreamweaver_lock($path) {
		// if there are no longer any unpublished fields in this file, remove the lock file now
		// return 1 if the file didn't exist or did not need to be deleted; return 0 if it should have been deleted but we were not able to delete
		$result = 1;
		if ((USE_LCK_FILES)&&(CONTENT_TYPE != "database")) {
			if (file_exists(SITE_DIRECTORY."/".$path.".LCK")) {
				$query = "
					SELECT content.ID
					FROM content
					INNER JOIN (SELECT MAX(ID) AS ID FROM content GROUP BY path, field) AS content_latest ON content_latest.ID=content.ID
					WHERE path='$path'
					AND FIND_IN_SET(content.status, '1,2,3')
				";
				$result = db_do_query($query);
				$count = db_num_rows($result);
				if (!$count) {
					$result = delete_file(SITE_DIRECTORY."/".$path.".LCK");
				}
			}
		}
		return $result;
	}
	
	
	function load_contemplate_assembler() {
		// this places functions into the global scope, but variables are only in the function scope,
		// so we need to declare those as globals here and in any function that calls this

		// in order for the assembler functions to work, we have to declare all the globals that they use
		GLOBAL $error_log, $file_missing;
		GLOBAL $template, $content_file;
		GLOBAL $latest_mod_date, $latest_mod_date_formatted;
		GLOBAL $script_location, $request, $URL_rewriting_flag;

		// now we can include the assembler script
		// the ampersand avoids errors like "constant SHOW_ERRORS already defined"; is there a better solution for that? without cluttering Contemplate's constants file with "is_defined" tests?
		// we still need to check function_exists in the assembler because those cause a fatal error; but then users don't have to see that extra code 
		$_GET["skip_processing"] = 1;
		@include_once CONTEMPLATE_DIRECTORY."/assembler.php";

		// this makes the assembler think we're running from the contemplate directory
		$script_location = CONTEMPLATE_DIRECTORY."/";
	}
	
	
	function get_contemplate_page_definition($page) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
		
		// this loads the page definitions file (we're assuming there is only one)
		$page_definitions = get_page_definitions();

		// get the arguments
		$page_definition = get_page_definition($page_definitions, $page);
		
		return $page_definition;
	}
	
	
	function get_contemplate_file($file_name) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get the file source via the filesystem
		$source = get_file("../".$file_name, "filesystem", 0);

		return $source;
	}
	
	
	function get_contemplate_field_names($source, $file_name) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get the content field names and build a link for each one
		$file_type = (strpos($file_name, "xml") !== false) ? "xml" : "html" ;
		$fields = get_field_names($source, $file_type);

		return $fields;
	}
	
	
	function get_contemplate_field($source, $field, $file_name) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get the content field names and build a link for each one
		$file_type = (strpos($file_name, "xml") !== false) ? "xml" : "html" ;
		$field = get_field($source, $field, $file_type);

		return $field;
	}
	
	
	function get_contemplate_form_names($source) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get the content field names and build a link for each one
		$fields = get_form_names($source);

		return $fields;
	}
	
	
	function get_contemplate_database() {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// connect to the database defined in Contemplate's config file
		get_database("content");
	}
	
	
	function get_contemplate_database_tables() {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get the database tables allowed from Contemplate's config file
		$database_tables = CONTENT_DATABASE_TABLES;
		$database_tables = str_replace(" ", "", $database_tables);
		if ($database_tables) {
			$database_tables = explode(",", $database_tables);
		} else {
			$database_tables = array();
		}

		return $database_tables;
	}
	
	
	function get_contemplate_database_keys($table, $key_column) {
		// get ready to use Contemplate's functions
		load_contemplate_assembler();
			
		// get ready to connect to Contemplate's database
		get_contemplate_database();
			
		// get the database values for the given table and key column
		$keys = array();
		$query = "SELECT `$key_column` AS `key` FROM `$table` ORDER BY `$key_column`";
		$result = db_do_query($query);
		$count = db_num_rows($result);
		for ($i=0; $i<$count; $i++) {
			$row = db_get_row($result, $i);
			$keys[] = $row["key"];
		}
		$keys = implode(",", $keys);

		return $keys;
	}
	
	
	function get_images() {
		// build a JavaScript list of images to give to the TinyMCE image dialog

		$output = array();
		$directories = explode(",", SITE_IMAGE_DIRECTORIES);

		foreach($directories as $directory) {
			$directory_full_path = (SITE_SUBDIRECTORY) ? str_replace(SITE_SUBDIRECTORY, SITE_DIRECTORY, $directory) : SITE_DIRECTORY.$directory ;
			$images = list_directory($directory_full_path, "files", 1);
			if ($images !== false) {
				//sort($images);
				foreach($images as $image) {
					$directory_and_image = "$directory/$image";
					$output[] = "[\"$directory_and_image\", \"$directory_and_image\"]";
				}
			}
		}
		
		if (count($output)) {
			$output = implode(",\n", $output);
			$output = "var tinyMCEImageList = new Array(\n".$output."\n);";
			header("Content-type: text/javascript");
			return $output;
		} else {
			return "";
		}
	}
	
	
	function strip_template($file_contents) {
		// if we're running within the parent system in integration mode, strip out the header and footer
		if (THIS_PAGE_WRAPPED) {
			$file_contents = preg_replace("/(.*)<html.*\/title>(.*)<\/head>.*<\?php[^>]+include[^>]+HEADER_FILE[^>]+\?>(.*)<\?php[^>]+include[^>]+FOOTER_FILE[^>]+\?>.*/s", "$1$2$3", $file_contents);
		}
		return $file_contents;
	}
	
?>
