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

/* fox.php, a form processing extension to PmWiki 2. Copyright Hans Bracker 2007.
   This program is free software; you can redistribute it 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.
   Other contributors:
   * Original script adddeleteline2.php by Nils Knappmeier (nk@knappi.org)
   * AccessCode code from commentbox.php by John Rankin
   * Code review (using htmlentities instead of urlencode) and some of the code
     for processing {name[]} markup in the template by Martin Fick (2006)
   * Original deletelink directive by Petko Yotov (2006)
   * Permission check on idea by Patrick R. Michaud
   * FoxFilter hook and other improvements by Feral
*/
$RecipeInfo['Fox']['Version'] = '2007-12-22';
// switch on echos for debugging
$FoxDebug = 0;

// auth permission level for adding and deleting posts. 
// 'edit': needs edit permission. 
// 'read': needs read permission (can post to edit protected pages) 
// 'ALWAYS':no permission needed, can also post to 'read' protected pages.
SDV($FoxAuth, 'edit');
SDV($FoxConfigPageFmt, "$SiteGroup.FoxConfig"); //default config page for page permission patterns
SDV($FoxTemplatePageFmt, "$SiteGroup.FoxTemplates#null"); //default page + section for templates (null is dummy) 
SDV($EnablePostDirectives, false); //set to true to allow posting of directives of form (: :)
SDV($EnableAccessCode, false);  //set to true to enable accesscode check (needs access code form fields as well)
SDV($EnableFoxDeleteMsg, false); //set to true to enable post delete confirmation 
SDV($FoxDeleteMsg, '$[Please confirm:   Do you want to delete this post?]');
SDV($FoxDeleteSummaryMsg, '$[Post&nbsp;deleted]'); //if you don't want a summary message, set it to ''
# Default posting permission patterns for allowed and prohibited pages to post to 
# (use of wildcards * and $ is possible, use of - at start will prohibit psting to that page)
# A prohibiting pattern overrules a allowing pattern of th esame match!
SDVA($FoxPagePermissions, array(
'$SiteGroup.DUMMY'       => 'all', //dummy entry so permissions will be checked! 
'$SiteGroup.*'      => 'none',
'$SiteAdminGroup.*'   => 'none',
'PmWiki.*'          => 'none',
'*.GroupFooter'     => 'none',
'*.GroupHeader'     => 'none',
'*.GroupAttributes' => 'none',

// examples of page permission patterns:
//'*.*' => 'all',     // all Fox actions allowed on all pages
//'Comments.*'                => 'add,delete', // adding and deleting posts in Comments group
//'Comments.{$Group}-{$Name}' => 'add,delete', // adding and deleting posts in comments group for pages with name Group-Name
//'*.{$Name}-Comment' => 'add',  // adding posts for pages with -Comment suffix
//'{$FullName}-Talk' => 'add',   // adding posts for pages in current group with -Talk suffix
// the following patterns for 'current page' and 'current group' could be exploited to post to edit protected pages
//'{$Group}.{$Name}' => 'add', // adding to current page
//'{$Group}.*' => 'add',       // adding to pages in current group
));

// Search path for fox.css files, if any.
SDV($FoxPubListFmt, array (
       "pub/fox/fox.css"        => "$PubDirUrl/fox/fox.css",
       "$FarmD/pub/fox/fox.css" => "$FarmPubDirUrl/fox/fox.css" ));

# load special styles for delete button and links and for message classes
foreach((array)$FoxPubListFmt as $k=>$v) {
  if (file_exists(FmtPageName($k,$pagename))) {
      $HTMLHeaderFmt['fox'][] =
      	"<link rel='stylesheet' type='text/css' href='$v' media='screen' />\n";
  		break;
  }
}

$pagename = ResolvePageName($pagename);

## Creates the HTML code for the (:fox name [placement] [parameters]:) directive
function FoxFormMarkup($pagename, $name, $place="", $opt ) {
    $PageUrl = PageVar($pagename, '$PageUrl');    
    $opt = ParseArgs($opt);
    if($opt['redirect']) { $opt['redir'] = $opt['redirect']; unset($opt['redirect']);}  
    if ($opt['formcheck']) $javacheck = FoxFormCheck($opt['formcheck']); 
    else $javacheck = '';
    $out = $javacheck;
    $out.= "<form action='{$PageUrl}' method='post' ".
          ($opt['formcheck'] ? "onsubmit='return checkform(this);'> \n" : "> \n").
            "<input type='hidden' name='foxpage' value='$pagename' /> \n".
            "<input type='hidden' name='action' value='foxpost' /> \n".
            "<input type='hidden' name='foxname' value='$name' /> \n".
          ($place ? "<input type='hidden' name='foxplace' value='$place' />\n" : ""); 
    foreach($opt as $key => $arg)
       if(!is_array($arg))
         $out.= "<input type='hidden' name='".$key."' value='".$arg."' /> \n";
    return Keep($out);
} //}}}
Markup('foxform','directives','/\(:fox\\s+([\\w]+)\\s?(#\\w+)*\\s*(.*?)\\s*:\)/e',"FoxFormMarkup(\$pagename, PSS('$1'), PSS('$2'), PSS('$3'))");

