<?php if (!defined('PmWiki')) exit();

$RecipeInfo['ZAP']['Version'] = '2007-12-04';
$FmtPV['$ZAPversion'] = "'2007-12-04'";

## DESCRIPTION:  The ZAP recipe extends forms capability on PmWiki, by adding a flexible and easily extensible forms processing engine. For info, see docs at http://www.pmwiki.org/wiki/Cookbook/ZAP, or visit the ZAP demo site at WWW.ZAPSITE.ORG.  Author: Dan Vis  <editor �t fast d�t st>, Copyright 2006.  

## LICENSE:  You can redistribute this software and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.


//REQUIRED PMWIKI PARAMETERS
$HandleActions['zap'] = 'ZAPengine';
$PageAttributes['passwdzap'] = 'Set ZAP forms password: ';
SDV($DefaultPasswords['zap'],'');

//# PRESERVE POST VALUES
//foreach ($_POST as $k=>$v) {
//	if(!is_array($v)) $InputValues[$k] = htmlspecialchars($v);
//	}

#######################################
###         Main ZAP ENGINE         ###
#######################################

function ZAPengine() {
## ZAP INITIALIZATION
// Is there some reason not to declare some of these global?  Someone suggested it might be a security vulnerability. ???
	global $pagename, $ScriptUrl, $msg, $msg_form, $ZAParray, $PCache, $Version, $ZAPfieldlabel;
	if (substr($Version, 0, 10) != "pmwiki-2.2") ZAPabort('version', "ZAP requires a later version of PmWiki. Please upgrade to the latest beta. Thank you! ");
	$pagename = ResolvePageName($pagename);
	$ZAParray = ZAPsecure();
	$msg['submit'] = "Form submitted. ";
	if (!isset($ZAParray['nextpage'])) $ZAParray['nextpage'] = $pagename;
	if (!isset($ZAParray['datapage'])) $ZAParray['datapage'] = $pagename;
//  print_r($ZAParray);die();

## ZAP FORMS PROCESSING COMMANDS
	foreach ($ZAParray as $field => $value) {
		$value = preg_replace('/\\{(\\w+)\\}/e', 'ZAPfieldreplace("$1", $ZAParray)', $ZAParray[$field]);
		if (($field == "msg") || (substr($field, 0, 4) == "msg_")) {
			if (strlen($field) == 3) $msg_form['clear'] = $value . " ";
			else $msg_form[substr($field, 4)] = $value . " ";
			continue;
			}

		if (($field == "warn") && ($value != "")) ZAPabort($field, $value);

		if ($field == "passdata") {
			$f = explode(",", $value);
			foreach ($f as $ff) {
				if ((isset($ZAParray[$ff])) && ($ZAParray[$ff] != '')) $passdata .= "?$ff=$ZAParray[$ff]";
				}
			continue;
			}

		if (substr($field, 0, 8) == "savedata") ZAPsave($ZAParray['datapage'], $value, '', 'Fields have been saved. '); 
		
		if (substr($field, 0, 9) == "erasedata") ZAPsave($ZAParray['datapage'], $value, '', 'Fields have been erased. '); 
		
		if (substr($field, -4, 4) == "list") {
			$value = trim($value);
			$value = trim($value, "\x7f..\xff\x0..\x1f");
			if ((substr($value, 0, 1) == "+") || (substr($value, 0, 1) == "-")) {
				$current = PageTextVar($ZAParray['datapage'], $field);
				$value = ZAPcsv($value, $current);
				}
			else $value = ZAPcsv($value);
			}

		if (substr($field, -4, 4) == "page") $value = ZAPpageshortcuts($value);

		if (substr($field, 0, 4) == "link") $value = "$ScriptUrl?n=$value";

		if (substr($field, 0, 8) == "required") {
			$f = explode(",", $value);
			foreach ($f as $ff) {
				if ($ZAParray[$ff] == "") {
					if (isset($ZAPfieldlabel[$ff])) $out = $out . "$ZAPfieldlabel[$ff] ";
					else $out = $out . "$ff ";
					}
				}
			if ($out != '') {
				$out = substr($out, 0, -1);
				ZAPabort('required', "The following fields are missing: $out. ");
				}
			continue;
			}

		
## ZAP COMMANDS YOU CANNOT POST
		if ((substr($field, 0, 8) == "datapage") || (substr($field, 0, 6) == "target")) {
			if (isset($_POST[$field])) ZAPabort('post', "The \"$field\" field cannot be processed this way. ");
			$value = ZAPpageshortcuts($value);
			$ZAParray['datapage'] = $value;
			}

		if ((substr($field, 0, 2) == "if") || (substr($field, 0, 8) == "validate")) {
			if (isset($_POST[$field])) ZAPabort('post', "The \"$field\" field cannot be processed this way. ");
			$ZAPcond = substr($value, 0, strpos($value, " ? "));
			$value = substr($value, strpos($value, " ? ") + 3);
			$ZAPacts[0] = '';
			if (! strpos($value, " : ")) $ZAPacts[1] = explode(" , ", substr($value, strpos($value, " ? ")));
			else {
				$ZAPacts[1] = explode(" , ", substr($value, 0, strpos($value, " : "))); //true
				$ZAPacts[0] = explode(" , ", substr($value, strpos($value, " : ") + 3)); //false
				}
			if (substr($field, 0, 2) == "if") {
				if (CondText($pagename, "! $ZAPcond", true)) $c = "1";
				else $c = "0";
				}
			else {
				$valfield = substr($field, 8);
				if (substr($valfield, 0, 1) == '_') $valfield = substr($valfield, 1);
				if (!preg_match($ZAPcond, $ZAParray[$valfield])) $c = "0";
				else $c = "1";
				}
			if (!is_array($ZAPacts[$c])) $ZAPacts[$c] = array($ZAPacts[$c]);
			foreach ($ZAPacts[$c] as $act) {
				$f = substr($act, 0, strpos($act, "="));
				$ZAParray[$f] = substr($act, strpos($act, "=") + 1);
				}
			continue;
			}

## ZAP PLUGIN EXTENSIONS
		if (strpos($field, "_")) $func = substr($field, 0, strpos($field, "_"));
		else ($func = $field);
		if ((function_exists("ZAPX$func")) && ($func == strtolower($func))) {
			if (isset($_POST[$field])) ZAPabort('post', "The \"$field\" field cannot be processed as a POST value. ");
			$ZAPfunc = "ZAPX$func";
			if (ZAPauth($func, $pagename, 'Commands') == true) $value = $ZAPfunc($value, $field);
			else ZAPabort('not_enabled', "The $func command is not enabled for this page. ");
			}
		$ZAParray[$field] = $value;
		}

## ZAP CLOSING STEPS
	unset($PCache[$pagename]);
	if (($ZAParray['nextpage'] != $pagename) || (isset($passdata))) Redirect(FmtPageName($ZAParray['nextpage'] . $passdata, $pagename));
	ZAPmessages();
	}


