<?php if (!defined('PmWiki')) exit(); # vim: set ts=4 sw=4 et: ## ## File: toolbox.php ## Version: 2009-04-20 ## SVN ID: $Id: toolbox.php 356 2009-04-20 20:56:41Z pbowers $ ## Status: alpha ## Author: Peter Bowers ## Create Date: May 19, 2008 ## Copyright: 2008, Peter Bowers ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License, Version 2, as ## published by the Free Software Foundation. ## http://www.gnu.org/copyleft/gpl.html ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## toolbox.php contains an eclectic group of functions which are designed to ## be of generic use in different recipes. No end-user functionality is ## provided - this is purely for recipe developers to make their lives simpler. # # Err($pagename, $opt, $msg) # This function will output an error. Currently all it does is to echo the # error, but eventually it will allow for logging errors, putting them in # (:errors:), etc. # # dbg($printlevel, $message) # This function provides trace statement capability. A global $DebugLevel # controls which statements will print. Any statement with a $printlevel # greater than or equal to $DebugLevel will print. # # od($text) # For use in debugging. HTML can sometimes make parts of your text disappear # or etc. If you display it through od() then any non-alpha, non-numeric # characters will be replaced with either a name for that character or a # CHR(###) representation. # # RunMarkupRules($pagename, $RuleList, $Text, $opt) # Run the markup rules held in $RuleList over the source held in $Text and # return the results. This enables running a subset of markup rules. # # writeptv($pagename, $pn, $var, $val, $ptvfmt, $opt) # Write a PTV to a given page. 1st 4 arguments are mandatory, remainder are # optional. # $pagename - a reference page (can be the same as $pn) # $pn - the name of the page to which the PTV will be written # $var - the name of the variable (no $ or {} characters) # $val - the value to be written to the variable # $ptvfmt - defaults "hidden", other options: "text", "deflist", "section" # $opt - array containing options such as # 'fmtpn'=>1 - whether to run FmtPagename() over $pn # 'fmtval'=>1 - whether to run FmtPagename() over $val # 'simplewrite'=>1 - use WritePage() instead of UpdatePage() # # logit($pagename, $pn, $text, $opt) # Append text to the end of a page. # $pagename - a reference page (can be the same as $pn) # $pn - the page to which to append # $text - the text to write # $opt - an array containing modifying semantics # [NO_FMTPAGENAME] - suppress running FmtPageName() over $pn # [NO_FMTTEXT] - suppress running FmtPagename() over $text $RecipeInfo['Toolbox']['Version'] = '2009-04-20'; define(toolbox, true); ## While these variables cannot be counted on absolutely, if you load ## toolbox.php fairly early in your config.php then you can do a comparison ## of microtime(true) against $Timeout to determine how close you are to ## timing out on the max_execution_time. It's wise to give yourself a few ## seconds of leeway by subtracting from $Timeout or adding to microtime(). ## EXAMPLE: if (microtime(true)+2 > $Timeout) ... ## -don't forget to globalize $Timeout... $StartTime = microtime(true); $Timeout = $StartTime + ini_get('max_execution_time'); SDV($DebugLevel, 5); // Default is no debugging. Set to lower # for more detail $pagename = ResolvePageName($pagename); # Err() # Right now a simple echo. Later on it might use $opt or something to get # fancier. Possibly logging errors to a file or optionally putting them in # (:messages:) instead or a (new) (:errmessage:). That sort of thing. That's # why it's nice to put all errors through a single function. function Err($pagename, $opt, $msg) { echo "$msg<br>\n"; } # SafeFmtPagename() $MarkupExpr["dbg"] = 'tbDbg($pagename, @$argp, @$args)'; function tbDbg($pagename, $opt, $args) { for ($i = 0; $i < 3; $i++) if ($args[$i] == 'array') $args[$i] = array('a'=>1,'b'=>'abc','def'); dbg(5,"Dbg: " . $args[0], $args[1], $args[2]); return (''); } # dbg() # Printlevel indicates what level of importance this line is. # 0=never print # 1=very detailed debugging # 2=a little less detailed # 3=fairly normal debugging # 4=high-level, print it without thinking about it # (you can go higher, but the indentation doesn't work) # if $printlevel is lower than $DebugLevel (global) then nothing will be # printed. Thus you get more detailed debug by setting the $DebugLevel to # a lower number. Typically $DebugLevel=1 is maximum detail and $DebugLevel=5 # has all debug statements turned off. function dbg($printlevel, $text, $txt2 = '', $txt3 = '', $txt4 = '', $txt5='') { global $MessagesFmt, $DebugLevel; if ($printlevel < $DebugLevel) return; # If your cookbook is messed up enough that you can't get a page to display # and so you can't see the (:messages:) output then uncomment the line # below and the lines will be echoed instead (arrays are not handled). #echo "<pre>" . (is_array($text)?print_r($text,true):str_repeat(" ", 5-$printlevel).$text) . " (" . (microtime(true) - $StartTime) . ")</pre><br>\n"; foreach (array($text, $txt2, $txt3, $txt4, $txt5) as $txt) { if ($txt == '') break; elseif (is_array($txt)) { $MessagesFmt[] = "<pre>" . print_r($txt,true) . "</pre>"; } else { $suffix = "</li></ul>"; $prefix = ''; for ($i = 4-$printlevel; $i > 0; $i--) { $prefix .= "<dl><dd>"; $suffix .= "</dd></dl>"; } $prefix .= "<ul><li>"; $MessagesFmt[] = $prefix . $txt . $suffix . "\n"; } } } # RunMarkupRules() # $RuleList - an array holding indices into the $MarkupTable array - these rules # (in this order) will be processed # $Text - this is the text upon which the rules will be run # RETURN: The modified text function RunMarkupRules($pagename, $RuleList, $Text, $opt=array()) { global $MarkupTable; $func = 'RunMarkupRules()'; $d=0; dbg($d*3,"$func: Entering with text=$Text"); dbg($d*3,"$func: RuleList=".print_r($RuleList, true)); foreach ((array)$RuleList as $rule) { dbg($d*1,"rule=$rule, Text=$Text"); if ($MarkupTable[$rule]) { $p = $MarkupTable[$rule]['pat']; $r = $MarkupTable[$rule]['rep']; #$Text = preg_replace($p,$r,$Text); if ($p{0} == '/') $Text=preg_replace($p,$r,$Text); elseif (strstr($Text,$p)!==false) $Text=eval($r); } else { Err($pagename, $opt, "ERROR: RunMarkupRules: Unknown markup rule $rule"); } } dbg($d*3,"$func: Returning text=$Text"); return($Text); } # od() # This function name is taken from the shell tool "od=octal dump" and as such # is somewhat mis-named, but it does the same sort of thing and is a nice # short name. It takes text and makes sure that text will be represented in # your browser without being interfered with by any rules. This means that most # non-alpha characters will be replaced by a name for that character. # # For instance, displaying "My name <myaddr@foo.com>" in HTML will always lose # the text between angle brackets. od() will render that as # "My_name_ OPEN-ANGLE myaddr AT foo DOT com CLOSE-ANGLE" # The names of characters may be eclectic, but it can be helpful for debugging. function od($text) { $repl = array(':' => 'COLON', ';' => 'SEMI-COLON', '/'=>'SLASH', '(' => 'OPEN-PAREN', ' '=>' _ ', '@' => 'AT', '\\' => 'BACKSLASH', CHR(10)=>'LF', CHR(13)=>'CR', ')'=>'CLOSE-PAREN', '.' => 'DOT', '{'=>'OPEN-CURLY', '}'=>'CLOSE-CURLY', '='=>'EQUALS', '|' => 'VERT', '#'=>'POUND', ','=>'COMMA', '$'=>'DOLLAR', '-'=>'DASH', '+'=>'PLUS', '"' => 'DOUBLE-QUOTE', "'" => 'SINGLE-QUOTE', '!' => 'BANG', '?' => 'QUESTION', '<' => 'OPEN-ANGLE', '>' => 'CLOSE-ANGLE', '*' => 'ASTERISK', '&' => 'AMPERSAND'); $rtn = ''; for ($i = 0; $i < strlen($text); $i++) if (in_array($text[$i], array_keys($repl))) { #echo "$foo[$i](" . ord($foo[$i]) . ") => " . $repl[$foo[$i]] . "<br>\n"; $rtn .= ' ' . $repl[$text[$i]] . ' '; } elseif (ord($text[$i]) < 65 && (ord($text[$i])<48 || ord($text[$i])>57)) $rtn .= 'CHR(' . ord($text[$i]). ')'; else $rtn .= $text[$i]; return($rtn); } # writeptv() # This function writes a PageTextVar to $pn. # If the PTV already exists in page=$pn then it is updated without changing the # type of PTV. If it does not exist then it is written to (the bottom of) $pn # in the format specified by $fmtpn. # # Note that PmWiki authorizations are respected in this function and history is # maintained and etc. (RetrieveAuthPage is used instead of ReadPage and # UpdatePage is used instead of WritePage()). If you need to bypass # authorizations you'll need to look elsewhere. # # $var and $val can be either singletons or arrays. if arrays then they must # be identical in size. function writeptv($pagename, $pn, $var, $val, $ptvfmt='hidden', $fmtpn=true, $fmtval=false) { global $WikiShWriting; if ($WikiShWriting) return(false); $func = 'writeptv()'; $d=1; dbg($d*4,"$func: Entering: $pn, $var, $val, $ptvfmt"); if (!is_array($var)) $var = (array)$var; if (!is_array($val)) $val = (array)$val; if (@$opt['fmtpn']) $pn = FmtPageName($pn, $pagename); $pn = MakePageName($pagename, $pn); $page = RetrieveAuthPage($pn, 'edit', false); if (!$page) return(false); $opage = $page; dbg($d*1,"$func: Before modified: >>$page[text]<<"); for (reset($var),reset($val);list($x,$vr)=each($var),list($x,$vl)=each($val);) { if (@$opt['fmtval']) $vl = FmtPageName($vl, $pagename); $page['text'] = ptv2text($page['text'], $vr, $vl, $ptvfmt); } dbg($d*1,"$func: After appended: >>$page[text]<<"); $WikiShWriting = true; if (@$opt['simplewrite']) $rtn = WritePage($pn, $page); else $rtn = UpdatePage($pn, $opage, $page); $WikiShWriting = false; return($rtn); } # ptv2text() # Do the text manipulation to place a PTV definition within a text string # This is a support function for writeptv(), but it is very helpful in and of # itself if you want to handle the read/write of the page yourself. # $var and $val must be elements, not arrays. function ptv2text($text, $var, $val, $ptvfmt='hidden') { global $PageTextVarPatterns; $func = 'ptv2text()'; $d=1; dbg($d*3, "$func: entering. var=$var, val=$val, fmt=$ptvfmt"); switch ($ptvfmt) { case 'text': $newdef = "$var: $val"; break; case 'deflist': $newdef = ":$var: $val"; break; case 'section': $newdef = $val; if (substr($val, -1) != "\n") $newdef .= "\n"; break; case 'hidden': case '': default: $newdef = "(:$var:$val:)"; break; } if ($ptvfmt == 'section') { list($a, $b, $c) = tbTextSection($text, "#$var"); dbg($d*5, "$func: a=$a, b=$b, c=$c"); if ($c) $text = $a.$newdef.$c; else $text = $a.$newdef; dbg($d*5, "$func: after section replace text=$text"); } else { $set = false; foreach ((array)$PageTextVarPatterns as $ptvpat) { if (preg_match_all($ptvpat, $text, $match, PREG_SET_ORDER)) { foreach ($match as $m) { dbg($d*1,"$func: 1=$m[1], 2=$m[2], 3=$m[3], 4=$m[4]"); if ($m[2] == $var) { dbg($d*1,"$func: found val=$m[3]"); $text = str_replace($m[0], $newdef, $text); $set = true; break 2; } } } } if (!$set) $text .= "\n$newdef"; } dbg($d*1,"$func: After replaced: ", array($text)); return($text); } # logit() # Simply append some text to the end of a page. # PmWiki authorizations are bypassed in this function to facilitate writing to # administrator-only pages function logit($pagename, $pn, $text, $opt=array()) { if (!@$opt['NO_FMTPAGENAME']) $pn = FmtPageName($pn, $pagename); $pn = MakePageName($pagename, $pn); $page = ReadPage($pn, READPAGE_CURRENT); if (!@$opt['NO_FMTTEXT']) $text = FmtPageName($text, $pagename); if (@$opt['pre']) $page['text'] = $text . "\n" . $page['text']; else $page['text'] .= "\n" . $text; WritePage($pn, $page); } # tbUpdateAuthPage() # # This function is a substitute for UpdatePage(). If called identically it # will provide a check against pmwiki auth level 'edit' before updating the # page. But you can also call it with sections to write to just a section, # pass it just text as the $newpage, etc. Additionally you can check a # SecLayer authorization level by passing the $slAuth and $slStore. # # This function enforces valid SecLayer authorization AND valid PmWiki # authorization before calling UpdatePage(). # # ARGUMENTS # $pagename - the page to be updated # - optional #section specification # - optional >/pattern/mod specification # </pattern/mod specification # =/pattern/mod specification # >> specification (append to page) # << specification (prepend to page) # $opage - the array holding old (current) page info (or false to read it) # $npage - the array holding new page info (or just the new text) # $pmauth - the pmwiki authorization level required # $slAuth - the SecLayer authorization level required # $slStore - the SecLayer authorization store # # Note that $opage and $npage are overloaded. They can use the standard array # definitions or $opage can be false/null (the page will be read again to get # the old values) and $npage just a textual value (the array will be used from # $opage). (note the paragraph below for a restriction on $npage semantics) # # If you wish to use the section specification or the >/</= location in order # to put your text in a specific place on the page then $npage MUST be ONLY # text, not an array... # function tbUpdateAuthPage($pagename, $opage, $npage, $pmauth='edit', $opt=array(), $slAuth=null, $slStore=null) { global $tbError, $PageNameChars; $tbError = ''; SDV($PageNameChars, '-[:alnum:]'); # Isolate pagename, operators, etc. if (!preg_match("/(?P<pagename>[$PageNameChars]+)(?:(?P<section>#[-\\w#]+$)|(?:(?P<op>[<>=])(?P<fullpat>(?P<delim>[\/|#!])(?P<pat>[^(?P=delim)]+)(?P=delim)(?P<mod>[msi]*))))?/", $pagename, $m)) { $tbError = 'Pagename ($pagesource) does not match expected pattern'; return(false); } $pn = MakePageName($pagename, $m['pagename']); if ($slAuth && $slStore && is_array($slStore)) if (!slAuthorized($pn, $slStore, $slAuth)) { $tbError = "Page=$pn does not have SecLayer auth=$slAuth"; return(false); } if ($opage) { if ($pmauth && !CondAuth($pagename, $pmauth)) { $tbError = "Page=$pn does not allow auth=$pmauth"; return(false); } } elseif ($pmauth) { if (!($opage = RetrieveAuthPage($pn, $pmauth))) { $tbError = "Page=$pn cannot be read with auth=$pmauth"; return(false); } } else { if (!($opage = ReadPage($pn, READPAGE_CURRENT))) { $tbError = "Page=$pn cannot be read even without requiring auth"; return(false); } } if (!is_array($npage)) { $x = $npage; $npage = $opage; $otext = $opage['text']; # Check for section write if ($m['section']) { list($a,$b,$c) = tbTextSection($otext, $m['section']); $npage = $a.$npage.$c; } # Check for op/pat write elseif ($m['op']) { if ($m['fullpat'] == '<' || ($m['op'] == '<' && !$m['fullpat'])) { # page<< or page< - prepend text $otext = $npage . $otext; } elseif ($m['fullpat'] == '>' || ($m['op'] == '>' && !$m['fullpat'])) { # page>> or page> - append text $otext .= $npage; } else { } } $npage['text'] = $x; } return(UpdatePage($pagename, $opage, $npage)); } ## tbTextSection() ## ## Moved from WikiSh wshTextSection() which was in turn copied from ## pmwiki.php TextSection() ## DIFFERENCES from TextSection(): ## Returns 3-element array: (1) pre-section text with starting anchor, ## (2) section text, (3) ending anchor and post-section text. ## If section definition is not valid then either all in (1) (if no ## start anchor) or all in (1) and (2) (if no end anchor) ## ## TextSection extracts a section of text delimited by page anchors. ## The $sections parameter can have the form ## #abc - [[#abc]] to next anchor ## #abc#def - [[#abc]] up to [[#def]] ## #abc#, #abc.. - [[#abc]] to end of text ## ##abc, ..#abc - beginning of text to [[#abc]] ## Returns the text unchanged if no sections are requested, ## or false if a requested beginning anchor isn't in the text. function tbTextSection($text, $sections, $args = NULL) { global $WikiShVars; $func = 'tbTextSection()'; $args = (array)$args; $npat = '[[:alpha:]][-\\w*]*'; # Section was invalid in the call to this function - no way to append a # section because we can't identify the section we're looking for. if (!preg_match("/#($npat)?(\\.\\.)?(#($npat)?)?/", $sections, $match)) { $tbError = "ERROR: $func: section definition \"$sections\" invalid"; return array($text, '', ''); } @list($x, $aa, $dots, $b, $bb) = $match; dbg(1,"$func: text=$text, aa=$aa, dots=$dots, b=$b, bb=$bb"); if (!$dots && !$b) $bb = $npat; dbg(1,"$func: aa=$aa, dots=$dots, b=$b, bb=$bb"); if ($aa) { $pos = strpos($text, "[[#$aa]]"); if ($pos === false) { $pre = $text . "[[#$aa]]"; dbg(1,"$func: returning prematurely"); return array($pre, '', ''); } if (@$args['anchors']) while ($pos > 0 && $text[$pos-1] != "\n") $pos--; else $pos += strlen("[[#$aa]]"); $pre = substr($text, 0, $pos); $text = substr($text, $pos); } dbg(1,"$func: pre=$pre, text=$text"); if ($bb) { if (preg_match("/^(.*?)(\\[\\[#$bb\\]\\].*)$/s", $text, $m)) { $text = $m[1]; $post = $m[2]; } elseif ($b) $post = "[[$b]]"; else $post = ''; } dbg(1,"$func: pre=$pre, text=$text, post=$post"); return array($pre, $text, $post); } # This markup and function are only to test tbRetrieveAuthPage() # {(tbretrieve page#section pmauth slauth)} $MarkupExpr["tbretrieve"] = 'tbRetrieveTest($pagename, @$argp, @$args)'; function tbRetrieveTest($pagename, $argp, $args) { global $tbError, $wshAuthPage; if ($page=tbRetrieveAuthPage($pagename, $args[0], $args[1], false, 0, $wshAuthPage, $args[2], ($args[3]?array('if', 'include'):null))) return "success: old=".str_replace('(', '[', $page['text']).", new=".str_replace('(', '[', $page['ntext']); else return "failure: $tbError"; } # tbRetrieveAuthPage() # This function acts SOMEWHAT as RetrieveAuthPage() (or RetrieveAuthSection()) # with the following differences: # (1) The full $page array is returned with an additional $page['ntext'] which # contains the manipulated text (sections and rules, if requested) (this # element of the array - $page['ntext'] - should be unset() before posting # the page using this array) ($page['text'] will hold the original page # text) # (2) SecLayer authorizations are enforced if $slStore and $slAuth are provided # (3) Sections are read as expected (as specified in $pagesource) # (4) If $RuleList is provided as an array, the list of markup rules contained # therein will be run. (rules such as 'if', 'comment', and 'include' are # often useful) # (5) $pagesource is expected to be in the form page, page#section or alternates # (6) Note that the default for $prompt is false rather than true, in keeping # with the fact that this function will more likely be used for reading # pages that are not being edited but are rather being manipulated # # If you call it exactly as RetrieveAuthPage() it still gives you the capability # of correctly parsing sections without doing it in your recipe. # # Do note the need for doing an unset($page['ntext']) prior to posting the page # or you will end up with undesired (and useless) page attribute named ntext. # function tbRetrieveAuthPage($pagename, $pagesource, $authlev, $prompt=false, $since=0, $slStore=null, $slAuth='', $RuleList=null) { global $tbError, $PageNameChars; $func = 'tbRetrieveAuthPage()'; $d=1; dbg($d*4,"$func: pagesource=$pagesource, authlev=$authlev, slauth=$slauth"); $tbError = ''; SDV($PageNameChars, '-[:alnum:]'); # Isolate pagename, operators, etc. if (!preg_match("/(?P<pagename>[${PageNameChars}.]+)(?:(?P<section>#[-\\w#]+$)|(?P<op>[<>=]))?/", $pagesource, $m)) { $tbError = 'Pagename ($pagesource) does not match expected pattern'; return(false); } $pn = MakePageName($pagename, $m['pagename']); dbg($d*1, "$func: pn=$pn"); # If $slAuth and $slStore are provided the check SecLayer authorization dbg($d*1, "$func: SecLayer against $slAuth"); if ($slAuth && $slStore && is_array($slStore)) { if (!slAuthorized($pn, $slStore, $slAuth)) { $tbError = "Page=$pn does not have SecLayer auth=$slAuth"; return(false); } } # Do the actual read of the page, enforcing pmwiki authorizations $page = RetrieveAuthPage($pn, $authlev, $prompt, $since); if (!$page) { $tbError = "Page=$pn does not have pmwiki auth=$authlev"; return(false); } $text = $page['text']; dbg($d*1, "Text=$text"); # Parse section if specified if ($m['section']) $text = TextSection($text, $m['section']); dbg($d*1, "after section text=$text"); # Run markup rules if requested if ($RuleList && is_array($RuleList)) $text = RunMarkupRules($pagename, $RuleList, $text); dbg($d*1, "after rules text=$text"); $page['ntext'] = $text; return($page); }