# Creates the HTML code for delete links {[foxdelline]}, {[foxdelrange]}
# and delete butons {[foxdelline button]} and {[foxdelrange button]}
function FoxDeleteMarkup($pagename, $key='dummy', $targetname="", $range, $type, $label) {
   global $ScriptUrl, $EnablePathInfo, $EnableFoxDeleteMsg, $FoxDeleteMsg, $FoxDeleteSummaryMsg;

	if ($targetname=="") $targetname = $pagename;
	$TargetPageUrl = PUE(($EnablePathInfo) 
			? "$ScriptUrl/$targetname"
         : "$ScriptUrl?n=$targetname");

    # javascript delete message dialogue
    if($EnableFoxDeleteMsg==true) 
         $onclick = "onclick='return confirm(\"{$FoxDeleteMsg}\")'";
    else $onclick = "";
    if($label=='') $label = '$[Delete]';
    if($type=='button') {
      # delete button
      return FmtPageName("<form class='foxdelbutton' action='{$TargetPageUrl}' method='post'>
            <input type='hidden' name='n' value='$targetname' />
            <input type='hidden' name='base' value='$pagename' />
            <input type='hidden' name='action' value='foxdelete' />
            <input type='hidden' name='csum' value='{$FoxDeleteSummaryMsg}' />
            <input type='hidden' name='linekey' value='$key' />
            <input type='submit' name='doit' value='{$label}' class='inputbutton' {$onclick}/>
            </form>", $targetname);    
    }
    else    
       # delete link which works with and without javascript:
       return FmtPageName("<a class='foxdellink' href='{$TargetPageUrl}?action=foxdelete&amp;linekey=$key&amp;base=$pagename&amp;csum={$FoxDeleteSummaryMsg}' rel='nofollow'
              {$onclick}>{$label}</a>",$targetname);
} //}}}
Markup('foxdelete','directives','/\{\[foxdel(line|range) ?(|button)\\s+(\\w+)\\s+(.*?)\\s+(.*?)\\s*\]}/e',
        "Keep(FoxDeleteMarkup(\$pagename, PSS('$3'), PSS('$4'), '$1', '$2', PSS('$5')))");

# Creates the HTML code for (:foxtemplate "templatestring":) as hidden input form field
# $template is the template string
function FoxTemplateMarkup($template) {
    $template = str_replace('"', '', $template);
    return '<input type="hidden" name="foxtemplate" value="'.htmlentities($template, ENT_QUOTES).'"/>';
} //}}}
Markup('foxtemplate','[=','/\(:foxtemplate\\s+(.*)\\s*:\)/e',"Keep(FoxTemplateMarkup(PSS('$1')))");

# create hidden HTML input tags for template and target pages, for foxpost and foxcopy markup
function FoxPostMarkup($pagename, $n, $arg) {
	if ($n=="post") { $from = 'template';    $to = 'target'; }	
	if ($n=="copy") { $from = 'foxcopyfrom'; $to = 'foxcopyto'; }
	$arg = ParseArgs($arg,'(?>([\\w-]+\\.?[\\w-]+\\#?\\#?[\\w-]+\\.?\\.?\\#?[\\w-]*)(?:=&gt;|[=:]))');
	foreach (array_keys($arg) as $k=>$v)
		if($k>0)	{ $i=$k-1; $out .= '<input type="hidden" name="'.$from.'[]" value="'.$v.'"/>'; }
	foreach (array_values($arg) as $k=>$v)
		if($k>0) { $i=$k-1; $out .= '<input type="hidden" name="'.$to.'[]" value="'.$v.'"/>'; }
	return $out;
}
Markup('foxpost','directives','/\(:fox(post|copy)\\s+(.*?):\)/ei',"Keep(FoxPostMarkup(\$pagename, PSS('$1'), PSS('$2')))");

# (:foxend name:)
Markup('foxendform','directives','/\(:foxend(\\s[\\w]+):\)/', "</form>");
# (:foxprepend:) and (:foxappend:) just vanish because they are only used later in  FoxAddEntry
Markup('foxaprepend','directives','/\(:fox(ap|pre)pend\\s*(.*?)\\s*:\)/','');
# (:foxallow:) for string check
Markup('foxallow','directives','/\(:foxallow\\s*(.*?)\\s*:\)/','');
# #foxbegin# and #foxend# invisible markers
Markup('foxentry','<fulltext','/#fox(begin|end)( \w+)?#/','');


# add FoxEditTemplate to $EditFunctions for FoxHandlePost
if($action=='edit' && $_REQUEST['foxtemptext']==1) 
   array_unshift($EditFunctions, 'FoxEditTemplate');

## provide page text for ?action=edit&foxtemptext=1
function FoxEditTemplate($pagename, &$page, &$new) {
   if (@$new['text'] > '') return;
   if (@$_REQUEST['foxtemptext'])  {
      if ($_SESSION["FoxTempPageText"] > '') $new['text'] = $_SESSION["FoxTempPageText"];
         return;
   }
} //}}}


## add action foxpost
$HandleActions['foxpost']='FoxHandlePost';
## Main post function. Insert text into page
function FoxHandlePost($pagename) {
   global $FoxDebug, $InputValues;
   //get arguments from POST and GET
	$fx = FoxRequestArgs($_POST);

	//store current values to redisplay, in case we abort.
	foreach ($fx as $k=>$v) 
		$InputValues[$k] = $v;

	//use foxpage as abort target.
	if(isset($fx['foxpage']))
		$pagename = $fx['foxpage'];

	//refuse n=pagename submissions
	if(isset($_POST['n']))
		FoxAbort($pagename,"\$[ERROR: 'n' is not a legal field name.]");

	//initialise
	$basename = $targetname = $pagename;
  
   //preprocess fields with FoxFilter, which calls external filter functions 
   if (isset($fx['foxfilter'])) 
      $fx = FoxFilter($pagename, $fx);
   
   //make foxgroup input suitable for group name, add foxgrouptitle to preserve original input
   if (isset($fx['foxgroup'])) {
   	$fx['foxgrouptitle'] = $fx['foxgroup'];
   	$fx['foxgroup'] = FoxWikiWord($fx['foxgroup']);
   }
   
   //do {$$var} field substitutions
   $fx = FoxFieldSubstitutions($pagename, $fx);  
   
   //check if current page should be target
   if (!isset($fx['newedit']) && !isset($fx['target']))
   	if (isset($fx['template']) || isset($fx['foxtemplate']))
      	$fx['target'] = $pagename; 
      
   //process special fields which could be arrays or lists for making arrays
   $special = array('target', 'template', 'ptvtarget', 'ptvfields', 'pagecheck');   
   foreach ((array)$special as $n) {
		if(isset($fx[$n])) {
				if(!is_array($fx[$n]))
					$fx[$n] = explode(",", $fx[$n]);
				foreach ($fx[$n] as $t=>$v)
         		$fx[$n.'['.$t.']'] = $v;
		}
   }
   //create  $targtemp array: target=>template
   if (isset($fx['target'])) {
		foreach ($fx['target'] as $i=>$t)
			$targtemp[$t] = $fx['template'][$i];
		//add ptvtargets to target->template array
		if (isset($fx['ptvtarget'])) {
			foreach ((array)$fx['ptvtarget'] as $ptg) {
				if (in_array($ptg, $fx['target'])) continue;
				$targtemp[$ptg] = 'none';
			}
		}
   }
   //do ptv_ field renaming
   $fx = FoxPTVPreProcess($pagename, $fx);  

   //check various security restrictions 
   FoxSecurityCheck($pagename, $fx);

	//foxcopy=1: set foxcopy array
	if (!isset($fx['foxcopyto']) && $fx['foxcopy']==1) {
		$fx['foxcopyto'] = $fx['target'];
		$fx['foxcopyfrom'] = $fx['template'];
	}

	//set $basename for redirect
	if (isset($fx['redir'])) {
		if($fx['redir']==1) {
			//for redir=1 use first target
			if (isset($fx['target'])) 
				$basename = FoxGroupName($pagename, $fx, $fx['target'][0]);
			else if (isset($fx['foxcopyto'])) 
				$basename = FoxGroupName($pagename, $fx, $fx['foxcopyto'][0]);
		}	
		else $basename = FoxGroupName($pagename, $fx, $fx['redir']);
	}
	if (isset($fx['urlfmt'])) $urlfmt = $fx['urlfmt'];
	else $urlfmt = '$PageUrl';


//DEBUG//
if($FoxDebug) {
?><pre><?php
echo "\$fx ";
print_r($fx);
?></pre><?php	
}

	//copy pages
	if (isset($fx['foxcopyto']) && isset($fx['foxcopyfrom']))
		FoxCopyPage($pagename, $fx);

   //process all target pages with corresponding templates
   if($targtemp && !$fx['foxcopy']==1) {
         foreach ($targtemp as $tgt => $tplname) { 
//DEBUG// echo " TARG-TEMP>".$tgt.">".$tplname;
         //set target pagename
         $targetname = FoxGroupName($pagename, $fx, $tgt);
         //load template
         $template = FoxLoadTemplate($pagename, $tplname, $fx);

         FoxAddEntry($pagename, $targetname, $template, $fx);
      }
   }

   //open page to edit using newedit value, or jump to existing page
   if(isset($fx['newedit'])) {
         if($fx['template']) $tplname = end($fx['template']);
         $template = FoxLoadTemplate($pagename, $tplname, $fx['template'], $fx);
         $targetname = FoxGroupName($pagename, $fx, $fx['newedit']);
         if(PageExists($targetname)) Redirect($targetname); //jump to existing page
         FoxNewEdit($pagename, $targetname, $template, $fx);   
   }
   //FoxMessage($pagename, "Operation was successful");
   Redirect($basename, $urlfmt);
} //}}}


## check 'foxgroup' and set targetname,
function FoxGroupName($pagename, $fx, $name) {
  global $FoxDebug; if($FoxDebug) echo " GROUPNAME>$name"; //DEBUG//
	if(isset($fx['foxgroup'])) $group = $fx['foxgroup'];
	else { $tname = MakePageName($pagename, $name);
		return $tname;
	}
	if (isset($fx['target']) || isset($fx['foxcopyto'])) {
		// handle 'escaped' target name: ignore foxgroup 
		if($name{0}=="\\") {
			$name = str_replace("\\","",$name);
			$tname = MakePageName($pagename, $name);
		}
		else {
			$name = FoxWikiWord($name);
			$tname = "$group.$name";
		}
	}
	if (isset($fx['newedit'])) {
		$name = FoxWikiWord($fx['newedit']);
		$tname = "$group.$name";	
	}
	return $tname;      
} //}}}


## add a new entry
function FoxAddEntry($pagename, $targetname, $template, $fx) {
	global $FoxDebug; if($FoxDebug) echo " ADDENTRY>"; //DEBUG//
	global $FoxAuth, $EnableBlocklist;
	// permission check
	Lock(2);
	$page = RetrieveAuthPage($targetname, $FoxAuth, true);
	FoxPagePermissionCheck($pagename, 'add', $targetname, $page, $fx);
	$newpage = $page;

	# create new entries from $fx 
	$addstring = FoxTemplateEngine($targetname, $template, $fx, '', 'FoxAddEntry');  //decode the template

	# placing $addstring into the page text
	$foxname = $fx['foxname'];
	// Handle the special names #top and #bottom
	if($fx['foxplace']=='#top') { $newpage['text'] = $addstring."\n".$page['text']; }
	elseif($fx['foxplace']=='#bottom') { $newpage['text'] = $page['text']."\n".$addstring; }
	// Handle locations specified by 'fox prepend' and 'fox append'
	else {  
		$textrows = explode("\n",$page['text']); 
		// Handle the special names #prepend or #append
		// Look for (:foxappend ... or (:foxprepend ... and append or prepend the $
		if(isset($fx['foxplace'])) $fp = $fx['foxplace'];
		else $fp = '#bottom'; //set default for placement
		if($fp=='#top'||$fp=='#bottom'||$fp=='#append'||$fp=='#prepend') $fplace = ""; 
		else $fplace = ' '.$fp;
		$formstart = '(:fox '.$foxname;
		$formend = '(:foxend '.$foxname;
		$app = '(:foxappend '.$foxname.$fplace.':)';
		$pre = '(:foxprepend '.$foxname.$fplace.':)';

		foreach ($textrows as $nr => $line) {
			if(strstr($line,$formend) AND $fp=='#prepend') { $done=1; $textrows[$nr] = "$line\n".$addstring; break; }
			if(strstr($line,$formstart) AND $fp=='#append') { $done=1; $textrows[$nr] = $addstring."\n$line"; break; }
			if(strstr($line,$pre)) { $done=1; $textrows[$nr] = "$line\n".$addstring; break; }
			if(strstr($line,$app)) { $done=1; $textrows[$nr] = $addstring."\n$line"; break; }
		}
		if($done==1) {
			$text = implode("\n",$textrows);
			$newpage['text'] = $text;
		}
		else { $newpage['text'] = $page['text']."\n".$addstring; } //default: append to bottom 
	}
	// update any PTVs
	if(isset($fx['ptvtarget']))
		$newpage['text'] = FoxPTVUpdate($pagename, $fx, $newpage['text'] );
	// save page
	$FoxEditFunctions = array('ReplaceOnSave', 'SaveAttributes', 'PostPage', 'PostRecentChanges');
	if($EnableBlocklist) array_unshift($FoxEditFunctions, 'CheckBlocklist');
	UpdatePage($targetname, $page, $newpage, $FoxEditFunctions);
	Lock(0);
	return '';
} //}}}


## newedit opens page in the edit form
function FoxNewEdit($pagename, $targetname, $template, $fx) {
  global $FoxDebug; if($FoxDebug) echo " NEWEDIT> "; //DEBUG//    
   $urlfmt = '$PageUrl?action=edit';
   if (@$fx['template'] OR @$fx['foxtemplate']) {
      // permission check
      FoxPagePermissionCheck($pagename, 'newedit', $targetname, '', $fx);
      // merging fields and template, put into Session var for use with ?action=edit&foxtemptext=1
      @session_start();
      $_SESSION["FoxTempPageText"] = FoxTemplateEngine($targetname, $template, $fx, '', 'FoxNewEdit');
      // add special template marker before redirecting to edit
      $urlfmt.= '&foxtemptext=1';
   }
   Redirect($targetname, $urlfmt); // open new page to edit
} //}}}


## copy template  to target without template var substitutions
function FoxCopyPage($pagename, $fx, $targetname='', $tplname='') {
   global $FoxDebug; if($FoxDebug) echo " COPY> "; //DEBUG// 
   global $FoxAuth;
	$fx['foxcopy-flag'] = 1;
	foreach($fx['foxcopyto'] as $k=>$tgt) {
		if(!isset($fx['foxcopyfrom'][$k])) 
				FoxAbort($pagename, " ERROR: No Template!");
		$template = FoxLoadTemplate($pagename, $fx['foxcopyfrom'][$k], $fx);
		$targetname = FoxGroupName($pagename, $fx, $tgt);
		// check permission, open target page
		$page = RetrieveAuthPage($targetname, $FoxAuth, true);
		FoxPagePermissionCheck($pagename, 'copy', $targetname, $page, $fx);		
		$text = $page['text'];
		//check for section to replace with template
		if (strstr($tgt,'#')) {
			$sect = RetrieveAuthSection($pagename, $tgt, '', $FoxAuth);
			$s = strlen($sect);
			$t = strpos($text, $sect);
			$above = substr($text,0,$t);
			$b = $t + $s;
			$below = substr($text,$b);
			$text = $above."\n".$template."\n".$below;
		}
		//replace all since no section
		else $text = $template;		
		//save target page
		$newpage = $page;
		$newpage['text'] = $text; 
		UpdatePage($targetname, $page, $newpage);
   }
   unset($fx['foxcopy-flag']);
   //FoxMessage($pagename, "Copy Successful");
   return '';   
} //}}}


## get template from template page, #section, or (:foxtemplate 'string':)
function FoxLoadTemplate($pagename, $tplname, $fx) {
 global $FoxDebug; if($FoxDebug) echo " TEMPLATE>$tplname"; //DEBUG//  
   global $FoxTemplatePageFmt;
  //first check if no template is wanted
	if(!isset($fx['foxcopy-flag']))  
		if($fx['template'][0]==="0" OR $fx['foxtemplate']==="" OR $tplname=='none') 
       	return ""; 
	//determine template page
	if($fx['template']) {
		$tplast = end($fx['template']); }
	$tplist = array($tplname, $tplast, $FoxTemplatePageFmt);
	foreach ((array)$tplist as $n) {
		$tplpage = MakePageName($pagename, $n);
		if(!$tplname) $tplname = $n;
		if (PageExists($tplpage)) break;
	}
  //load template page
   $page = ReadPage($tplpage, READPAGE_CURRENT);
   if ($page) 
   	 //TextSection will process any section passed, or the whole page
   	 $template = trim(TextSection($page['text'], $tplname),"\r\n");
   //try to load template from (:foxtemplate "templatestring":) 
   if (!$template && $fx['foxtemplate'])
      $template = html_entity_decode(stripmagic($fx['foxtemplate']));
   //if all failed abort
   if (!$template) { 
     FoxAbort($pagename, "NO TEMPLATE! Template page or string is missing! ");
   }
   return $template;          
} //}}}


# Processes $template provided and inserts the values of $fx array.
# $linekeyseed is an optional parameter that allows the caller to specify a seed for the linekey
# that is used to identify the range in the delete-action.
# The engine returns a string with the new entries. If no markup of the form {$$name[]}
# is present in the template, the array will contain exactly one entry, where the usual text substitutions have
# been performed.
# If a {$$name[]} markup is present, $fx['name'] has to be an array. For each element of $fx['name'],
# one entry is found in the array, where all the other fields are replicated, but {name[]} is substituted 
# by each element of $fx['name'].
# If more than one markups {$$name[]} are present, the outcome is undefined...
#
function FoxTemplateEngine($pagename, $template, $fx, $linekeyseed=NULL, $caller = '') {
  global $FoxDebug; if($FoxDebug) echo " ENGINE> "; //DEBUG//
    global $EnablePostDirectives, $EnableFoxBreakPage, $FoxPostsPerPage, $FoxBreakPageFmt, $Now;

    if($template=="") return '';

    // create the data to be added, from template and variables
    $string = $template;

    //replace \n by newlines
    $string = preg_replace('/\\\\n/',"\n",$string);  
    // replace {$$(timestamp)} with Unix timestamp now
    $string = preg_replace('/\\{\\$\\$\\(timestamp\\)\\}/e',"\$Now",$string);
    // replace {$$(date:fmt)}
    $string = preg_replace('/\\{\\$\\$\\(date[:\\s]+(.*?)\\)\\}/e',"date(PSS('$1'))",$string);  
    //replace {$$(strftime:fmt)}
    $string = preg_replace('/\\{\\$\\$\\(strftime[:\\s]+(.*?)\\)\\}/e',"strftime(PSS('$1'))",$string);  
    //prevent posting of directives (: ... :)
    if ($EnablePostDirectives==false) $fx = preg_replace('/\\(:/', "(&#x3a;", $fx ); 
    //replace {$$name} fields with values
    $string = preg_replace("/\\{\\$\\$([A-Za-z][-_:.\\w]*)\\}/e","stripmagic(\$fx[PSS('$1')])",$string); 

    //reiterate through array of {$$name[]}
    if(preg_match('/\\{\\$\\$([A-Za-z][-_:.\\w]*)\\[\\]\\}/', $string, $m)) {
        list($x, $name) = $m;
        $result = Array();
        foreach((array)$_POST[$name] as $val)  
            if ($val) $result[] = preg_replace("/\\{\\$\\$([A-Za-z][-_:.\\w\\[\\]]*)\\}/", $val, $string);
    } 
    else {
      //replace {$$$...} with {$$...} for posting of forms with replacement vars
      $string = preg_replace("/(\\{)(\\$)(\\$\\$[A-Za-z\\(][-_:.\\w\\[\\]\\)\\s]*\\})/","$1$3",$string);
      $result = Array($string);
    }
    
    //create a unique linekeyseed, if necessary
    if ($linekeyseed==NULL) $linekeyseed=time().'a'.rand(0,100000);
    
    foreach ($result as $index => $entry) {
        $linekey = $linekeyseed.'b'.$index;
        //adding linekey + pagename to any foxdelete markup for unique id
        $entry = str_replace('{[foxdelline button',"{[foxdelline button $linekey {\$FullName} ",$entry); // Add linekey to delete statements
        $entry = str_replace('{[foxdelline',"{[foxdelline $linekey {\$FullName} ",$entry); // for link-delete
        $entry = str_replace('{[foxdelrange button',"{[foxdelrange button $linekey {\$FullName} ",$entry); // Add linekey to delete statements
        $entry = str_replace('{[foxdelrange',"{[foxdelrange $linekey {\$FullName} ", $entry); // for link-delete
        $entry = str_replace('#foxbegin#',"#foxbegin $linekey#",$entry); //Add line-key to delete marker
        $entry = str_replace('#foxend#',"#foxend $linekey#",$entry); // Add line-key to delete marker
        $result[$index] = $entry;
    }
    return implode("\n",$result);
} //}}}


## get arguments from POST or GET
function FoxRequestArgs( $fx = NULL) {
	if (is_null($fx)) $fx = array_merge($_GET, $_POST);
	foreach ($fx as $name=>$value) {
   	if(is_array($value))
   		foreach($value as $k=>$val)
   			$fx[$name][$k] = stripmagic($val);
		else $fx[$name] = stripmagic($value);
	}
	return $fx;
} //}}}


## call external filter functions
SDV($FoxFilterFunctions, array());
function FoxFilter($pagename, $fx) {
  global $FoxDebug; if($FoxDebug) echo " FILTER> "; //DEBUG// 
   global $FoxFilterFunctions;
   //get filter keynames
   $fx['foxfilter'] = preg_split("/[\s,|]+/", $fx['foxfilter'], -1, PREG_SPLIT_NO_EMPTY);
   foreach($fx['foxfilter'] as $f) {
      $ffn = $FoxFilterFunctions[$f];
      if (function_exists($ffn) ) { 
         // use specific filter
         if(is_callable($ffn, false, $callable_name)) {
            $fx = $callable_name($pagename, $fx);
            if(!$fx) Redirect($pagename); // Filter is telling us to abort;
         }  
      }  
   }  
   return $fx;
} //}}}


## substitute any {$$var} in fields with values
function FoxFieldSubstitutions($pagename, $fx) {
  global $FoxDebug; if($FoxDebug) echo " SUBST>"; //DEBUG//
   //check all fields for substitutions
   foreach($fx as $name => $value) {
		# Skip known ones:
      if (preg_match('/^(n|foxpage|action|foxname|foxplace|foxfields|template|foxtemplate)$/i', $name)) continue;
		# replace {$$name} fields with values
      $fx[$name] = $value = preg_replace("/\\{\\$\\$([A-Za-z][-_:.\\w\\[\\]]*)\\}/e","stripmagic(\$fx[PSS('$1')])",$value);
		# evaluate markup expressions
      $fx[$name] = $value = preg_replace("/^\\{\\$\\$(\\(\\w+\\b.*?\\))\\}$/e", "MarkupExpression(\$pagename, PSS('$1'))", $value); 
   }
   return $fx;
} //}}}


## create NAME fields from ptv_NAME fields and add NAME to ptvfields array
function FoxPTVPreProcess($pagename, $fx) {
	global $FoxDebug; if($FoxDebug) echo " PTVPRE>"; //DEBUG//
	foreach($fx as $n => $v) {
		if(substr($n,0,4)=="ptv_") { 
			$n = substr($n,4);
			$fx[':ptvfields'][] = $n;
			$fx[$n] = $v;
		}
	}
	return $fx;
} //}}}


## update page text variables
function FoxPTVUpdate($pagename, $fx, $text ) {
  global $FoxDebug; if($FoxDebug) echo " PTVUPDATE>"; //DEBUG//
	global $PageTextVarPatterns;
      //PTVs to check
      if(is_array($fx['ptvfields']))
      	foreach($fx['ptvfields'] as $n) $update[$n] = $fx[$n];
      else $update = $fx; //use all fields to look for PTVs
		//look through PTV patterns and replace matches
		foreach($PageTextVarPatterns as $pat) {
			if (!preg_match_all($pat, $text, $match, PREG_SET_ORDER)) continue;
			foreach($match as $m) {
				$var = $m[2]; if (!@$update[$var]) continue;
					$val = $update[$var];
				if (!preg_match('/s[eimu]*$/', $pat))
					$val = str_replace("\n", ' ', $val); 
				$text = str_replace($m[0], $m[1].$val.$m[4], $text);
			}
		}
	return $text;
} //}}}		


## Delete one line or range of lines
$HandleActions['foxdelete']='FoxHandleDelete';
function FoxHandleDelete($pagename) {
    global $FoxAuth;
    $args = FoxRequestArgs();
    # delete permission set by $FoxAuth
    $page = RetrieveAuthPage($pagename, $FoxAuth, true);
    FoxPagePermissionCheck($pagename, 'delete', $pagename, $page, $fx);
    if (!$page) FoxAbort($pagename, "ERROR: Cannot read $page! ");
    $newpage = $page;

    if(isset($args['linekey'])) $key = $args['linekey'];  # Retrieve the line-key

    # trim text and add newline so the following regexes also work for the last line
    $text = rtrim($page['text'])."\n";
    $old = $text;
    # Remove the line containing the delete statement with the provided line-key (if it exists)
    $text = preg_replace('/^.*\\{\\[foxdelline(| button)? '.$key.' .*\\n/m',"",$text);
    # Remove the range containing the delrange statement with the provided line-key (if it exists)
    $text = preg_replace('/#foxbegin '.$key.'#.*?\\{\\[foxdelrange(| ?button) '.$key.' .*?#foxend '.$key.'#.*?\n/s',"",$text);
    if($old==$text) 
         FoxAbort($pagename, "ERROR: Delete action was unsuccessful! ");
    # Remove the added newline character (or any whitespace from the end)
    $text = rtrim($text);
	 
	 # save page
    $newpage['text'] = $text;
    UpdatePage($pagename, $page, $newpage);    
    # set up page redirection, cater for deletelink ($_GET)
    if(@$args['base']) $pagename = $args['base'];
    Redirect($pagename);
} //}}}


## check page posting permissions
function FoxPagePermissionCheck($pagename, $type, $targetname, $page, $fx) {
   global $FoxDebug; if($FoxDebug) echo " PERMISSION>$targetname";
   global $FoxConfigPageFmt, $FoxPagePermissions, $FoxAuth;
   // get name patterns from FoxConfig page 
   $Name = PageVar($pagename, '$Name');
   $Group = PageVar($pagename, '$Group');
   $pn = FmtPageName($FoxConfigPageFmt, $pagename);
   $fpage = ReadPage($pn, READPAGE_CURRENT);   
   if($fpage && preg_match_all("/^\\s*([\\*\\w][^\\s:]*):\\s*(.*)/m",$fpage['text'], $matches, PREG_SET_ORDER)) {
         foreach($matches as $m) $FoxPagePermissions[$m[1]] = $m[2];
   }
   // name check for type against $FoxPagePermissions
   $pnames = array();
   foreach($FoxPagePermissions as $n => $t) {
        if(strstr($t,'-'.$type)||strstr($t,'none')) { $pnames[$n]='-'.$n; continue; }
        if(strstr($t,$type)||strstr($t,'all')) $pnames[$n]=$n; 
   }   
   $pnames = FmtPageName(implode(',',$pnames),$pagename);
   if($pnames=='') $pnames = '-';
   $namecheck = (boolean)MatchPageNames($targetname,$pnames);
   // string check against string patterns
   $strcheck = 0;
   if(PageExists($targetname)) { 
      $strcheck = (boolean)( preg_match("/\\(:fox(prepend|append|allow)/", $page['text']) OR
         preg_match("/\\(:fox ".$fx['foxname']." (#prepend|#append|#top|#bottom)/", $page['text']) );
   }
   if($namecheck==0 && $strcheck==0) FoxAbort($pagename, "PERMISSION DENIED to $type on $targetname!");
   else return '';
} //}}}


## check access code, captcha, new page exists, and required fields
function FoxSecurityCheck($pagename, $fx) {
   global $FoxDebug; if($FoxDebug) echo " SECURITY>";
   global $FoxAuth, $FoxNameFmt, $EnableAccessCode, $EnablePostCaptchaRequired;
   // if enabled check for access code match 
   if($EnableAccessCode AND (!(isset($fx['access'])&&($fx['access']==$fx['accesscode'])))) { 
      FoxAbort($pagename, "ERROR: Missing or wrong Access Code!");
   }
   // if enabled check for Captcha code (captcha.php is required)
   if($EnablePostCaptchaRequired AND !IsCaptcha()) {
      FoxAbort($pagename, "ERROR: Missing or wrong Captcha Code!");
   }
   //  check pagecheck: if pagecheck page names exists already
   if(isset($fx['pagecheck'])) {
      // pagecheck=1 checks all target pages
      if($fx['pagecheck'][0]==1) 
         $fx['pagecheck'] = $fx['target'];
      foreach ($fx['pagecheck'] as $pt) {
         $page = MakePageName($pagename, $pt);
         if($pagename==$page) FoxAbort($pagename, "ERROR: You are not allowed to post to this page!");
         if(PageExists($page) AND in_array($pt, $fx['target'])) 
             FoxAbort($pagename, "Sorry, page \"$pt\" exists already. Please choose another page name.");
      }
   }
   // check for 'post' from submit button and refuse empty textarea named 'text'
   // ToDo: expand this to check  array of mandatory fields ???
   if (!isset($fx['post']) OR (isset($fx['text']) AND $fx['text']=="")) {
       FoxAbort($pagename, "ERROR: No text or missing post!");
   }
   return $fx;
} //}}}


## make a WikiWord out of a string
function FoxWikiWord($str) {
  global $FoxDebug; if($FoxDebug) echo " WIKIWORD> "; //DEBUG//
	global $MakePageNamePatterns;
	$str = preg_replace('/[#?].*$/', '', $str);
   $nm = preg_replace(array_keys($MakePageNamePatterns),
               array_values($MakePageNamePatterns), $str);
	return $nm;
} //}}}

# display message (not functioning as yet)
function FoxMessage($pagename, $msg) {
   global $MessagesFmt;
   $MessagesFmt[] = "<div class='wikimessage'>$[$msg]</div>";
   HandleBrowse($pagename);
} //}}}


## abort by displaying error message and retuning to page
function FoxAbort($pagename, $msg) {
   global $MessagesFmt;
   $MessagesFmt[] = "<div class='wikimessage'>$[$msg]</div>";
   HandleBrowse($pagename);
   exit;
} //}}}


## build javascript for simple validation that required fields have values 
function FoxFormCheck($formcheck) {
   $reqfields = preg_split("/[\s,|]+/", $formcheck, -1, PREG_SPLIT_NO_EMPTY);
   $out = "   
   <script type='text/javascript' language='JavaScript1.2'><!--
     function checkform ( form ) {
      ";
   foreach($reqfields as $required) {
      $out .= 
      "if (form.$required && form.$required.value == \"\") {
           window.alert( 'Entry in field \'$required\' is required!' );
           form.$required.focus();
           return false ;
         }
      ";
   }
   $out .= 
   "return true ;
     }
   --></script>
   ";
   return $out;    
} //}}}


## provide {$AccessCode} page variable:
$FmtPV['$AccessCode'] = rand(100, 999);


## add page variable {$FoxPostCount}, counts message items per page
$FmtPV['$FoxPostCount'] = 'FoxStringCount($pn,"#foxbegin")';
function FoxStringCount($pagename,$find) {
   $page = ReadPage($pagename, READPAGE_CURRENT);
   $n = substr_count($page['text'], $find);
//   if ($n==0) return '';  #suppressing 0
   return $n;
}
///EOF