#######################################
###      ZAP UTILITY FUNCTIONS      ###
#######################################

## Produces message output from msg array.  Each element in the array is checked for a replacement at Site.ZAPMessages
## (can be used for sitewide customization or easy internationalizations) and then checked for a form defined substitute.
## That is, if msg_submit is set to a value in the form it will override the default or config values for that entry. 
## Setting a msg field in the form will clear the entire message array and reset to the single value in the msg field.
function ZAPmessages() {
	global $msg, $msg_form, $pagename, $MessagesFmt;
	foreach ($msg as $mi => $mv) {
		if (ZAPconfig($mi, $mv, "Site.ZAPMessages") != '') $msg[$mi] = ZAPconfig($mi, $mv, "Site.ZAPMessages"); 
		}
	if (is_array($msg_form)) $msg = array_merge($msg, $msg_form); 
	if (isset($msg_form['clear'])) $m = $msg_form['clear'];
	else $m = implode(' ', $msg);
	$MessagesFmt[] = "<div class='wikimessage'>$[$m]</div>";
	HandleBrowse($pagename);
	die();
	}

## Adds a message to the message array, and shut down processing immediately.
function ZAPabort($id, $m) {
	global $msg;
	$msg['id'] = $m;
	ZAPmessages();
	}

## Can be used to save or erase Text Vars on a page.
function ZAPsave($page, $value, $text='', $message='') {
	global $pagename, $ZAParray, $msg;
	if ((!ZAPauth($pagename, $page, 'Targets')) && ($pagename != $page)) ZAPabort('save', "You are not authorized to write to this page. ");
	$oldpage = ReadPage($page);
	$newpage = $oldpage;
// Shouldn't savedata escape page text for these kinds of functions for directives? Or leave to functions...
	if ($text != '') $newpage['text'] = $text;
	else $newpage['text'] = $oldpage['text'];
	if ($value != '') {
		$escapein = array('(:', ':)', '$');
		$escapeout = array('( :', ': )', '-$-');
		$f = explode(",", $value);
// It would be better and easier to use the -field syntax and get rid of erasedata. Maybe one message... 'Data fields updated'
		foreach ($f as $ff) {
			$ff = trim($ff);
			if ($message == 'Fields have been erased. ') {
// Make the ending \n optional using ? in the pattern for cleaner replacements.
// Make sure all new fields get save to a new line.  Double check this is working clean.
				$newpage['text'] = preg_replace('/\\(:'.$ff.': (.*?):\\)\n/s', '', $newpage['text']);
				}
			else {
				$v = str_replace($escapein, $escapeout, $ZAParray[$ff]);
				if (strpos($newpage[text], "(:$ff: ") !== false) $newpage['text'] =  preg_replace('/\\(:'.$ff.': (.*?):\\)/s', "(:$ff: $v:)", $newpage['text'], 1);
				else $newpage['text'] .= "(:$ff: $v:)\n";
				$newpage['text'] = str_replace('-$-', '$', $newpage['text']);
				}
			}
		}
	$msg['savemessage'] = $message;
	UpdatePage($page, $oldpage, $newpage);
	return;
	}

