<?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);
}