## Used for ZAP's list and group management functions. To add or remove items from list just set field equal to +Item,-Item.
## If no $current is supplied, it trims each items and returns $value.
function ZAPcsv($value, $current='') {
	if ($current != '') {
		$list = ",,$current,,";
		$i = explode(",", $value);
		foreach ($i as $ii) {
			$ii = trim($ii);
			$flag = substr($ii, 0, 1);
			$item = trim(substr($ii, 1));
			if (($flag == "-") && (strpos($list, ",$item,"))) $list = str_replace(",$item,", ",", $list);			
			if (($flag == "+") && (! strpos($list, ",$item,"))) $list = substr($list, 0, -1) . $item . ",,";
			}
		$value = substr($list, 2, -2);
		}
	$i = explode(",", $value);
	foreach ($i as $ii) {
		$out[] = trim($ii);
		}
	$value = implode(",", $out);	
	return $value;
	}	

## Retrieves markup from a template for use in new pages, pages insertions, emails, etc. (type is defined by $x)
## First it looks for appropriate field in the form (if $x = email, emailtemplate) for the template's page name.
## If not set, it looks for the default location relevant to the current page, ie Group.Name-template.
## Finally markup content is returned, or false if no value found or user not authorized to read the template. 
function ZAPtemplate($x) { 
	global $pagename, $ZAParray;
	if (isset($ZAParray[$x . "template"])) $xx = ZAPpageshortcuts($ZAParray[$x . "template"]);
	else $xx = $pagename . "-template";
	if ((PageExists($xx)) && (CondAuth($xx, 'read'))) { 
		$page = ReadPage($xx);
		$r = $page['text'];
		}
	else return false;
	$r = preg_replace('/\\{\\$\\$(\\w+)\\}/e', 'ZAPfieldreplace("$1", $ZAParray)', $r);
	return $r;
	}

## Used to perform field replacements in the templating engine above and other places in the code.
function ZAPfieldreplace($x, $ZAParray) {
	if (isset($ZAParray[$x])) return ($ZAParray[$x]);
	return "\{$x}";
	}

## Used in nearly all functions involving page names allowing several useful shortcuts. See docs for syntax.
## Also verifies that pages cannot be set to the site group unless enabled via a special config file setting.
function ZAPpageshortcuts($v) {
	global $pagename, $FmtPV, $EnableZAPsiteEdits, $SiteAdminGroup;
	if (strpos($v, "/")) $v = str_replace("/", ".", $v);
	if (! strpos($v, ".")) return $pagename;
	$vv[0] = substr($v, 0, strpos($v, "."));  
	$vv[1] = substr($v, strpos($v, ".") + 1);  
	if (strpos($vv[1], "?")) {
		$vv[2] = substr($vv[1], strpos($vv[1], "?"));
		$vv[1] = substr($vv[1], 0, strpos($vv[1], "?"));
		}
	if ((($vv[0] == $SiteAdminGroup) || ($vv[0] == "SiteAdmin")) && ($EnableZAPsiteEdits != true)) ZAPabort('site', "Group $SiteAdminGroup is blocked on this system. ");
	$pn = explode(".", $pagename);
	$rr1 = array('*','^'); 
	$rr2 = array($pn[0],$pn[1]);
	$vv[0] = str_replace($rr1, $rr2, $vv[0]);
	$time = time();
// Should there be some way to make these shortcuts configurable--so you can put some of this in extensions?
	$order = substr($FmtPV['$orderpage'], 1, -1);
	$rr1 = array('*','^','@','$','+','~','!'); 
	$rr2 = array($pn[1],$pn[0],$GLOBALS[Author],$order,$time,"Profiles","Categories");
	$vv[0] = str_replace($rr1, $rr2, $vv[0]);
	$vv[1] = str_replace($rr1, $rr2, $vv[1]);
	if (substr($vv[1], -1) == "#") $vv[1] = substr($vv[1], 0, -1) . ZAPthread($vv[0]);
	$v = $vv[0] . "." . $vv[1];
	if (strpos($v, "#") != false) $v = MakePageName($pagename, substr($v, 0, strpos($v, "#"))) . substr($v, strpos($v, "#"));
	else $v = MakePageName($pagename, $v) . $vv[2];
	return $v;
	}

## Used to find the highest numbered page in a group and return that number plus one.
SDV($ZAPthreadstart,'1000');
function ZAPthread($g) {
	global $ZAPthreadstart;
	$e = $ZAPthreadstart - 1;
	$gg = explode(",", $g);
	foreach($gg as $ggg) {
		foreach(ListPages("/^$ggg\\.\\d/") as $n) {
		$n = substr($n,strlen($ggg)+1);
		if (! ereg("^[0-9]+$", $n)) continue;
		$e = max($e,$n);
		}
	}
	$e = $e + 1;
	return $e;
	}

## This function is used to verify that a form is properly submitted (to prevent various potential security risks)
## Basically, it checks that a required session variable was set.  Then, sets up the $ZAParray variable for processing.
function ZAPsecure() {
	global $pagename;
	if(!CondAuth($pagename, "zap")) ZAPabort('submit', "You are not authorized to submit this form. ");
	$formpage = str_replace(".", "", $pagename);
	$formkey = $_REQUEST['ZAPkey'];
	session_start();
	if ($_SESSION['ZAP']["$formpage$formkey"]['secure'] != "set") ZAPabort('key', "An error occurred. Form could not be processed.");
	$ZAParray = $_SESSION['ZAP']["$formpage$formkey"];
	foreach($_SESSION['ZAP'] as $i => $ii) unset($_SESSION['ZAP'][$i]);  
	foreach ($_POST as $field => $value) {
		if (is_array($value)) $value = implode(",", $value);
		$_POST[$field] = stripmagic($value);
		}
	foreach($_POST as $f => $v) {
// These escapes are to protect hijacking the if/validate commands. Not sure they are needed... Hmmm...
		$escapein = array(' ? ', ' : ', ' , ');
		$escapeout = array('&nbsp;?&nbsp;', '&nbsp;:&nbsp;', '&nbsp;,&nbsp;');
		if (!isset($ZAParray[$f])) $ZAParray[$f] = str_replace($escapein, $escapeout, $v);
		}
	return $ZAParray;
	}

## This function allows you to retrieve a text var value (field $var on page $page) if defined.  If not the default value ($def) is returned. 	
function ZAPconfig($var, $def, $page) {
	$value = PageTextVar($page, $var);
	if ($value == '') {
		$page = ReadPage($page);
		$value = preg_match('/'.$value.'\\=\\"(.*?)\\"/s', $page['text'], $v); 
		$value = $v[1];
		}
	if ($value == '') $value = $def;
	return $value;
	}

## This function is used to check various kinds of permissions in ZAP--namely commands and targetss
## ZAPauth('edit', 'Test.Main', 'Commands') will verify whether or not the edit command is allowed for page Test.Main
## ZAPauth('Test.One', 'Test.Two', 'Targets') verifies whether a form on Test.One can write to Test.Two
## The permissiable values are all set on Site.ZAPCommands or Site.ZAPTargets as normal PTV's
function ZAPauth($var, $p, $check) {
	global $SiteAdminGroup;
	if (! PageExists("$SiteAdminGroup.ZAP$check")) return true;
	if ($check == 'Targets') $var = str_replace(".", "_", $var);
	$authlist = PageTextVar("$SiteAdminGroup.ZAP$check", $var);
	if (($authlist == '') && ($check == "Targets")) $authlist = PageTextVar("$SiteAdminGroup.ZAP$check", substr($var, 0, strpos($var, "_")));
	if ($authlist == '') return false;
	$list = explode(",", $authlist);
	foreach ($list as $a) {
		$page = $p;
		if (substr($a, -1) == "*") $page = substr($page, 0, strlen($a) - 1) . "*";
		elseif (strpos($a, '.') == NULL) $page = substr($page, 0, strpos($page, '.'));
		if ($a == $page) return true;
		}
	return false;
	}


#######################################
###           ZAP MARKUPS           ###
#######################################

## This markup sets up a ZAP form with a special secuity session variable to verify the form was setup properly
Markup('zapform', '<input', '/\(:zapform(.*?):\)/ei', "ZAPform('$1')");
Markup('zapend', 'inline', '/\(:zapend:\\)/', '</form>');
function ZAPform($d) {
	if (isset($_REQUEST['q'])) return '';
	global $pagename, $ScriptUrl, $EnablePathInfo;
	if (strpos($d, "upload")) $u = "enctype=multipart/form-data ";
	$arg = ParseArgs($d);
	if (isset ($arg['action'])) $a = "action=$arg[action] method=post ";
	else $a = "action=$ScriptUrl".($EnablePathInfo==1 ? "/".str_replace(".","/",$pagename) : "?n=$pagename")." method=post ";
	if (isset($arg['key'])) $formkey = "name=$arg[key] ";
	else $formkey = '';
	ZAPinput("secure=set $arg[key]");
	return "(:input form $formkey$u$a:)(:input hidden action zap:)(:input hidden ZAPkey \"$arg[key]\":)";
	}

## Input values can be sent directly to the ZAP array via session values using this markup.
## Values submitted this way cannot be overwritten by post values and are thus safer. Required syntax for most ZAP commands.
Markup('zapinput', 'inline', '/\(:zap (.*?):\)/ei', "ZAPinput('$1')");
function ZAPinput($x) {
	if (isset($_REQUEST['q'])) return '';
	global $pagename;
// Do I need stripslashes or magic slashes?
	$x = stripslashes($x);
	$arg = ParseArgs($x);
	foreach ($arg['#'] as $i => $ii) {
// Is this set up right with stripslashes now?  What about just " or '?  Double check this...
		if (substr($ii, 0, 2) == '\"') $arg['#'][$i] = substr($arg['#'][$i], 2, -2);
		}
// What if entered key=name to input.  Shouldn't it be made to accept that?
	$formkey = $arg['#'][3];
	$f = $arg['#'][0];
	$v = $arg['#'][1];
	if ($f == '') {
		$f = $v;
		$v = '';
		}
	$formpage = trim(str_replace(".", "", $pagename));
	@session_start();
	$_SESSION['ZAP']["$formpage$formkey"][$f] = $v;
	session_write_close();
	return;
	}

## Put this markup on a page and the specified form will be submitted as soon as the page is loaded
## action=edit or stop will suspend submission. POST values will be ignored in auto submit forms.
// Actually, better check. I think hidden and default POST values will be used... If so, that's OK, of course.
Markup('zapsubmit', '>inline', '/\(:zapsubmit(.*?):\)/ei', "ZAPsubmit('$1')");
function ZAPsubmit($formkey) {
	if (isset($_REQUEST['q'])) return '';
	if (($_GET['action'] == "edit") || ($_GET['action'] == "stop")) return '';
	global $ZAParray, $pagename, $PageUrl;
	$formpage = trim(str_replace(".", "", $pagename));
	$formkey = trim($formkey);	
	if (($_SESSION['ZAP']["$formpage$formkey"]['nextpage'] == $pagename) || ($_SESSION['ZAP']["$formpage$formkey"]['nextpage'] == '')) return "ZAPsubmit error. Field nextpage required. ";
	$_POST['ZAPkey'] = trim($key);
	ZAPengine();
	return;
	}

## This markup allows you to create a link that will submit a zap form when clicked
## POST values can be used in link submit forms.
Markup('zaplink','>inline','/\(:zaplink(.*?):\)/ei', "Keep(ZAPlink('$1'))");
function ZAPlink($args) {
	if (isset($_REQUEST['q'])) return '';
	global $pagename, $ScriptUrl;
	$arg = ParseArgs($args);
	if (!isset($arg['key'])) $arg['key'] = '';
	if (!isset($arg['label'])) $arg['label'] = $pagename;
	if (!isset($arg['linkpage'])) $arg['linkpage'] = $pagename;
	else $arg['linkpage'] = ZAPpageshortcuts($arg['linkpage']);
	$arg['secure'] = "set";
	$formkey = trim($arg['key']);
	$formpage = trim(str_replace(".", "", $pagename));
	session_start();
	$_SESSION['ZAP']["$formpage$formkey"]['key'] = $arg['key'];
	$_SESSION['ZAP']["$formpage$formkey"]['label'] = $arg['label'];
	$_SESSION['ZAP']["$formpage$formkey"]['linkpage'] = $arg['linkpage'];
	$_SESSION['ZAP']["$formpage$formkey"]['secure'] = "set";
	session_write_close();
	return "<a class='wikilink' href='{$ScriptUrl}?n={$arg['linkpage']}?action=zap&ZAPkey={$arg['key']}' rel='nofollow'>{$arg['label']}</a>";
	}

## Automatically converts GET values to page variables for use on page
## Cannot override existing page variables. (:zapget:) markup no longer needed.
Markup('zapget', '<{$var}', '/\(:zapget:\)/e', '');
foreach ($_GET as $g => $gg) if ((!isset($FmtPV["$$g"])) && ($gg != "")) $FmtPV["$$g"] = "'" . $gg . "'";

// Check these out and fix the ZAP toolbox to work with the new PmWiki defaut.
// You need a keep function still--but change to a markup expression that can be nested in the source markup.
// But be careful or users could unescape their own directives.  Not good.  How will you solve???
// Can't put (: :) inside the default input markup so you have a problem.  You need to get ZAPwiki done...
## These markups will soon be deprecated now that Pm is slowly bringing his own markup out for this...
Markup('textarea', 'inline', '/\\(:textarea (.*?):\\)/e', "Keep('<textarea '.PQA(PSS('$1')).' class=inputbox>')");
Markup('textareaend', 'inline', '/\(:textareaend:\\)/', '</textarea>');
Markup('zapkeep', '<if', '/\\(:keep (.*?):\\)/esi', "Keep(ZAPkeep(PSS('$1')))");
function ZAPkeep($x) {
	$htmlin = array(' : ', ' , ', ' ? ', ':)', '(:');
	$htmlout = array('&nbsp;:&nbsp;', '&nbsp;,&nbsp;', '&nbsp;?&nbsp;', ': )', '( :');
	$x = stripslashes(str_replace($htmlout, $htmlin, $x));
	return $x;
	}