1<?php
2/*
3    PmWiki
4    Copyright 2001-2021 Patrick R. Michaud
5    pmichaud@pobox.com
6    http://www.pmichaud.com/
7
8    This program is free software; you can redistribute it and/or modify
9    it under the terms of the GNU General Public License as published by
10    the Free Software Foundation; either version 2 of the License, or
11    (at your option) any later version.
12
13    This program is distributed in the hope that it will be useful,
14    but WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16    GNU General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21
22    ----
23    Note from Pm:  Trying to understand the PmWiki code?  Wish it had
24    more comments?  If you want help with any of the code here,
25    write me at <pmichaud@pobox.com> with your question(s) and I'll
26    provide explanations (and add comments) that answer them.
27
28    Script maintained by Petko YOTOV www.pmwiki.org/petko
29    $Id: pmwiki.php 3711 2021-04-09 05:31:52Z petko $
30*/
31error_reporting(E_ALL ^ E_NOTICE);
32StopWatch('PmWiki');
33@ini_set('magic_quotes_runtime', 0);
34@ini_set('magic_quotes_sybase', 0);
35if (@ini_get('pcre.backtrack_limit') < 1000000)
36  @ini_set('pcre.backtrack_limit', 1000000);
37if (ini_get('register_globals'))
38  foreach($_REQUEST as $k=>$v) {
39    if (preg_match('/^(GLOBALS|_SERVER|_GET|_POST|_COOKIE|_FILES|_ENV|_REQUEST|_SESSION|FarmD|WikiDir)$/i', $k)) exit();
40    ${$k}=''; unset(${$k});
41  }
42$UnsafeGlobals = array_keys($GLOBALS); $GCount=0; $FmtV=array();
43$FmtV['$TokenName'] = 'pmtoken';
44SDV($FarmD,dirname(__FILE__));
45SDV($WorkDir,'wiki.d');
46define('PmWiki',1);
47if (preg_match('/\\w\\w:/', $FarmD)) exit();
48@include_once("$FarmD/scripts/version.php");
49$GroupPattern = '[[:upper:]][\\w]*(?:-\\w+)*';
50$NamePattern = '[[:upper:]\\d][\\w]*(?:-\\w+)*';
51$BlockPattern = 'form|div|table|t[rdh]|p|[uo]l|d[ltd]|h[1-6r]|pre|blockquote';
52$WikiWordPattern = '[[:upper:]][[:alnum:]]*(?:[[:upper:]][[:lower:]0-9]|[[:lower:]0-9][[:upper:]])[[:alnum:]]*';
53$WikiDir = new PageStore('wiki.d/{$FullName}');
54$WikiLibDirs = array(&$WikiDir,new PageStore('$FarmD/wikilib.d/{$FullName}'));
55$PageFileEncodeFunction = 'PUE'; # only used if $WikiDir->encodefilenames is set
56$PageFileDecodeFunction = 'urldecode';
57$LocalDir = 'local';
58$InterMapFiles = array("$FarmD/scripts/intermap.txt",
59  "$FarmD/local/farmmap.txt", '$SiteGroup.InterMap', 'local/localmap.txt');
60$Newline = "\263";                                 # deprecated, 2.0.0
61$KeepToken = "\034\034";
62$Now=time();
63define('READPAGE_CURRENT', $Now+604800);
64$TimeFmt = '%B %d, %Y, at %I:%M %p';
65$TimeISOFmt = '%Y-%m-%dT%H:%M:%S';
66$TimeISOZFmt = '%Y-%m-%dT%H:%M:%SZ';
67$MessagesFmt = array();
68$BlockMessageFmt = "<h3 class='wikimessage'>$[This post has been blocked by the administrator]</h3>";
69$EditFields = array('text');
70$EditFunctions = array('AutoCheckToken', 'EditTemplate', 'RestorePage', 'ReplaceOnSave',
71  'SaveAttributes', 'PostPage', 'PostRecentChanges', 'AutoCreateTargets',
72  'PreviewPage');
73$EnablePost = 1;
74$ChangeSummary = substr(preg_replace('/[\\x00-\\x1f]|=\\]/', '',
75        stripmagic(@$_REQUEST['csum'])), 0, 100);
76$AsSpacedFunction = 'AsSpaced';
77$SpaceWikiWords = 0;
78$RCDelimPattern = '  ';
79$RecentChangesFmt = array(
80  '$SiteGroup.AllRecentChanges' =>
81    '* [[{$Group}.{$Name}]]  . . . $CurrentTime $[by] $AuthorLink: [=$ChangeSummary=]',
82  '$Group.RecentChanges' =>
83    '* [[{$Group}/{$Name}]]  . . . $CurrentTime $[by] $AuthorLink: [=$ChangeSummary=]');
84$UrlScheme = (@$_SERVER['HTTPS']=='on' || @$_SERVER['SERVER_PORT']==443)
85             ? 'https' : 'http';
86$ScriptUrl = $UrlScheme.'://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
87$PubDirUrl = preg_replace('#/[^/]*$#', '/pub', $ScriptUrl, 1);
88$HTMLVSpace = "<vspace>";
89$HTMLPNewline = '';
90$MarkupFrame = array();
91$MarkupFrameBase = array('cs' => array(), 'vs' => '', 'ref' => 0,
92  'closeall' => array(), 'is' => array(),
93  'escape' => 1);
94$WikiWordCountMax = 1000000;
95$WikiWordCount['PmWiki'] = 1;
96$TableRowIndexMax = 1;
97$UrlExcludeChars = '<>"{}|\\\\^`()[\\]\'';
98$QueryFragPattern = "[?#][^\\s$UrlExcludeChars]*";
99$SuffixPattern = '(?:-?[[:alnum:]]+)*';
100$LinkPageSelfFmt = "<a class='selflink' href='\$LinkUrl' title='\$LinkAlt'>\$LinkText</a>";
101$LinkPageExistsFmt = "<a class='wikilink' href='\$LinkUrl' title='\$LinkAlt'>\$LinkText</a>";
102$LinkPageCreateFmt =
103  "<a class='createlinktext' rel='nofollow' title='\$LinkAlt'
104    href='{\$PageUrl}?action=edit'>\$LinkText</a><a rel='nofollow'
105    class='createlink' href='{\$PageUrl}?action=edit'>?</a>";
106$UrlLinkFmt =
107  "<a class='urllink' href='\$LinkUrl' title='\$LinkAlt' rel='nofollow'>\$LinkText</a>";
108umask(002);
109$CookiePrefix = '';
110$SiteGroup = 'Site';
111$SiteAdminGroup = 'SiteAdmin';
112$DefaultGroup = 'Main';
113$DefaultName = 'HomePage';
114$GroupHeaderFmt = '(:include {$Group}.GroupHeader self=0 basepage={*$FullName}:)(:nl:)';
115$GroupFooterFmt = '(:nl:)(:include {$Group}.GroupFooter self=0 basepage={*$FullName}:)';
116$PagePathFmt = array('{$Group}.$1','$1.$1','$1.{$DefaultName}');
117$PageAttributes = array(
118  'passwdread' => '$[Set new read password:]',
119  'passwdedit' => '$[Set new edit password:]',
120  'passwdattr' => '$[Set new attribute password:]');
121$XLLangs = array('en');
122if (preg_match('/^C$|\.UTF-?8/i',setlocale(LC_ALL,0)))
123  setlocale(LC_ALL,'en_US');
124$FmtP = array();
125$FmtPV = array(
126  # '$ScriptUrl'    => 'PUE($ScriptUrl)',   ## $ScriptUrl is special
127  '$PageUrl'      =>
128    'PUE(($EnablePathInfo)
129         ? "$ScriptUrl/$group/$name"
130         : "$ScriptUrl?n=$group.$name")',
131  '$FullName'     => '"$group.$name"',
132  '$Groupspaced'  => '$AsSpacedFunction($group)',
133  '$Namespaced'   => '$AsSpacedFunction($name)',
134  '$Group'        => '$group',
135  '$Name'         => '$name',
136  '$Titlespaced'  => 'FmtPageTitle(@$page["title"], $name, 1)',
137  '$Title'        => 'FmtPageTitle(@$page["title"], $name, 0)',
138  '$LastModifiedBy' => '@$page["author"]',
139  '$LastModifiedHost' => '@$page["host"]',
140  '$LastModified' => 'strftime($GLOBALS["TimeFmt"], $page["time"])',
141  '$LastModifiedSummary' => '@$page["csum"]',
142  '$LastModifiedTime' => '$page["time"]',
143  '$Description'  => '@$page["description"]',
144  '$SiteGroup'    => '$GLOBALS["SiteGroup"]',
145  '$SiteAdminGroup' => '$GLOBALS["SiteAdminGroup"]',
146  '$VersionNum'   => '$GLOBALS["VersionNum"]',
147  '$Version'      => '$GLOBALS["Version"]',
148  '$WikiTitle'    => '$GLOBALS["WikiTitle"]',
149  '$Author'       => 'NoCache($GLOBALS["Author"])',
150  '$AuthId'       => 'NoCache($GLOBALS["AuthId"])',
151  '$DefaultGroup' => '$GLOBALS["DefaultGroup"]',
152  '$DefaultName'  => '$GLOBALS["DefaultName"]',
153  '$BaseName'     => 'MakeBaseName($pn)',
154  '$Action'       => '$GLOBALS["action"]',
155  '$PasswdRead'   => 'PasswdVar($pn, "read")',
156  '$PasswdEdit'   => 'PasswdVar($pn, "edit")',
157  '$PasswdAttr'   => 'PasswdVar($pn, "attr")',
158  );
159$SaveProperties = array('title', 'description', 'keywords');
160$PageTextVarPatterns = array(
161  'var:'        => '/^(:*[ \\t]*(\\w[-\\w]*)[ \\t]*:[ \\t]?)(.*)($)/m',
162  '(:var:...:)' => '/(\\(: *(\\w[-\\w]*) *:(?!\\))\\s?)(.*?)(:\\))/s'
163  );
164
165
166$WikiTitle = 'PmWiki';
167$Charset = 'ISO-8859-1';
168$HTTPHeaders = array(
169  "Expires: Tue, 01 Jan 2002 00:00:00 GMT",
170  "Cache-Control: no-store, no-cache, must-revalidate",
171  "Content-type: text/html; charset=ISO-8859-1;");
172$CacheActions = array('browse','diff','print');
173$EnableHTMLCache = 0;
174$NoHTMLCache = 0;
175$HTMLTagAttr = '';
176$HTMLDoctypeFmt =
177  "<!DOCTYPE html
178    PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"
179    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">
180  <html xmlns='http://www.w3.org/1999/xhtml' \$HTMLTagAttr><head>\n";
181$HTMLStylesFmt['pmwiki'] = "
182  ul, ol, pre, dl, p { margin-top:0px; margin-bottom:0px; }
183  code.escaped { white-space: pre; }
184  .vspace { margin-top:1.33em; }
185  .indent { margin-left:40px; }
186  .outdent { margin-left:40px; text-indent:-40px; }
187  a.createlinktext { text-decoration:none; border-bottom:1px dotted gray; }
188  a.createlink { text-decoration:none; position:relative; top:-0.5em;
189    font-weight:bold; font-size:smaller; border-bottom:none; }
190  img { border:0px; }
191  ";
192$HTMLHeaderFmt['styles'] = array(
193  "<style type='text/css'><!--",&$HTMLStylesFmt,"\n--></style>");
194$HTMLBodyFmt = "</head>\n<body>";
195$HTMLStartFmt = array('headers:',&$HTMLDoctypeFmt,&$HTMLHeaderFmt,
196  &$HTMLBodyFmt);
197$HTMLEndFmt = "\n</body>\n</html>";
198$PageStartFmt = array(&$HTMLStartFmt,"\n<div id='contents'>\n");
199$PageEndFmt = array('</div>',&$HTMLEndFmt);
200
201$HandleActions = array(
202  'browse' => 'HandleBrowse', 'print' => 'HandleBrowse',
203  'edit' => 'HandleEdit', 'source' => 'HandleSource',
204  'attr' => 'HandleAttr', 'postattr' => 'HandlePostAttr',
205  'logout' => 'HandleLogoutA', 'login' => 'HandleLoginA');
206$HandleAuth = array(
207  'browse' => 'read', 'source' => 'read', 'print' => 'read',
208  'edit' => 'edit', 'attr' => 'attr', 'postattr' => 'attr',
209  'logout' => 'read', 'login' => 'login');
210$ActionTitleFmt = array(
211  'edit' => '| $[Edit]',
212  'attr' => '| $[Attributes]',
213  'login' => '| $[Login]');
214$DefaultPasswords = array('admin'=>'@lock','read'=>'','edit'=>'','attr'=>'');
215$AuthCascade = array('edit'=>'read', 'attr'=>'edit');
216$AuthList = array('' => 1, 'nopass:' => 1, '@nopass' => 1);
217$SessionEncode = 'base64_encode';
218$SessionDecode = 'base64_decode';
219
220$CallbackFnTemplates = array(
221  'default' => '%s',
222  'return' => 'return %s;',
223  'markup_e' => 'extract($GLOBALS["MarkupToHTML"]); return %s;',
224  'qualify'  => 'extract($GLOBALS["tmp_qualify"]); return %s;',
225);
226
227$Conditions['enabled'] = '(boolean)@$GLOBALS[$condparm]';
228$Conditions['false'] = 'false';
229$Conditions['true'] = 'true';
230$Conditions['group'] =
231  "(boolean)MatchPageNames(\$pagename, FixGlob(\$condparm, '$1$2.*'))";
232$Conditions['name'] =
233  "(boolean)MatchPageNames(\$pagename, FixGlob(\$condparm, '$1*.$2'))";
234$Conditions['match'] = 'preg_match("!$condparm!",$pagename)';
235$Conditions['authid'] = 'NoCache(@$GLOBALS["AuthId"] > "")';
236$Conditions['exists'] = "(boolean)ListPages(FixGlob(
237  str_replace(array('[[',']]'), array('', ''), \$condparm) , '$1*.$2'))";
238$Conditions['equal'] = 'CompareArgs($condparm) == 0';
239function CompareArgs($arg)
240  { $arg = ParseArgs($arg); return strcmp(@$arg[''][0], @$arg[''][1]); }
241
242$Conditions['auth'] = 'NoCache(CondAuth($pagename, $condparm))';
243function CondAuth($pagename, $condparm) {
244  global $HandleAuth;
245  @list($level, $pn) = explode(' ', $condparm, 2);
246  $pn = ($pn > '') ? MakePageName($pagename, $pn) : $pagename;
247  if (@$HandleAuth[$level]>'') $level = $HandleAuth[$level];
248  return (boolean)RetrieveAuthPage($pn, $level, false, READPAGE_CURRENT);
249}
250
251## CondExpr handles complex conditions (expressions)
252## Portions Copyright 2005 by D. Faure (dfaure@cpan.org)
253function CondExpr($pagename, $condname, $condparm) {
254  global $CondExprOps;
255  SDV($CondExprOps, 'and|x?or|&&|\\|\\||[!()]');
256  if ($condname == '(' || $condname == '[')
257    $condparm = preg_replace('/[\\]\\)]\\s*$/', '', $condparm);
258  $condparm = str_replace('&amp;&amp;', '&&', $condparm);
259  $terms = preg_split("/(?<!\\S)($CondExprOps)(?!\\S)/i", $condparm, -1,
260                      PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
261  foreach($terms as $i => $t) {
262    $t = trim($t);
263    if (preg_match("/^($CondExprOps)$/i", $t)) continue;
264    if ($t) $terms[$i] = CondText($pagename, "if $t", 'TRUE') ? '1' : '0';
265  }
266  return @eval('return(' . implode(' ', $terms) . ');');
267}
268$Conditions['expr'] = 'CondExpr($pagename, $condname, $condparm)';
269$Conditions['('] = 'CondExpr($pagename, $condname, $condparm)';
270$Conditions['['] = 'CondExpr($pagename, $condname, $condparm)';
271
272$MarkupTable['_begin']['seq'] = 'B';
273$MarkupTable['_end']['seq'] = 'E';
274Markup('fulltext','>_begin');
275Markup('split','>fulltext',"\n",
276  '$RedoMarkupLine=1; return explode("\n",$x);');
277Markup('directives','>split');
278Markup('inline','>directives');
279Markup('links','>inline');
280Markup('block','>links');
281Markup('style','>block');
282Markup('closeall', '_begin',
283  '/^\\(:closeall:\\)$/', "MarkupMarkupClose");
284function MarkupMarkupClose() { return '<:block>' . MarkupClose(); }
285
286$ImgExtPattern="\\.(?:gif|jpg|jpeg|a?png|svgz?|GIF|JPG|JPEG|A?PNG|SVGZ?|webp|WEBP)";
287$ImgTagFmt="<img src='\$LinkUrl' alt='\$LinkAlt' title='\$LinkAlt' />";
288
289$BlockMarkups = array(
290  'block' => array('','','',0),
291  'ul' => array('<ul><li>','</li><li>','</li></ul>',1),
292  'dl' => array('<dl>','</dd>','</dd></dl>',1),
293  'ol' => array('<ol><li>','</li><li>','</li></ol>',1),
294  'p' => array('<p>','','</p>',0),
295  'indent' =>
296     array("<div class='indent'>","</div><div class='indent'>",'</div>',1),
297  'outdent' =>
298     array("<div class='outdent'>","</div><div class='outdent'>",'</div>',1),
299  'pre' => array('<pre>','','</pre>',0),
300  'table' => array("<table width='100%'>",'','</table>',0));
301
302foreach(array('http:','https:','mailto:','ftp:','news:','gopher:','nap:',
303    'file:', 'tel:', 'geo:') as $m)
304  { $LinkFunctions[$m] = 'LinkIMap';  $IMap[$m]="$m$1"; }
305$LinkFunctions['<:page>'] = 'LinkPage';
306
307$q = preg_replace('/(\\?|%3f)([-\\w]+=)/i', '&$2', @$_SERVER['QUERY_STRING']);
308if ($q != @$_SERVER['QUERY_STRING']) {
309  unset($_GET);
310  parse_str($q, $_GET);
311  $_REQUEST = array_merge($_REQUEST, $_GET, $_POST);
312}
313
314if (isset($_GET['action'])) $action = $_GET['action'];
315elseif (isset($_POST['action'])) $action = $_POST['action'];
316else $action = 'browse';
317
318$pagename = @$_REQUEST['n'];
319if (!$pagename) $pagename = @$_REQUEST['pagename'];
320if (!$pagename &&
321    preg_match('!^'.preg_quote($_SERVER['SCRIPT_NAME'],'!').'/?([^?]*)!',
322      $_SERVER['REQUEST_URI'],$match))
323  $pagename = urldecode($match[1]);
324if (preg_match('/[\\x80-\\xbf]/',$pagename))
325  $pagename=utf8_decode($pagename);
326$pagename = preg_replace('![^[:alnum:]\\x80-\\xff]+$!','',$pagename);
327$pagename_unfiltered = $pagename;
328$pagename = preg_replace('![${}\'"\\\\]+!', '', $pagename);
329$FmtPV['$RequestedPage'] = 'PHSC($GLOBALS["pagename_unfiltered"], ENT_QUOTES)';
330$Cursor['*'] = &$pagename;
331if (function_exists("date_default_timezone_get") ) { # fix PHP5.3 warnings
332  @date_default_timezone_set(@date_default_timezone_get());
333}
334
335$DenyHtaccessContent = <<<EOF
336<IfModule !mod_authz_host.c>
337  Order Deny,Allow
338  Deny from all
339</IfModule>
340
341<IfModule mod_authz_host.c>
342  Require all denied
343</IfModule>
344
345EOF;
346
347if (file_exists("$FarmD/local/farmconfig.php"))
348  include_once("$FarmD/local/farmconfig.php");
349if (IsEnabled($EnableLocalConfig,1)) {
350  if (file_exists("$LocalDir/config.php"))
351    include_once("$LocalDir/config.php");
352  elseif (file_exists('config.php'))
353    include_once('config.php');
354}
355
356SDV($CurrentTime, strftime($TimeFmt, $Now));
357SDV($CurrentTimeISO, strftime($TimeISOFmt, $Now));
358
359if (IsEnabled($EnableStdConfig,1))
360  include_once("$FarmD/scripts/stdconfig.php");
361
362if (isset($PostConfig) && is_array($PostConfig)) {
363  asort($PostConfig, SORT_NUMERIC);
364  foreach ($PostConfig as $k=>$v) {
365    if (!$k || !$v || $v<50) continue;
366    if (function_exists($k)) $k($pagename);
367    elseif (file_exists($k)) include_once($k);
368  }
369}
370
371function pmsetcookie($name, $val="", $exp=0, $path="", $dom="", $secure=null, $httponly=null) {
372  global $EnableCookieSecure, $EnableCookieHTTPOnly, $SetCookieFunction;
373  if(IsEnabled($SetCookieFunction))
374    return $SetCookieFunction($name, $val, $exp, $path, $dom, $secure, $httponly);
375  if (is_null($secure))   $secure   = IsEnabled($EnableCookieSecure,   false);
376  if (is_null($httponly)) $httponly = IsEnabled($EnableCookieHTTPOnly, false);
377  setcookie($name, $val, $exp, $path, $dom, $secure, $httponly);
378}
379if (IsEnabled($EnableCookieSecure, false))
380    @ini_set('session.cookie_secure', $EnableCookieSecure);
381if (IsEnabled($EnableCookieHTTPOnly, false))
382    @ini_set('session.cookie_httponly', $EnableCookieHTTPOnly);
383
384foreach((array)$InterMapFiles as $f) {
385  $f = FmtPageName($f, $pagename);
386  if (($v = @file($f)))
387    $v = preg_replace('/^\\s*(?>\\w[-\\w]*)(?!:)/m', '$0:', implode('', $v));
388  else if (@PageExists($f)) {
389    $p = ReadPage($f, READPAGE_CURRENT);
390    $v = @$p['text'];
391  } else continue;
392  if (!preg_match_all("/^\\s*(\\w[-\\w]*:)[^\\S\n]+(\\S*)/m", $v,
393                      $match, PREG_SET_ORDER)) continue;
394  foreach($match as $m) {
395    if (strpos($m[2], '$1') === false) $m[2] .= '$1';
396    $LinkFunctions[$m[1]] = 'LinkIMap';
397    $IMap[$m[1]] = FmtPageName($m[2], $pagename);
398  }
399}
400
401$keys = array_keys($AuthCascade);
402while ($keys) {
403  $k = array_shift($keys); $t = $AuthCascade[$k];
404  if (in_array($t, $keys))
405    { unset($AuthCascade[$k]); $AuthCascade[$k] = $t; array_push($keys, $k); }
406}
407
408$LinkPattern = implode('|',array_keys($LinkFunctions));  # after InterMaps
409SDV($LinkPageCreateSpaceFmt,$LinkPageCreateFmt);
410$ActionTitle = FmtPageName(@$ActionTitleFmt[$action], $pagename);
411
412
413if (!@$HandleActions[$action] || !function_exists($HandleActions[$action]))
414  $action='browse';
415if (IsEnabled($EnableActions, 1)) HandleDispatch($pagename, $action);
416Lock(0);
417return;
418
419##  HandleDispatch() is used to dispatch control to the appropriate
420##  action handler with the appropriate permissions.
421##  If a message is supplied, it is added to $MessagesFmt.
422function HandleDispatch($pagename, $action, $msg=NULL) {
423  global $MessagesFmt, $HandleActions, $HandleAuth;
424  if ($msg) $MessagesFmt[] = "<div class='wikimessage'>$msg</div>";
425  $fn = $HandleActions[$action];
426  $auth = @$HandleAuth[$action];
427  if (!$auth) $auth = 'read';
428  return $fn($pagename, $auth);
429}
430
431## helper functions
432function stripmagic($x) {
433  $fn = 'get_magic_quotes_gpc';
434  if (!function_exists($fn)) return $x;
435  if (is_array($x)) {
436    foreach($x as $k=>$v) $x[$k] = stripmagic($v);
437    return $x;
438  }
439  return @$fn() ? stripslashes($x) : $x;
440}
441function pre_r(&$x)
442  { return '<pre>'.PHSC(print_r($x, true)).'</pre>'; }
443function PSS($x)
444  { return str_replace('\\"','"',$x); }
445function PVS($x)
446  { return preg_replace("/\n[^\\S\n]*(?=\n)/", "\n<:vspace>", $x); }
447function PVSE($x) { return PVS(PHSC($x, ENT_NOQUOTES)); }
448function PZZ($x,$y='') { return ''; }
449function PRR($x=NULL)
450  { if ($x || is_null($x)) $GLOBALS['RedoMarkupLine']++; return $x; }
451function PUE($x)
452  { return preg_replace_callback('/[\\x80-\\xff \'"<>]/', "cb_pue", $x); }
453function cb_pue($m) { return '%'.dechex(ord($m[0])); }
454function PQA($x, $keep=true) {
455  $out = '';
456  if (preg_match_all('/([a-zA-Z][-\\w]*)\\s*=\\s*("[^"]*"|\'[^\']*\'|\\S*)/',
457                     $x, $attr, PREG_SET_ORDER)) {
458    foreach($attr as $a) {
459      if (preg_match('/^on/i', $a[1])) continue;
460      $val = preg_replace('/^([\'"]?)(.*)\\1$/', '$2', $a[2]);
461      if ($keep) $val = Keep(PHSC($val, ENT_QUOTES, null, false));
462      else $val = str_replace("'", '&#39;', $val);
463
464      $out .= "{$a[1]}='$val' ";
465    }
466  }
467  return $out;
468}
469function SDV(&$v,$x) { if (!isset($v)) $v=$x; }
470function SDVA(&$var,$val)
471  { foreach($val as $k=>$v) if (!isset($var[$k])) $var[$k]=$v; }
472function IsEnabled(&$var,$f=0)
473  { return (isset($var)) ? $var : $f; }
474function SetTmplDisplay($var, $val)
475  { NoCache(); $GLOBALS['TmplDisplay'][$var] = $val; }
476function NoCache($x = '') { $GLOBALS['NoHTMLCache'] |= 1; return $x; }
477function ParseArgs($x, $optpat = '(?>(\\w+)[:=])') {
478  $z = array();
479  preg_match_all("/($optpat|[-+])?(\"[^\"]*\"|'[^']*'|\\S+)/",
480    $x, $terms, PREG_SET_ORDER);
481  foreach($terms as $t) {
482    $v = preg_replace('/^([\'"])?(.*)\\1$/', '$2', $t[3]);
483    if ($t[2]) { $z['#'][] = $t[2]; $z[$t[2]] = $v; }
484    else { $z['#'][] = $t[1]; $z[$t[1]][] = $v; }
485    $z['#'][] = $v;
486  }
487  return $z;
488}
489function PHSC($x, $flags=ENT_COMPAT, $enc=null, $dbl_enc=true) { # for PHP 5.4
490  if (is_null($enc)) $enc = "ISO-8859-1"; # single-byte charset
491  if (! is_array($x)) return @htmlspecialchars($x, $flags, $enc, $dbl_enc);
492  foreach($x as $k=>$v) $x[$k] = PHSC($v, $flags, $enc, $dbl_enc);
493  return $x;
494}
495function PCCF($code, $template = 'default', $args = '$m') {
496  global $CallbackFnTemplates, $CallbackFunctions, $PCCFOverrideFunction;
497  if ($PCCFOverrideFunction && is_callable($PCCFOverrideFunction))
498    return $PCCFOverrideFunction($code, $template, $args);
499
500  if (!isset($CallbackFnTemplates[$template]))
501    Abort("No \$CallbackFnTemplates[$template]).");
502  $code = sprintf($CallbackFnTemplates[$template], $code);
503  if (!isset($CallbackFunctions[$code])) {
504    $fn = create_function($args, $code); # called by old addon|skin|recipe needing update, see pmwiki.org/Troubleshooting
505    if ($fn) $CallbackFunctions[$code] = $fn;
506    else StopWatch("Failed to create callback function: ".PHSC($code));
507  }
508  return $CallbackFunctions[$code];
509}
510function PPRE($pat, $rep, $x) {
511  $lambda = PCCF("return $rep;");
512  return preg_replace_callback($pat, $lambda, $x);
513}
514function PPRA($array, $x) {
515  foreach((array)$array as $pat => $rep) {
516    $fmt = $x; # for $FmtP
517    if (is_callable($rep) && $rep != '_') $x = preg_replace_callback($pat,$rep,$x);
518    else $x = preg_replace($pat,$rep,$x);# simple text OR called by old addon|skin|recipe needing update, see pmwiki.org/Troubleshooting
519  }
520  return $x;
521}
522## callback functions
523class PPRC { # PmWiki preg replace callbacks + pass local vars
524  var $vars;
525  function __construct($vars = false) {
526    if ($vars && !is_null($vars)) $this->vars = $vars;
527  }
528  function pagevar($m) { # called from FmtPageName
529    $pagename = $this->vars;
530    return PageVar($pagename, $m[1]);
531  }
532}
533
534# restores kept/protected strings
535function cb_expandkpv($m) { return $GLOBALS['KPV'][$m[1]]; }
536
537# make a string upper or lower case in various patterns
538function cb_toupper($m) { return strtoupper($m[1]); }
539function cb_tolower($m) { return strtolower($m[1]); }
540
541function pmcrypt($str, $salt=null) {
542  if ($salt && preg_match('/^(-?@|\\*$)/',  $salt)) return false;
543  if (!is_null($salt)) return crypt($str, $salt);
544
545  if (function_exists('password_hash'))
546    return password_hash($str, PASSWORD_DEFAULT);
547  return crypt($str);
548}
549
550# generate or check a random one-time token to prevent CSRF
551function pmtoken($token = null) {
552  global $SessionMaxTokens, $PmTokenFn;
553  if(IsEnabled($PmTokenFn) && function_exists($PmTokenFn))
554    return $PmTokenFn($token);
555  @session_start();
556  if(!isset($_SESSION['pmtokens'])) $_SESSION['pmtokens'] = array();
557  if(is_null($token)) { # create a one-time token
558    $len = mt_rand(20,30);
559    $token = "";
560    while(strlen($token)<$len) {
561      $token .= chr(mt_rand(32,126));
562    }
563    if(count($_SESSION['pmtokens']))
564      $id = max(array_keys($_SESSION['pmtokens']))+1;
565    else $id = 0;
566    $_SESSION['pmtokens'][$id] = $token;
567    if(IsEnabled($SessionMaxTokens, 0)) {
568      $max = $SessionMaxTokens;
569      $_SESSION['pmtokens'] = array_slice($_SESSION['pmtokens'], -$max, $max, true);
570    }
571    return "$id:" . md5($token);
572  }
573  # else: check a token, if correct, delete it
574  @list($id, $hash) = explode(':', $token);
575  $id = intval($id);
576  if(isset($_SESSION['pmtokens'][$id]) && $hash == md5($_SESSION['pmtokens'][$id])) {
577    unset($_SESSION['pmtokens'][$id]);
578    return true;
579  }
580  return false;
581}
582
583function StopWatch($x) {
584  global $StopWatch, $EnableStopWatch;
585  if (!$EnableStopWatch) return;
586  static $wstart = 0, $ustart = 0;
587  list($usec,$sec) = explode(' ',microtime());
588  $wtime = ($sec+$usec);
589  if (!$wstart) $wstart = $wtime;
590  if ($EnableStopWatch != 2)
591    { $StopWatch[] = sprintf("%05.2f %s", $wtime-$wstart, $x); return; }
592  $dat = getrusage();
593  $utime = ($dat['ru_utime.tv_sec']+$dat['ru_utime.tv_usec']/1000000);
594  if (!$ustart) $ustart=$utime;
595  $StopWatch[] =
596    sprintf("%05.2f %05.2f %s", $wtime-$wstart, $utime-$ustart, $x);
597}
598
599
600## DRange converts a variety of string formats into date (ranges).
601## It returns the start and end timestamps (+1 second) of the specified date.
602function DRange($when) {
603  global $Now;
604  ##  unix/posix @timestamp dates
605  if (preg_match('/^\\s*@(\\d+)\\s*(.*)$/', $when, $m)) {
606    $t0 = $m[2] ? strtotime($m[2], $m[1]) : $m[1];
607    return array($t0, $t0+1);
608  }
609  ##  ISO-8601 dates
610  $dpat = '/
611    (?<!\\d)                 # non-digit
612    (19\\d\\d|20[0-3]\\d)    # year ($1)
613    ([-.\\/]?)               # date separator ($2)
614    (0\\d|1[0-2])            # month ($3)
615    (?: \\2                  # repeat date separator
616      ([0-2]\\d|3[0-1])      # day ($4)
617      (?: T                  # time marker
618        ([01]\\d|2[0-4])     # hour ($5)
619        ([.:]?)              # time separator ($6)
620        ([0-5]\\d)           # minute ($7)
621        (?: \\6              # repeat time separator
622          ([0-5]\\d|60)      # seconds ($8)
623        )?                   # optional :ss
624      )?                     # optional Thh:mm:ss
625    )?                       # optional -ddThh:mm:ss
626    (?!\d)                   # non-digit
627    /x';
628  if (preg_match($dpat, $when, $m) &&
629    !preg_match('/[+-]\\s*\\d+\\s*(sec(ond)?|min(ute)?|forth?night|day|week(day)?|month|year)s?/i', $when)) {
630    $n = $m;
631    ##  if no time given, assume range of 1 day (except when full month)
632    if (@$m[4]>'' && @$m[5] == '') { @$n[4]++; }
633    ##  if no day given, assume 1st of month and full month range
634    if (@$m[4] == '') { $m[4] = 1; $n[4] = 1; $n[3]++; }
635    ##  if no seconds given, assume range of 1 minute (except when full day)
636    if (@$m[7]>'' && @$m[8] == '') { @$n[7]++; }
637    $t0 = @mktime($m[5], $m[7], $m[8], $m[3], $m[4], $m[1]);
638    $t1 = @mktime($n[5], $n[7], $n[8], $n[3], $n[4], $n[1]);
639    return array($t0, $t1);
640  }
641  ##  now, today, tomorrow, yesterday
642  NoCache();
643  if ($when == 'now') return array($Now, $Now+1);
644  $m = localtime(time());
645  if ($when == 'tomorrow') { $m[3]++; $when = 'today'; }
646  if ($when == 'yesterday') { $m[3]--; $when = 'today'; }
647  if ($when == 'today')
648    return array(mktime(0, 0, 0, $m[4]+1, $m[3]  , $m[5]+1900),
649                 mktime(0, 0, 0, $m[4]+1, $m[3]+1, $m[5]+1900));
650  if (preg_match('/^\\s*$/', $when)) return array(-1, -1);
651  $t0 = strtotime($when);
652  $t1 = strtotime("+1 day", $t0);
653  return array($t0, $t1);
654}
655
656## DiffTimeCompact subtracts 2 timestamps and outputs a compact
657## human-readable delay in hours, days, weeks, months or years
658function DiffTimeCompact($time, $time2=null, $precision=1) {
659  if(is_null($time2)) $time2 = $GLOBALS['Now'];
660  $suffix = explode(',', XL('h,d,w,m,y'));
661  $x = $hours = abs($time2 - $time)/3600;
662  if($x<24) return round($x,$precision).$suffix[0];
663  $x /= 24;   if($x<14) return round($x,$precision).$suffix[1];
664  $x /= 7;    if($x< 9) return round($x,$precision).$suffix[2];
665  $x = $hours/2/365.2425; if($x<24) return round($x,$precision).$suffix[3];
666  return round($hours/24/365.2425,$precision).$suffix[4];
667}
668
669## FileSizeCompact outputs a human readable file size
670## with an appropriate suffix.
671## Note: unreliable filemtime()/stat() over 2GB @ 32bit
672function FileSizeCompact($n, $precision=1) {
673  if(!(float)$n) return 0;
674  $units = 'bkMGTPEZY';
675  $b = log((float)$n, 1024);
676  $fb = floor($b);
677  return round(pow(1024,$b-$fb),$precision).@$units[$fb];
678}
679
680## AsSpaced converts a string with WikiWords into a spaced version
681## of that string.  (It can be overridden via $AsSpacedFunction.)
682function AsSpaced($text) {
683  $text = preg_replace("/([[:lower:]\\d])([[:upper:]])/", '$1 $2', $text);
684  $text = preg_replace('/([^-\\d])(\\d[-\\d]*( |$))/','$1 $2',$text);
685  return preg_replace("/([[:upper:]])([[:upper:]][[:lower:]\\d])/",
686    '$1 $2', $text);
687}
688
689## Lock is used to make sure only one instance of PmWiki is running when
690## files are being written.  It does not "lock pages" for editing.
691function Lock($op) {
692  global $WorkDir, $LockFile, $EnableReadOnly;
693  if ($op > 0 && IsEnabled($EnableReadOnly, 0))
694    Abort('Cannot modify site -- $EnableReadOnly is set', 'readonly');
695  SDV($LockFile, "$WorkDir/.flock");
696  mkdirp(dirname($LockFile));
697  static $lockfp,$curop;
698  if (!$lockfp) $lockfp = @fopen($LockFile, "w");
699  if (!$lockfp) {
700    if ($op <= 0) return;
701    @unlink($LockFile);
702    $lockfp = fopen($LockFile,"w") or
703      Abort('Cannot acquire lockfile', 'flock');
704    fixperms($LockFile);
705  }
706  if ($op<0) { flock($lockfp,LOCK_UN); fclose($lockfp); $lockfp=0; $curop=0; }
707  elseif ($op==0) { flock($lockfp,LOCK_UN); $curop=0; }
708  elseif ($op==1 && $curop<1)
709    { session_write_close(); flock($lockfp,LOCK_SH); $curop=1; }
710  elseif ($op==2 && $curop<2)
711    { session_write_close(); flock($lockfp,LOCK_EX); $curop=2; }
712}
713
714## mkdirp creates a directory and its parents as needed, and sets
715## permissions accordingly.
716function mkdirp($dir) {
717  global $ScriptUrl;
718  if (file_exists($dir)) return;
719  if (!file_exists(dirname($dir))) mkdirp(dirname($dir));
720  if (mkdir($dir, 0777)) {
721    fixperms($dir);
722    if (@touch("$dir/xxx")) { unlink("$dir/xxx"); return; }
723    rmdir($dir);
724  }
725  $parent = realpath(dirname($dir));
726  $bdir = basename($dir);
727  $perms = decoct(fileperms($parent) & 03777);
728  $msg = "PmWiki needs to have a writable <tt>$dir/</tt> directory
729    before it can continue.  You can create the directory manually
730    by executing the following commands on your server:
731    <pre>    mkdir $parent/$bdir\n    chmod 777 $parent/$bdir</pre>
732    Then, <a href='{$ScriptUrl}'>reload this page</a>.";
733  $safemode = ini_get('safe_mode');
734  if (!$safemode) $msg .= "<br /><br />Or, for a slightly more
735    secure installation, try executing <pre>    chmod 2777 $parent</pre>
736    on your server and following <a target='_blank' href='$ScriptUrl'>
737    this link</a>.  Afterwards you can restore the permissions to
738    their current setting by executing <pre>    chmod $perms $parent</pre>.";
739  Abort($msg);
740}
741
742## fixperms attempts to correct permissions on a file or directory
743## so that both PmWiki and the account (current dir) owner can manipulate it
744function fixperms($fname, $add = 0, $set = 0) {
745  clearstatcache();
746  if (!file_exists($fname)) Abort('?no such file');
747  if ($set) { # advanced admins, $UploadPermSet
748    if (fileperms($fname) != $set) @chmod($fname,$set);
749  }
750  else {
751    $bp = 0;
752    if (fileowner($fname)!=@fileowner('.') && @fileowner('.')!==0)
753      $bp = (is_dir($fname)) ? 007 : 006;
754    if (filegroup($fname)==@filegroup('.')) $bp <<= 3;
755    $bp |= $add;
756    if ($bp && (fileperms($fname) & $bp) != $bp)
757      @chmod($fname,fileperms($fname)|$bp);
758  }
759}
760
761## GlobToPCRE converts wildcard patterns into pcre patterns for
762## inclusion and exclusion.  Wildcards beginning with '-' or '!'
763## are treated as things to be excluded.
764function GlobToPCRE($pat) {
765  $pat = preg_quote($pat, '/');
766  $pat = str_replace(array('\\*', '\\?', '\\[', '\\]', '\\^', '\\-', '\\!'),
767                     array('.*',  '.',   '[',   ']',   '^', '-', '!'), $pat);
768  $excl = array(); $incl = array();
769  foreach(preg_split('/,+\s?/', $pat, -1, PREG_SPLIT_NO_EMPTY) as $p) {
770    if ($p[0] == '-' || $p[0] == '!') $excl[] = '^'.substr($p, 1).'$';
771    else $incl[] = "^$p$";
772  }
773  return array(implode('|', $incl), implode('|', $excl));
774}
775
776## FixGlob changes wildcard patterns without '.' to things like
777## '*.foo' (name matches) or 'foo.*' (group matches).
778function FixGlob($x, $rep = '$1*.$2') {
779  return preg_replace('/([\\s,][-!]?)([^\\/.\\s,]+)(?=[\\s,])/', $rep, ",$x,");
780}
781
782## MatchPageNames reduces $pagelist to those pages with names
783## matching the pattern(s) in $pat.  Patterns can be either
784## regexes to include ('/'), regexes to exclude ('!'), or
785## wildcard patterns (all others).
786function MatchPageNames($pagelist, $pat) {
787  # Note: MatchNames() is the generic function matching patterns,
788  # works for attachments and other arrays. We can commit to
789  # keep it generic, even if we someday change MatchPageNames().
790  return MatchNames($pagelist, $pat);
791}
792function MatchNames($list, $pat) {
793  global $Charset, $EnableRangeMatchUTF8;
794  # allow range matches in utf8; doesn't work on pmwiki.org and possibly elsewhere
795  $pcre8 = (IsEnabled($EnableRangeMatchUTF8,0) && $Charset=='UTF-8')? 'u' : '';
796  $list = (array)$list;
797  foreach((array)$pat as $p) {
798    if (count($list) < 1) break;
799    if (!$p) continue;
800    switch ($p[0]) {
801      case '/':
802        $list = preg_grep($p, $list);
803        break;
804      case '!':
805        $list = array_diff($list, preg_grep($p, $list));
806        break;
807      default:
808        list($inclp, $exclp) = GlobToPCRE(str_replace('/', '.', $p));
809        if ($exclp)
810          $list = array_diff($list, preg_grep("/$exclp/i$pcre8", $list));
811        if ($inclp)
812          $list = preg_grep("/$inclp/i$pcre8", $list);
813    }
814  }
815  return $list;
816}
817
818## ResolvePageName "normalizes" a pagename based on the current
819## settings of $DefaultPage and $PagePathFmt.  It's normally used
820## during initialization to fix up any missing or partial pagenames.
821function ResolvePageName($pagename) {
822  global $DefaultPage, $DefaultGroup, $DefaultName,
823    $GroupPattern, $NamePattern, $EnableFixedUrlRedirect;
824  SDV($DefaultPage, "$DefaultGroup.$DefaultName");
825  $pagename = preg_replace('!([./][^./]+)\\.html?$!', '$1', $pagename);
826  if ($pagename == '') return $DefaultPage;
827  $p = MakePageName($DefaultPage, $pagename);
828  if (!preg_match("/^($GroupPattern)[.\\/]($NamePattern)$/i", $p)) {
829    header('HTTP/1.1 404 Not Found');
830    Abort("\$[?invalid page name] \"$p\"");
831  }
832  if (preg_match("/^($GroupPattern)[.\\/]($NamePattern)$/i", $pagename))
833    return $p;
834  if (IsEnabled($EnableFixedUrlRedirect, 1)
835      && $p && (PageExists($p) || preg_match('/[\\/.]/', $pagename)))
836    { Redirect($p); exit(); }
837  return MakePageName($DefaultPage, "$pagename.$pagename");
838}
839
840## MakePageName is used to convert a string $str into a fully-qualified
841## pagename.  If $str doesn't contain a group qualifier, then
842## MakePageName uses $basepage and $PagePathFmt to determine the
843## group of the returned pagename.
844function MakePageName($basepage, $str) {
845  global $MakePageNameFunction, $PageNameChars, $PagePathFmt,
846    $MakePageNamePatterns, $MakePageNameSplitPattern;
847  if (@$MakePageNameFunction) return $MakePageNameFunction($basepage, $str);
848  SDV($PageNameChars,'-[:alnum:]');
849  SDV($MakePageNamePatterns, array(
850    "/'/" => '',                      # strip single-quotes
851    "/[^$PageNameChars]+/" => ' ',    # convert everything else to space
852    '/((^|[^-\\w])\\w)/' => 'cb_toupper', # CamelCase
853    '/ /' => ''));
854  SDV($MakePageNameSplitPattern, '/[.\\/]/');
855  $str = preg_replace('/[#?].*$/', '', $str);
856  $m = preg_split($MakePageNameSplitPattern, $str);
857  if (count($m)<1 || count($m)>2 || $m[0]=='') return '';
858  ##  handle "Group.Name" conversions
859  if (@$m[1] > '') {
860    $group = PPRA($MakePageNamePatterns, $m[0]);
861    $name =  PPRA($MakePageNamePatterns, $m[1]);
862    return "$group.$name";
863  }
864  $name = PPRA($MakePageNamePatterns, $m[0]);
865  $isgrouphome = count($m) > 1;
866  foreach((array)$PagePathFmt as $pg) {
867    if ($isgrouphome && strncmp($pg, '$1.', 3) !== 0) continue;
868    $pn = FmtPageName(str_replace('$1', $name, $pg), $basepage);
869    if (PageExists($pn)) return $pn;
870  }
871  if ($isgrouphome) {
872    foreach((array)$PagePathFmt as $pg)
873      if (strncmp($pg, '$1.', 3) == 0)
874        return FmtPageName(str_replace('$1', $name, $pg), $basepage);
875    return "$name.$name";
876  }
877  return preg_replace('/[^\\/.]+$/', $name, $basepage);
878}
879
880
881## MakeBaseName uses $BaseNamePatterns to return the "base" form
882## of a given pagename -- i.e., stripping any recipe-defined
883## prefixes or suffixes from the page.
884function MakeBaseName($pagename, $patlist = NULL) {
885  global $BaseNamePatterns;
886  if (is_null($patlist)) $patlist = (array)@$BaseNamePatterns;
887  foreach($patlist as $pat => $rep)
888    $pagename = preg_replace($pat, $rep, $pagename); # TODO
889  return $pagename;
890}
891
892
893## PCache caches basic information about a page and its attributes--
894## usually everything except page text and page history.  This makes
895## for quicker access to certain values in PageVar below.
896function PCache($pagename, $page) {
897  global $PCache;
898  foreach($page as $k=>$v)
899    if ($k!='text' && strpos($k,':')===false) $PCache[$pagename][$k]=$v;
900}
901
902## SetProperty saves a page property into $PCache.  For convenience
903## it returns the $value of the property just set.  If $sep is supplied,
904## then $value is appended to the current property (with $sep as
905## as separator) instead of replacing it. If $keep is suplied and the
906## property already exists, then $value will be ignored.
907function SetProperty($pagename, $prop, $value, $sep=NULL, $keep=NULL) {
908  global $PCache, $KeepToken;
909  NoCache();
910  $prop = "=p_$prop";
911  $value = preg_replace_callback("/$KeepToken(\\d.*?)$KeepToken/",
912                        "cb_expandkpv", $value);
913  if (!is_null($sep) && isset($PCache[$pagename][$prop]))
914    $value = $PCache[$pagename][$prop] . $sep . $value;
915  if (is_null($keep) || !isset($PCache[$pagename][$prop]))
916    $PCache[$pagename][$prop] = $value;
917  return $PCache[$pagename][$prop];
918}
919
920
921## PageTextVar loads a page's text variables (defined by
922## $PageTextVarPatterns) into a page's $PCache entry, and returns
923## the property associated with $var.
924function PageTextVar($pagename, $var) {
925  global $PCache, $PageTextVarPatterns, $MaxPageTextVars, $DefaultUnsetPageTextVars, $DefaultEmptyPageTextVars;
926  SDV($MaxPageTextVars, 500);
927  static $status;
928  if (@$status["$pagename:$var"]++ > $MaxPageTextVars) return '';
929  if (!@$PCache[$pagename]['=pagetextvars']) {
930    $pc = &$PCache[$pagename];
931    $pc['=pagetextvars'] = 1;
932    $page = RetrieveAuthPage($pagename, 'read', false, READPAGE_CURRENT);
933    if ($page) {
934      foreach((array)$PageTextVarPatterns as $pat)
935        if (preg_match_all($pat, IsEnabled($pc['=preview'],@$page['text']),
936          $match, PREG_SET_ORDER))
937          foreach($match as $m) {
938            $t = preg_replace("/\\{\\$:{$m[2]}\\}/", '', $m[3]);
939            $pc["=p_{$m[2]}"] = Qualify($pagename, $t);
940          }
941    }
942  }
943  if (! isset($PCache[$pagename]["=p_$var"]) && is_array($DefaultUnsetPageTextVars)) {
944    foreach($DefaultUnsetPageTextVars as $k=>$v) {
945      if (count(MatchNames($var, $k))) {
946        $PCache[$pagename]["=p_$var"] = $v;
947        break;
948      }
949    }
950    SDV($PCache[$pagename]["=p_$var"], ''); # to avoid re-loop
951  }
952  elseif (@$PCache[$pagename]["=p_$var"] == '' && is_array($DefaultEmptyPageTextVars)) {
953    foreach($DefaultEmptyPageTextVars as $k=>$v) {
954      if (count(MatchNames($var, $k))) {
955        $PCache[$pagename]["=p_$var"] = $v;
956        break;
957      }
958    }
959    SDV($PCache[$pagename]["=p_$var"], ''); # to avoid re-loop
960  }
961  return @$PCache[$pagename]["=p_$var"];
962}
963
964
965function PageVar($pagename, $var, $pn = '') {
966  global $Cursor, $PCache, $FmtPV, $AsSpacedFunction, $ScriptUrl,
967    $EnablePathInfo;
968  if ($var == '$ScriptUrl') return PUE($ScriptUrl);
969  if ($pn) {
970    $pn = isset($Cursor[$pn]) ? $Cursor[$pn] : MakePageName($pagename, $pn);
971  } else $pn = $pagename;
972  if ($pn) {
973    if (preg_match('/^(.+)[.\\/]([^.\\/]+)$/', $pn, $match)
974        && !isset($PCache[$pn]['time'])
975        && (!@$FmtPV[$var] || strpos($FmtPV[$var], '$page') !== false)) {
976      $page = ReadPage($pn, READPAGE_CURRENT);
977      PCache($pn, $page);
978    }
979    @list($d, $group, $name) = $match;
980    $page = &$PCache[$pn];
981    if (strpos(@$FmtPV[$var], '$authpage') !== false) {
982      if (!isset($page['=auth']['read'])) {
983        $x = RetrieveAuthPage($pn, 'read', false, READPAGE_CURRENT);
984        if ($x) PCache($pn, $x);
985      }
986      if (@$page['=auth']['read']) $authpage = &$page;
987    }
988  } else { $group = ''; $name = ''; }
989  if (@$FmtPV[$var]) return eval("return ({$FmtPV[$var]});");
990  if (strncmp($var, '$:', 2)==0) return PageTextVar($pn, substr($var, 2));
991  return '';
992}
993
994## FmtPageName handles $[internationalization] and $Variable
995## substitutions in strings based on the $pagename argument.
996function FmtPageName($fmt, $pagename) {
997  # Perform $-substitutions on $fmt relative to page given by $pagename
998  global $GroupPattern, $NamePattern, $EnablePathInfo, $ScriptUrl,
999    $GCount, $UnsafeGlobals, $FmtV, $FmtP, $FmtPV, $PCache, $AsSpacedFunction;
1000  if (strpos($fmt,'$')===false) return $fmt;
1001  $fmt = preg_replace_callback('/\\$([A-Z]\\w*Fmt)\\b/','cb_expandglobal',$fmt);
1002  $fmt = preg_replace_callback('/\\$\\[(?>([^\\]]+))\\]/',"cb_expandxlang",$fmt);
1003  $fmt = str_replace('{$ScriptUrl}', '$ScriptUrl', $fmt);
1004  $pprc = new PPRC($pagename);
1005  $fmt = preg_replace_callback('/\\{\\*?(\\$[A-Z]\\w+)\\}/',
1006    array($pprc, 'pagevar'), $fmt);
1007  if (strpos($fmt,'$')===false) return $fmt;
1008  if ($FmtP) $fmt = PPRA($FmtP, $fmt); # FIXME
1009  static $pv, $pvpat;
1010  if ($pv != count($FmtPV)) {
1011    $pvpat = str_replace('$', '\\$', implode('|', array_keys($FmtPV)));
1012    $pv = count($FmtPV);
1013  }
1014  $fmt = preg_replace_callback("/($pvpat)\\b/", array($pprc, 'pagevar'), $fmt);
1015  $fmt = preg_replace_callback('!\\$ScriptUrl/([^?#\'"\\s<>]+)!',
1016    'cb_expandscripturl', $fmt);
1017  if (strpos($fmt,'$')===false) return $fmt;
1018  static $g;
1019  if ($GCount != count($GLOBALS)+count($FmtV)) {
1020    $g = array();
1021    foreach($GLOBALS as $n=>$v) {
1022      if (is_array($v) || is_object($v) ||
1023        isset($FmtV["\$$n"]) || in_array($n,$UnsafeGlobals)) continue;
1024      $g["\$$n"] = $v;
1025    }
1026    $GCount = count($GLOBALS)+count($FmtV);
1027    krsort($g); reset($g);
1028  }
1029  $fmt = str_replace(array_keys($g),array_values($g),$fmt);
1030  $fmt = preg_replace_callback('/(?>(\\$[[:alpha:]]\\w+))/',
1031          "cb_expandfmtv", $fmt);
1032  return $fmt;
1033}
1034function cb_expandglobal($m){ return @$GLOBALS[$m[1]]; }
1035function cb_expandxlang ($m){ return NoCache(XL($m[1])); }
1036function cb_expandfmtv ($m){
1037  return isset($GLOBALS['FmtV'][$m[1]]) ? $GLOBALS['FmtV'][$m[1]] : $m[1];
1038}
1039function cb_expandscripturl($m) {
1040  global $EnablePathInfo, $ScriptUrl;
1041  return (@$EnablePathInfo) ? "$ScriptUrl/" . PUE($m[1])
1042    : "$ScriptUrl?n=".str_replace('/','.',PUE($m[1]));
1043}
1044
1045
1046## FmtPageTitle returns the page title, or the page name.
1047## It localizes standard technical pages (RecentChanges...)
1048function FmtPageTitle($title, $name, $spaced=0) {
1049  if ($title>'') return str_replace("$", "&#036;", $title);
1050  global $SpaceWikiWords, $AsSpacedFunction;
1051  if (preg_match("/^(Site(Admin)?
1052    |(All)?(Site|Group)(Header|Footer|Attributes)
1053    |(Side|Left|Right)Bar
1054    |(Wiki)?Sand[Bb]ox
1055    |(All)?Recent(Changes|Uploads)|(Auth|Edit)Form
1056    |InterMap|PageActions|\\w+QuickReference|\\w+Templates
1057    |NotifyList|AuthUser|ApprovedUrls|(Block|Auth)List
1058    )$/x", $name) && $name != XL($name))
1059      return XL($name);
1060  return ($spaced || $SpaceWikiWords) ? $AsSpacedFunction($name) : $name;
1061}
1062
1063## FmtTemplateVars uses $vars to replace all occurrences of
1064## {$$key} in $text with $vars['key'].
1065function FmtTemplateVars($text, $vars, $pagename = NULL) {
1066  global $FmtPV, $EnableUndefinedTemplateVars;
1067  if ($pagename) {
1068    $pat = implode('|', array_map('preg_quote', array_keys($FmtPV)));
1069    $pprc = new PPRC($pagename);
1070    $text = preg_replace_callback("/\\{\\$($pat)\\}/",
1071              array($pprc, 'pagevar'), $text);
1072  }
1073  foreach(preg_grep('/^[\\w$]/', array_keys($vars)) as $k)
1074    if (!is_array($vars[$k]))
1075      $text = str_replace("{\$\$$k}", @$vars[$k], $text);
1076  if (! IsEnabled($EnableUndefinedTemplateVars, 0))
1077    $text = preg_replace("/\\{\\$\\$\\w+\\}/", '', $text);
1078  return $text;
1079}
1080
1081## The XL functions provide translation tables for $[i18n] strings
1082## in FmtPageName().
1083function XL($key) {
1084  global $XL,$XLLangs;
1085  foreach($XLLangs as $l) if (isset($XL[$l][$key])) return $XL[$l][$key];
1086  return $key;
1087}
1088function XLSDV($lang,$a) {
1089  global $XL;
1090  foreach($a as $k=>$v) {
1091    if (!isset($XL[$lang][$k])) {
1092      if (preg_match('/^e_(rows|cols)$/', $k)) $v = intval($v);
1093      elseif (preg_match('/^ak_/', $k)) $v = @$v[0];
1094      $XL[$lang][$k]=$v;
1095    }
1096  }
1097}
1098function XLPage($lang,$p,$nohtml=false) {
1099  global $TimeFmt,$XLLangs,$FarmD, $EnableXLPageScriptLoad;
1100  $page = ReadPage($p, READPAGE_CURRENT);
1101  if (!$page) return;
1102  $text = preg_replace("/=>\\s*\n/",'=> ',@$page['text']);
1103  foreach(explode("\n",$text) as $l)
1104    if (preg_match('/^\\s*[\'"](.+?)[\'"]\\s*=>\\s*[\'"](.+)[\'"]/',$l,$m))
1105      $xl[stripslashes($m[1])] = stripslashes($nohtml? PHSC($m[2]): $m[2]);
1106  if (isset($xl)) {
1107    if (IsEnabled($EnableXLPageScriptLoad, 0) && @$xl['xlpage-i18n']) {
1108      $i18n = preg_replace('/[^-\\w]/','',$xl['xlpage-i18n']);
1109      include_once("$FarmD/scripts/xlpage-$i18n.php");
1110    }
1111    if (@$xl['Locale']) setlocale(LC_ALL,$xl['Locale']);
1112    if (@$xl['TimeFmt']) $TimeFmt=$xl['TimeFmt'];
1113    if (!in_array($lang, $XLLangs)) array_unshift($XLLangs, $lang);
1114    XLSDV($lang,$xl);
1115  }
1116}
1117
1118## CmpPageAttr is used with uksort to order a page's elements with
1119## the latest items first.  This can make some operations more efficient.
1120function CmpPageAttr($a, $b) {
1121  @list($x, $agmt) = explode(':', $a);
1122  @list($x, $bgmt) = explode(':', $b);
1123  if ($agmt != $bgmt)
1124    return ($agmt==0 || $bgmt==0) ? $agmt - $bgmt : $bgmt - $agmt;
1125  return strcmp($a, $b);
1126}
1127
1128## class PageStore holds objects that store pages via the native
1129## filesystem.
1130class PageStore {
1131  var $dirfmt;
1132  var $iswrite;
1133  var $encodefilenames;
1134  var $attr;
1135  function __construct($d='$WorkDir/$FullName', $w=0, $a=NULL) {
1136    $this->dirfmt = $d; $this->iswrite = $w; $this->attr = (array)$a;
1137    $GLOBALS['PageExistsCache'] = array();
1138  }
1139  function recodefn($s,$from,$to) {
1140    static $able;
1141    if(is_null($able)) {
1142      # can we rely on iconv() or on mb_convert_encoding() ?
1143      if (function_exists('iconv') && @iconv("UTF-8", "WINDOWS-1252//IGNORE", "te\xd0\xafst")=='test' )
1144        $able = 'iconv';
1145      elseif (function_exists('mb_convert_encoding') && @mb_convert_encoding("te\xd0\xafst", "WINDOWS-1252", "UTF-8")=="te?st")
1146        $able = 'mb';
1147    }
1148    switch ($able) {
1149      case "iconv":
1150        return @iconv($from,"$to//IGNORE",$s);
1151      case "mb":
1152        return @mb_convert_encoding($s,$to,$from);
1153    }
1154    if ($to=='UTF-8' && $from=='WINDOWS-1252') return utf8_decode($s);
1155    if ($from=='UTF-8' && $to=='WINDOWS-1252') return utf8_encode($s);
1156    return $s;
1157  }
1158  function pagefile($pagename) {
1159    global $FarmD;
1160    $dfmt = $this->dirfmt;
1161    if ($pagename > '') {
1162      $pagename = str_replace('/', '.', $pagename);
1163      if ($dfmt == 'wiki.d/{$FullName}')               # optimizations for
1164        return $this->PFE("wiki.d/$pagename");         # standard locations
1165      if ($dfmt == '$FarmD/wikilib.d/{$FullName}')     #
1166        return $this->PFE("$FarmD/wikilib.d/$pagename");
1167      if ($dfmt == 'wiki.d/{$Group}/{$FullName}')
1168        return $this->PFE(preg_replace('/([^.]+).*/', 'wiki.d/$1/$0', $pagename));
1169    }
1170    return $this->PFE(FmtPageName($dfmt, $pagename));
1171  }
1172  function PFE($f) { # pagefile_encode
1173    if (!$this->encodefilenames) return $f;
1174    global $PageFileEncodeFunction;
1175    return $PageFileEncodeFunction($f);
1176  }
1177  function PFD($f) { # pagefile_decode
1178    if (!$this->encodefilenames) return $f;
1179    global $PageFileDecodeFunction;
1180    return $PageFileDecodeFunction($f);
1181  }
1182  function read($pagename, $since=0) {
1183    $newline = '';
1184    $urlencoded = false;
1185    $pagefile = $this->pagefile($pagename);
1186    if ($pagefile && ($fp=@fopen($pagefile, "r"))) {
1187      $page = $this->attr;
1188      while (!feof($fp)) {
1189        $line = @fgets($fp, 4096);
1190        while (substr($line, -1, 1) != "\n" && !feof($fp))
1191          { $line .= fgets($fp, 4096); }
1192        $line = rtrim($line);
1193        if ($urlencoded) $line = urldecode(str_replace('+', '%2b', $line));
1194        @list($k,$v) = explode('=', $line, 2);
1195        if (!$k) continue;
1196        if ($k == 'version') {
1197          $ordered = (strpos($v, 'ordered=1') !== false);
1198          $urlencoded = (strpos($v, 'urlencoded=1') !== false);
1199          if (strpos($v, 'pmwiki-0.')!==false) $newline="\262";
1200        }
1201        if ($k == 'newline') { $newline = $v; continue; }
1202        if ($since > 0 && preg_match('/:(\\d+)/', $k, $m) && $m[1] < $since) {
1203          if ($ordered) break;
1204          continue;
1205        }
1206        if ($newline) $v = str_replace($newline, "\n", $v);
1207        $page[$k] = $v;
1208      }
1209      fclose($fp);
1210    }
1211    return $this->recode($pagename, @$page);
1212  }
1213  function write($pagename,$page) {
1214    global $Now, $Version, $Charset, $EnableRevUserAgent, $PageExistsCache, $DenyHtaccessContent;
1215    $page['charset'] = $Charset;
1216    $page['name'] = $pagename;
1217    $page['time'] = $Now;
1218    $page['host'] = $_SERVER['REMOTE_ADDR'];
1219    $page['agent'] = @$_SERVER['HTTP_USER_AGENT'];
1220    if(IsEnabled($EnableRevUserAgent, 0)) $page["agent:$Now"] = $page['agent'];
1221    $page['rev'] = @$page['rev']+1;
1222    unset($page['version']); unset($page['newline']);
1223    uksort($page, 'CmpPageAttr');
1224    $s = false;
1225    $pagefile = $this->pagefile($pagename);
1226    $dir = dirname($pagefile); mkdirp($dir);
1227    if (!file_exists("$dir/.htaccess") && $fp = @fopen("$dir/.htaccess", "w"))
1228      { fwrite($fp, $DenyHtaccessContent); fclose($fp); }
1229    if ($pagefile && ($fp=fopen("$pagefile,new","w"))) {
1230      $r0 = array('%', "\n", '<');
1231      $r1 = array('%25', '%0a', '%3c');
1232      $x = "version=$Version ordered=1 urlencoded=1\n";
1233      $s = true && fputs($fp, $x); $sz = strlen($x);
1234      foreach($page as $k=>$v)
1235        if ($k > '' && $k[0] != '=') {
1236          $x = str_replace($r0, $r1, "$k=$v") . "\n";
1237          $s = $s && fputs($fp, $x); $sz += strlen($x);
1238        }
1239      $s = fclose($fp) && $s;
1240      $s = $s && (filesize("$pagefile,new") > $sz * 0.95);
1241      if (file_exists($pagefile)) $s = $s && unlink($pagefile);
1242      $s = $s && rename("$pagefile,new", $pagefile);
1243    }
1244    $s && fixperms($pagefile);
1245    if (!$s)
1246      Abort("Cannot write page to $pagename ($pagefile)...changes not saved");
1247    PCache($pagename, $page);
1248    unset($PageExistsCache[$pagename]); # PITS:01401
1249  }
1250  function exists($pagename) {
1251    if (!$pagename) return false;
1252    $pagefile = $this->pagefile($pagename);
1253    return ($pagefile && file_exists($pagefile));
1254  }
1255  function delete($pagename) {
1256    global $Now, $PageExistsCache;
1257    $pagefile = $this->pagefile($pagename);
1258    @rename($pagefile,"$pagefile,del-$Now");
1259    unset($PageExistsCache[$pagename]); # PITS:01401
1260  }
1261  function ls($pats=NULL) {
1262    global $GroupPattern, $NamePattern;
1263    StopWatch("PageStore::ls begin {$this->dirfmt}");
1264    $pats=(array)$pats;
1265    array_push($pats, "/^$GroupPattern\.$NamePattern$/");
1266    $dir = $this->pagefile('$Group.$Name');
1267    $maxslash = substr_count($dir, '/');
1268    $dirlist = array(preg_replace('!/*[^/]*\\$.*$!','',$dir));
1269    $out = array();
1270    while (count($dirlist)>0) {
1271      $dir = array_shift($dirlist);
1272      $dfp = @opendir($dir); if (!$dfp) { continue; }
1273      $dirslash = substr_count($dir, '/') + 1;
1274      $o = array();
1275      while ( ($pagefile = readdir($dfp)) !== false) {
1276        if ($pagefile[0] == '.') continue;
1277        if ($dirslash < $maxslash && is_dir("$dir/$pagefile"))
1278          { array_push($dirlist,"$dir/$pagefile"); continue; }
1279        if ($dirslash == $maxslash) $o[] = $this->PFD($pagefile);
1280      }
1281      closedir($dfp);
1282      StopWatch("PageStore::ls merge {$this->dirfmt}");
1283      $out = array_merge($out, MatchPageNames($o, $pats));
1284    }
1285    StopWatch("PageStore::ls end {$this->dirfmt}");
1286    return $out;
1287  }
1288  function recode($pagename, $a) {
1289    if (!$a) return false;
1290    global $Charset, $PageRecodeFunction, $DefaultPageCharset, $EnableOldCharset;
1291    if (function_exists($PageRecodeFunction)) return $PageRecodeFunction($a);
1292    if (IsEnabled($EnableOldCharset)) $a['=oldcharset'] = @$a['charset'];
1293    SDVA($DefaultPageCharset, array(''=>@$Charset)); # pre-2.2.31 RecentChanges
1294    if (@$DefaultPageCharset[$a['charset']]>'')  # wrong pre-2.2.30 encs. *-2, *-9, *-13
1295      $a['charset'] = $DefaultPageCharset[@$a['charset']];
1296    if (!$a['charset'] || $Charset==$a['charset']) return $a;
1297    $from = ($a['charset']=='ISO-8859-1') ? 'WINDOWS-1252' : $a['charset'];
1298    $to = ($Charset=='ISO-8859-1') ? 'WINDOWS-1252' : $Charset;
1299    if($from != $to) {
1300      foreach($a as $k=>$v) $a[$k] = $this->recodefn($v,$from,$to);
1301    }
1302    $a['charset'] = $Charset;
1303    return $a;
1304  }
1305}
1306
1307function ReadPage($pagename, $since=0) {
1308  # read a page from the appropriate directories given by $WikiReadDirsFmt.
1309  global $WikiLibDirs,$Now;
1310  foreach ($WikiLibDirs as $dir) {
1311    $page = $dir->read($pagename, $since);
1312    if ($page) break;
1313  }
1314  if (@!$page) $page['ctime'] = $Now;
1315  if (@!$page['time']) $page['time'] = $Now;
1316  return $page;
1317}
1318
1319function WritePage($pagename,$page) {
1320  global $WikiLibDirs,$WikiDir,$LastModFile;
1321  $WikiDir->iswrite = 1;
1322  for($i=0; $i<count($WikiLibDirs); $i++) {
1323    $wd = &$WikiLibDirs[$i];
1324    if ($wd->iswrite && $wd->exists($pagename)) break;
1325  }
1326  if ($i >= count($WikiLibDirs)) $wd = &$WikiDir;
1327  $wd->write($pagename,$page);
1328  if ($LastModFile && !@touch($LastModFile))
1329    { unlink($LastModFile); touch($LastModFile); fixperms($LastModFile); }
1330}
1331
1332function PageExists($pagename) {
1333  ##  note:  $PageExistsCache might change or disappear someday
1334  global $WikiLibDirs, $PageExistsCache;
1335  if (!isset($PageExistsCache[$pagename])) {
1336    foreach((array)$WikiLibDirs as $dir)
1337      if ($PageExistsCache[$pagename] = $dir->exists($pagename)) break;
1338  }
1339  return $PageExistsCache[$pagename];
1340}
1341
1342function ListPages($pat=NULL) {
1343  global $WikiLibDirs;
1344  foreach((array)$WikiLibDirs as $dir)
1345    $out = array_unique(array_merge($dir->ls($pat),(array)@$out));
1346  return $out;
1347}
1348
1349function RetrieveAuthPage($pagename, $level, $authprompt=true, $since=0) {
1350  global $AuthFunction;
1351  SDV($AuthFunction,'PmWikiAuth');
1352  if (!function_exists($AuthFunction)) return ReadPage($pagename, $since);
1353  return $AuthFunction($pagename, $level, $authprompt, $since);
1354}
1355
1356function Abort($msg, $info='') {
1357  # exit pmwiki with an abort message
1358  global $ScriptUrl, $Charset, $AbortFunction;
1359  if (@$AbortFunction) return $AbortFunction($msg, $info);
1360  if ($info)
1361    $info = "<p class='vspace'><a target='_blank' rel='nofollow' href='http://www.pmwiki.org/pmwiki/info/$info'>$[More information]</a></p>";
1362  $msg = "<h3>$[PmWiki can't process your request]</h3>
1363    <p class='vspace'>$msg</p>
1364    <p class='vspace'>$[We are sorry for any inconvenience].</p>
1365    $info
1366    <p class='vspace'><a href='$ScriptUrl'>$[Return to] $ScriptUrl</a></p>";
1367  @header("Content-type: text/html; charset=$Charset");
1368  echo preg_replace_callback('/\\$\\[([^\\]]+)\\]/', "cb_expandxlang", $msg);
1369  exit;
1370}
1371
1372function Redirect($pagename, $urlfmt='$PageUrl', $redirecturl=null) {
1373  # redirect the browser to $pagename
1374  global $EnableRedirect, $RedirectDelay, $EnableStopWatch;
1375  SDV($RedirectDelay, 0);
1376  clearstatcache();
1377  if (is_null($redirecturl)) $redirecturl = FmtPageName($urlfmt,$pagename);
1378  if (IsEnabled($EnableRedirect,1) &&
1379      (!isset($_REQUEST['redirect']) || $_REQUEST['redirect'])) {
1380    header("Location: $redirecturl");
1381    header("Content-type: text/html");
1382    echo "<html><head>
1383      <meta http-equiv='Refresh' Content='$RedirectDelay; URL=$redirecturl' />
1384     <title>Redirect</title></head><body></body></html>";
1385     exit;
1386  }
1387  echo "<a href='$redirecturl'>Redirect to $redirecturl</a>";
1388  if (@$EnableStopWatch && function_exists('StopWatchHTML'))
1389    StopWatchHTML($pagename, 1);
1390  exit;
1391}
1392
1393function PrintFmt($pagename,$fmt) {
1394  global $HTTPHeaders,$FmtV;
1395  if (is_array($fmt))
1396    { foreach($fmt as $f) PrintFmt($pagename,$f); return; }
1397  if ($fmt == 'headers:') {
1398    foreach($HTTPHeaders as $h) (@$sent++) ? @header($h) : header($h);
1399    return;
1400  }
1401  $x = FmtPageName($fmt,$pagename);
1402  if (strncmp($fmt, 'function:', 9) == 0 &&
1403      preg_match('/^function:(\S+)\s*(.*)$/s', $x, $match) &&
1404      function_exists($match[1]))
1405    { $match[1]($pagename,$match[2]); return; }
1406  if (strncmp($fmt, 'file:', 5) == 0 && preg_match("/^file:(.+)/s",$x,$match)) {
1407    $filelist = preg_split('/[\\s]+/',$match[1],-1,PREG_SPLIT_NO_EMPTY);
1408    foreach($filelist as $f) {
1409      if (file_exists($f)) { include($f); return; }
1410    }
1411    return;
1412  }
1413  if (substr($x, 0, 7) == 'markup:')
1414    { print MarkupToHTML($pagename, substr($x, 7)); return; }
1415  if (substr($x, 0, 5) == 'wiki:')
1416    { PrintWikiPage($pagename, substr($x, 5), 'read'); return; }
1417  if (substr($x, 0, 5) == 'page:')
1418    { PrintWikiPage($pagename, substr($x, 5), ''); return; }
1419  echo $x;
1420}
1421
1422function PrintWikiPage($pagename, $wikilist=NULL, $auth='read') {
1423  if (is_null($wikilist)) $wikilist=$pagename;
1424  $pagelist = preg_split('/\s+/',$wikilist,-1,PREG_SPLIT_NO_EMPTY);
1425  foreach($pagelist as $p) {
1426    if (PageExists($p)) {
1427      $page = ($auth) ? RetrieveAuthPage($p, $auth, false, READPAGE_CURRENT)
1428              : ReadPage($p, READPAGE_CURRENT);
1429      if ($page['text'])
1430        echo MarkupToHTML($pagename,Qualify($p, $page['text']));
1431      return;
1432    }
1433  }
1434}
1435
1436function Keep($x, $pool=NULL) {
1437  if(is_array($x)) $x = $x[0]; # used in many callbacks
1438  # Keep preserves a string from being processed by wiki markups
1439  global $BlockPattern, $KeepToken, $KPV, $KPCount;
1440  $x = preg_replace_callback("/$KeepToken(\\d.*?)$KeepToken/", 'cb_expandkpv', $x);
1441  if (is_null($pool) && preg_match("!</?($BlockPattern)\\b!", $x)) $pool = 'B';
1442  $KPCount++; $KPV[$KPCount.$pool]=$x;
1443  return $KeepToken.$KPCount.$pool.$KeepToken;
1444}
1445
1446
1447##  MarkupEscape examines markup source and escapes any [@...@]
1448##  and [=...=] sequences using Keep().  MarkupRestore undoes the
1449##  effect of any MarkupEscape().
1450function MarkupEscape($text) {
1451  global $EscapePattern;
1452  SDV($EscapePattern, '\\[([=@]).*?\\1\\]');
1453  return preg_replace_callback("/$EscapePattern/s", "Keep", $text);
1454}
1455function MarkupRestore($text) {
1456  global $KeepToken, $KPV;
1457  return preg_replace_callback("/$KeepToken(\\d.*?)$KeepToken/", 'cb_expandkpv', $text);
1458}
1459
1460
1461##  Qualify() applies $QualifyPatterns to convert relative links
1462##  and references into absolute equivalents.
1463function Qualify($pagename, $text) {
1464  global $QualifyPatterns, $KeepToken, $KPV, $tmp_qualify;
1465  if (!@$QualifyPatterns) return $text;
1466  $text = MarkupEscape($text);
1467  $group = $tmp_qualify['group'] = PageVar($pagename, '$Group');
1468  $name  = $tmp_qualify['name']  = PageVar($pagename, '$Name');
1469  $tmp_qualify['pagename'] = $pagename;
1470  $text = PPRA((array)$QualifyPatterns, $text);
1471  return MarkupRestore($text);
1472}
1473
1474
1475function CondText($pagename,$condspec,$condtext) {
1476  global $Conditions;
1477  if (!preg_match("/^(\\S+)\\s*(!?)\\s*(\\S+)?\\s*(.*?)\\s*$/",
1478    $condspec,$match)) return '';
1479  @list($condstr,$condtype,$not,$condname,$condparm) = $match;
1480  if (isset($Conditions[$condname])) {
1481    $tf = @eval("return (".$Conditions[$condname].");");
1482    if (!$tf xor $not) $condtext='';
1483  }
1484  return $condtext;
1485}
1486
1487
1488##  TextSection extracts a section of text delimited by page anchors.
1489##  The $sections parameter can have the form
1490##    #abc           - [[#abc]] to next anchor
1491##    #abc#def       - [[#abc]] up to [[#def]]
1492##    #abc#, #abc..  - [[#abc]] to end of text
1493##    ##abc, #..#abc - beginning of text to [[#abc]]
1494##  Returns the text unchanged if no sections are requested,
1495##  or false if a requested beginning anchor isn't in the text.
1496function TextSection($text, $sections, $args = NULL) {
1497  $args = (array)$args;
1498  $npat = '[[:alpha:]][-\\w.]*';
1499  if (!preg_match("/#($npat)?(\\.\\.)?(#($npat)?)?/", $sections, $match))
1500    return $text;
1501  @list($x, $aa, $dots, $b, $bb) = $match;
1502  if (!$dots && !$b) $bb = $npat;
1503  if ($aa) {
1504    $aa = preg_replace('/\\.\\.$/', '', $aa);
1505    $pos = strpos($text, "[[#$aa]]");  if ($pos === false) return false;
1506    if (@$args['anchors'])
1507      while ($pos > 0 && $text[$pos-1] != "\n") $pos--;
1508    else $pos += strlen("[[#$aa]]");
1509    $text = substr($text, $pos);
1510  }
1511  if ($bb)
1512    $text = preg_replace("/(\n)[^\n]*\\[\\[#$bb\\]\\].*$/s", '$1', $text, 1);
1513  return $text;
1514}
1515
1516
1517##  RetrieveAuthSection extracts a section of text from a page.
1518##  If $pagesection starts with anything other than '#', it identifies
1519##  the page to extract text from.  Otherwise RetrieveAuthSection looks
1520##  in the pages given by $list, or in $pagename if $list is not specified.
1521##  The selected page is placed in the global $RASPageName variable.
1522##  The caller is responsible for calling Qualify() as needed.
1523function RetrieveAuthSection($pagename, $pagesection, $list=NULL, $auth='read') {
1524  global $RASPageName, $PCache;
1525  if ($pagesection[0] != '#')
1526    $list = array(MakePageName($pagename, $pagesection));
1527  else if (is_null($list)) $list = array($pagename);
1528  foreach((array)$list as $t) {
1529    $t = FmtPageName($t, $pagename);
1530    if (!PageExists($t)) continue;
1531    $tpage = RetrieveAuthPage($t, $auth, false, READPAGE_CURRENT);
1532    if (!$tpage) continue;
1533    $text = TextSection(IsEnabled($PCache[$t]['=preview'],$tpage['text']),$pagesection);
1534    if ($text !== false) { $RASPageName = $t; return $text; }
1535  }
1536  $RASPageName = '';
1537  return false;
1538}
1539
1540function IncludeText($pagename, $inclspec) {
1541  global $MaxIncludes, $IncludeOpt, $InclCount, $PCache;
1542  SDV($MaxIncludes,50);
1543  SDVA($IncludeOpt, array('self'=>1));
1544  if (@$InclCount[$pagename]++>=$MaxIncludes) return Keep($inclspec);
1545  $args = array_merge($IncludeOpt, ParseArgs($inclspec));
1546  while (count($args['#'])>0) {
1547    $k = array_shift($args['#']); $v = array_shift($args['#']);
1548    if ($k=='') {
1549      if ($v[0] != '#') {
1550        if (isset($itext)) continue;
1551        $iname = MakePageName($pagename, $v);
1552        if (!$args['self'] && $iname == $pagename) continue;
1553        $ipage = RetrieveAuthPage($iname, 'read', false, READPAGE_CURRENT);
1554        $itext = IsEnabled($PCache[$iname]['=preview'], @$ipage['text']);
1555      }
1556      $itext = TextSection(@$itext, $v, array('anchors' => 1));
1557      continue;
1558    }
1559    if (preg_match('/^(?:line|para)s?$/', $k)) {
1560      preg_match('/^(\\d*)(\\.\\.(\\d*))?$/', $v, $match);
1561      @list($x, $a, $dots, $b) = $match;
1562      $upat = ($k[0] == 'p') ? ".*?(\n\\s*\n|$)" : "[^\n]*(?:\n|$)";
1563      if (!$dots) { $b=$a; $a=0; }
1564      if ($a>0) $a--;
1565      $itext=preg_replace("/^(($upat){0,$b}).*$/s",'$1',$itext,1);
1566      $itext=preg_replace("/^($upat){0,$a}/s",'',$itext,1);
1567      continue;
1568    }
1569  }
1570  $basepage = isset($args['basepage'])
1571              ? MakePageName($pagename, $args['basepage'])
1572              : @$iname;
1573  if ($basepage) $itext = Qualify(@$basepage, @$itext);
1574  return FmtTemplateVars(PVSE(@$itext), $args);
1575}
1576
1577
1578function RedirectMarkup($pagename, $opt) {
1579  $k = Keep("(:redirect $opt:)");
1580  global $MarkupFrame, $EnableRedirectQuiet;
1581  if (!@$MarkupFrame[0]['redirect']) return $k;
1582  $opt = ParseArgs($opt);
1583  $to = @$opt['to']; if (!$to) $to = @$opt[''][0];
1584  if (!$to) return $k;
1585  if (preg_match('/^([^#]+)(#[A-Za-z][-\\w]*)$/', $to, $match))
1586    { $to = $match[1]; $anchor = @$match[2]; }
1587  $to = MakePageName($pagename, $to);
1588  if (!PageExists($to)) return $k;
1589  if ($to == $pagename) return '';
1590  if (@$opt['from']
1591      && !MatchPageNames($pagename, FixGlob($opt['from'], '$1*.$2')))
1592    return '';
1593  if (preg_match('/^30[1237]$/', @$opt['status']))
1594     header("HTTP/1.1 {$opt['status']}");
1595  Redirect($to, "{\$PageUrl}"
1596    . (IsEnabled($EnableRedirectQuiet, 0) && IsEnabled($opt['quiet'], 0)
1597      ? '' : "?from=$pagename")
1598    . $anchor);
1599  exit();
1600}
1601
1602
1603function Block($b) {
1604  global $BlockMarkups,$HTMLVSpace,$HTMLPNewline,$MarkupFrame;
1605  $mf = &$MarkupFrame[0]; $cs = &$mf['cs']; $vspaces = &$mf['vs'];
1606  $out = '';
1607  if ($b == 'vspace') {
1608    $vspaces .= "\n";
1609    while (count($cs)>0 && @end($cs)!='pre' && @$BlockMarkups[@end($cs)][3]==0)
1610      { $c = array_pop($cs); $out .= $BlockMarkups[$c][2]; }
1611    return $out;
1612  }
1613  @list($code, $depth, $icol) = explode(',', $b);
1614  if (!$code) $depth = 1;
1615  if (!is_numeric($depth)) $depth = strlen($depth); # PHP8
1616  if (!is_numeric($icol)) $icol = strlen($icol); # PHP8
1617  if ($depth > 0) $depth += @$mf['idep'];
1618  if ($icol > 0) $mf['is'][$depth] = $icol + @$mf['icol'];
1619  @$mf['idep'] = @$mf['icol'] = 0;
1620  while (count($cs)>$depth)
1621    { $c = array_pop($cs); $out .= @$BlockMarkups[$c][2]; }
1622  if (!$code) {
1623    if (@end($cs) == 'p') { $out .= $HTMLPNewline; $code = 'p'; }
1624    else if ($depth < 2) { $code = 'p'; $mf['is'][$depth] = 0; }
1625    else { $out .= $HTMLPNewline; $code = 'block'; }
1626  }
1627  if ($depth>0 && $depth==count($cs) && $cs[$depth-1]!=$code)
1628    { $c = array_pop($cs); $out .= $BlockMarkups[$c][2]; }
1629  while (count($cs)>0 && @end($cs)!=$code &&
1630      @$BlockMarkups[@end($cs)][3]==0)
1631    { $c = array_pop($cs); $out .= $BlockMarkups[$c][2]; }
1632  if ($vspaces) {
1633    $out .= (@end($cs) == 'pre') ? $vspaces : $HTMLVSpace;
1634    $vspaces='';
1635  }
1636  if ($depth==0) { return $out; }
1637  if ($depth==count($cs)) { return $out.$BlockMarkups[$code][1]; }
1638  while (count($cs)<$depth-1) {
1639    array_push($cs, 'dl'); $mf['is'][count($cs)] = 0;
1640    $out .= $BlockMarkups['dl'][0].'<dd>';
1641  }
1642  if (count($cs)<$depth) {
1643    array_push($cs,$code);
1644    $out .= $BlockMarkups[$code][0];
1645  }
1646  return $out;
1647}
1648
1649
1650function MarkupClose($key = '') {
1651  global $MarkupFrame;
1652  $cf = & $MarkupFrame[0]['closeall'];
1653  $out = '';
1654  if ($key == '' || isset($cf[$key])) {
1655    $k = array_keys((array)$cf);
1656    while ($k) {
1657      $x = array_pop($k); $out .= $cf[$x]; unset($cf[$x]);
1658      if ($x == $key) break;
1659    }
1660  }
1661  return $out;
1662}
1663
1664
1665function FormatTableRow($x, $sep = '\\|\\|') {
1666  global $TableCellAttrFmt, $TableCellAlignFmt, $TableRowAttrFmt,
1667    $TableRowIndexMax, $MarkupFrame, $FmtV, $EnableSimpleTableRowspan;
1668  static $rowcount;
1669  SDV($TableCellAlignFmt, " align='%s'");
1670
1671  if (IsEnabled($EnableSimpleTableRowspan, 0)) {
1672    $x = preg_replace("/\\|\\|__+(?=\\|\\|)/", '||', $x);
1673    $x = preg_replace("/\\|\\|\\^\\^+(?=\\|\\|)/", '', $x);
1674  }
1675  $x = preg_replace("/$sep\\s*$/",'',$x);
1676  $td = preg_split("/$sep/", $x); $y = '';
1677  for($i=0;$i<count($td);$i++) {
1678    if ($td[$i]=='') continue;
1679    $FmtV['$TableCellCount'] = $i;
1680    $attr = FmtPageName(@$TableCellAttrFmt, '');
1681    if (IsEnabled($EnableSimpleTableRowspan, 0)) {
1682      if (preg_match('/(\\+\\++)\\s*$/', $td[$i], $rspn)) {
1683        $td[$i] = preg_replace('/\\+\\++(\\s*)$/', '$1', $td[$i]);
1684        $attr .= " rowspan='".strlen($rspn[1])."'";
1685      }
1686    }
1687    $td[$i] = preg_replace('/^(!?)\\s+$/', '$1&nbsp;', $td[$i]);
1688    if (preg_match('/^!(.*?)!$/',$td[$i],$match))
1689      { $td[$i]=$match[1]; $t='caption'; $attr=''; }
1690    elseif (preg_match('/^!(.*)$/',$td[$i],$match))
1691      { $td[$i]=$match[1]; $t='th'; }
1692    else $t='td';
1693    if (preg_match('/^\\s.*\\s$/',$td[$i])) {
1694      if ($t!='caption') $attr .= sprintf($TableCellAlignFmt, 'center');
1695    }
1696    elseif (preg_match('/^\\s/',$td[$i])) { $attr .= sprintf($TableCellAlignFmt, 'right'); }
1697    elseif (preg_match('/\\s$/',$td[$i])) { $attr .= sprintf($TableCellAlignFmt, 'left'); }
1698    for ($colspan=1;$i+$colspan<count($td);$colspan++)
1699      if ($td[$colspan+$i]!='') break;
1700    if ($colspan>1) { $attr .= " colspan='$colspan'"; }
1701    $y .= "<$t $attr>".trim($td[$i])."</$t>";
1702  }
1703  if ($t=='caption') return "<:table,1>$y";
1704  if (@$MarkupFrame[0]['cs'][0] != 'table') $rowcount = 0; else $rowcount++;
1705  $FmtV['$TableRowCount'] = $rowcount + 1;
1706  $FmtV['$TableRowIndex'] = ($rowcount % $TableRowIndexMax) + 1;
1707  $trattr = FmtPageName(@$TableRowAttrFmt, '');
1708  return "<:table,1><tr $trattr>$y</tr>";
1709}
1710
1711function LinkIMap($pagename,$imap,$path,$alt,$txt,$fmt=NULL) {
1712  global $FmtV, $IMap, $IMapLinkFmt, $UrlLinkFmt, $IMapLocalPath, $ScriptUrl, $AddLinkCSS;
1713  SDVA($IMapLocalPath, array('Path:'=>1));
1714  if (@$IMapLocalPath[$imap]) {
1715    $path = preg_replace('/^(\\w+):/', "$1%3a", $path); # PITS:01260
1716  }
1717  $FmtV['$LinkUrl'] = PUE(str_replace('$1',$path,$IMap[$imap]));
1718  $FmtV['$LinkText'] = $txt;
1719  $FmtV['$LinkAlt'] = Keep(str_replace(array('"',"'"),array('&#34;','&#39;'),$alt));
1720  if (!$fmt)
1721    $fmt = (isset($IMapLinkFmt[$imap])) ? $IMapLinkFmt[$imap] : $UrlLinkFmt;
1722  if(IsEnabled($AddLinkCSS['samedomain'])) {
1723    $parsed_url = parse_url($FmtV['$LinkUrl']);
1724    $parsed_wiki = parse_url($ScriptUrl);
1725    if(! @$parsed_url['host'] || $parsed_url['host'] == $parsed_wiki['host']) {
1726      $fmt = preg_replace('/(<a[^>]*class=["\'])/', "$1{$AddLinkCSS['samedomain']} ", $fmt);
1727    }
1728  }
1729  # remove unused title attributes
1730  if(!$alt) $fmt = preg_replace('/\\stitle=([\'"])\\$LinkAlt\\1/', '', $fmt);
1731  return str_replace(array_keys($FmtV),array_values($FmtV),$fmt);
1732}
1733
1734## These 2 functions hide e-mail addresses from spam bot harvesters
1735## recover them for most users with a javascript utility,
1736## while keeping them readable for users with JS disabled.
1737## Based on Cookbook:DeObMail by Petko Yotov
1738## To enable, set $LinkFunctions['mailto:'] = 'ObfuscateLinkIMap';
1739function ObfuscateLinkIMap($pagename,$imap,$path,$title,$txt,$fmt=NULL) {
1740  global $FmtV, $IMap, $IMapLinkFmt;
1741  SDVA($IMapLinkFmt, array('obfuscate-mailto:' =>
1742    "<span class='_pmXmail' title=\"\$LinkAlt\"><span class='_t'>\$LinkText</span><span class='_m'>\$LinkUrl</span></span>"));
1743  $FmtV['$LinkUrl'] = cb_obfuscate_mail(str_replace('$1',$path,$IMap[$imap]));
1744  $FmtV['$LinkText'] = cb_obfuscate_mail(preg_replace('/^mailto:/i', '', $txt));
1745  if($FmtV['$LinkText'] == preg_replace('/^mailto:/i', '', $FmtV['$LinkUrl'])) $FmtV['$LinkUrl'] = '';
1746  else $FmtV['$LinkUrl'] = " -&gt; ".$FmtV['$LinkUrl'];
1747  $FmtV['$LinkAlt'] = str_replace(array('"',"'"),array('&#34;','&#39;'),cb_obfuscate_mail($title, 0));
1748  return str_replace(array_keys($FmtV),array_values($FmtV), $IMapLinkFmt['obfuscate-mailto:']);
1749}
1750
1751function cb_obfuscate_mail($x, $wrap=1) {
1752  $classes = array('.' => '_d', '@' => '_a');
1753  $texts = array( '.' => XL(' [period] '), '@' => XL(' [snail] '));
1754  foreach($classes as $k=>$v)
1755    $x = preg_replace("/(\\w)".preg_quote($k)."(\\w)/",
1756    ($wrap?
1757      "$1<span class='$v'>{$texts[$k]}</span>$2"
1758      : "$1{$texts[$k]}$2")
1759      , $x);
1760  return $x;
1761}
1762
1763function LinkPage($pagename,$imap,$path,$alt,$txt,$fmt=NULL) {
1764  global $QueryFragPattern, $LinkPageExistsFmt, $LinkPageSelfFmt,
1765    $LinkPageCreateSpaceFmt, $LinkPageCreateFmt, $LinkTargets,
1766    $EnableLinkPageRelative, $EnableLinkPlusTitlespaced, $AddLinkCSS;
1767  $alt = str_replace(array('"',"'"),array('&#34;','&#39;'),$alt);
1768  $path = preg_replace('/(#[-.:\\w]*)#.*$/', '$1', $path); # PITS:01388
1769  if (is_array($txt)) { # PITS:01392
1770    $suffix = $txt[1];
1771    $txt = $txt[0];
1772  }
1773  if (!$fmt && @$path[0] == '#') {
1774    $path = preg_replace("/[^-.:\\w]/", '', $path);
1775    if (trim($txt) == '+') $txt = PageVar($pagename, '$Title') . @$suffix;
1776    if ($alt) $alt = " title='$alt'";
1777    return ($path) ? "<a href='#$path'$alt>".str_replace("$", "&#036;", $txt)."</a>" : '';
1778  }
1779  if (!preg_match("/^\\s*([^#?]+)($QueryFragPattern)?$/",$path,$match))
1780    return '';
1781  $tgtname = MakePageName($pagename, $match[1]);
1782  if (!$tgtname) return '';
1783  $qf = @$match[2];
1784  @$LinkTargets[$tgtname]++;
1785  if (!$fmt) {
1786    if (!PageExists($tgtname) && !preg_match('/[&?]action=/', $qf))
1787      $fmt = preg_match('/\\s/', $txt)
1788             ? $LinkPageCreateSpaceFmt : $LinkPageCreateFmt;
1789    else
1790      $fmt = ($tgtname == $pagename && $qf == '')
1791             ? $LinkPageSelfFmt : $LinkPageExistsFmt;
1792  }
1793  $url = PageVar($tgtname, '$PageUrl');
1794  if (trim($txt) == '+') $txt = PageVar($tgtname,
1795    IsEnabled($EnableLinkPlusTitlespaced, 0) ? '$Titlespaced' : '$Title') . @$suffix;
1796  $txt = str_replace("$", "&#036;", $txt);
1797  if (@$EnableLinkPageRelative)
1798    $url = preg_replace('!^[a-z]+://[^/]*!i', '', $url);
1799  # remove unused title attributes
1800  if(!$alt) $fmt = preg_replace('/\\stitle=([\'"])\\$LinkAlt\\1/', '', $fmt);
1801  $fmt = str_replace(array('$LinkUrl', '$LinkText', '$LinkAlt'),
1802                     array($url.PUE($qf), $txt, Keep($alt)), $fmt);
1803  if(IsEnabled($AddLinkCSS['othergroup'])) {
1804    list($cgroup, ) = explode('.', $pagename);
1805    list($tgroup, ) = explode('.', $tgtname);
1806    if($cgroup != $tgroup)
1807      $fmt = preg_replace('/(<a[^>]*class=["\'])/', "$1{$AddLinkCSS['othergroup']} ", $fmt);
1808  }
1809  return FmtPageName($fmt,$tgtname);
1810}
1811
1812function MakeLink($pagename,$tgt,$txt=NULL,$suffix=NULL,$fmt=NULL) {
1813  global $LinkPattern,$LinkFunctions,$UrlExcludeChars,$ImgExtPattern,$ImgTagFmt,
1814    $LinkTitleFunction;
1815  if(preg_match("/^(.*)(?:\"(.*)\")\\s*$/",$tgt,$x)) list(,$tgt,$title) = $x;
1816  $t = preg_replace('/[()]/','',trim($tgt));
1817  $t = preg_replace('/<[^>]*>/','',$t);
1818  $t = trim(MarkupRestore($t));
1819  $txtr = trim(MarkupRestore($txt));
1820
1821  preg_match("/^($LinkPattern)?(.+)$/",$t,$m);
1822  if (!@$m[1]) $m[1]='<:page>';
1823  if (preg_match("/(($LinkPattern)([^$UrlExcludeChars]+$ImgExtPattern))(\"(.*)\")?$/",$txtr,$tm))
1824    $txt = $LinkFunctions[$tm[2]]($pagename,$tm[2],$tm[3],@$tm[5],
1825      $tm[1],$ImgTagFmt);
1826  else {
1827    if (is_null($txt)) {
1828      $txt = preg_replace('/\\([^)]*\\)/','',$tgt);
1829      if ($m[1]=='<:page>') {
1830        $txt = preg_replace('!/\\s*$!', '', $txt);
1831        $txt = preg_replace('!^.*[^<]/!', '', $txt);
1832      }
1833    }
1834    if ($m[1]=='<:page>' && trim($txt) == '+' && $suffix>'') { # PITS:01392
1835      $txt = array(trim($txt), $suffix);
1836    }
1837    else $txt .= $suffix;
1838  }
1839  if (@$LinkTitleFunction) $title = $LinkTitleFunction($pagename,$m,$txt);
1840  else $title = PHSC(MarkupRestore(@$title), ENT_QUOTES);
1841  $out = $LinkFunctions[$m[1]]($pagename,$m[1],@$m[2],@$title,$txt,$fmt);
1842  return preg_replace('/(<[^>]+)\\stitle=(""|\'\')/', '$1', $out);
1843}
1844
1845function Markup($id, $when, $pat=NULL, $rep=NULL, $tracelev=0) {
1846  global $MarkupTable, $EnableMarkupDiag;
1847  unset($GLOBALS['MarkupRules']);
1848  if (preg_match('/^([<>])?(.+)$/', $when, $m)) {
1849    $MarkupTable[$id]['cmd'] = $when;
1850    $MarkupTable[$m[2]]['dep'][$id] = $m[1];
1851    if (!$m[1]) $m[1] = '=';
1852    if (@$MarkupTable[$m[2]]['seq']) {
1853      $MarkupTable[$id]['seq'] = $MarkupTable[$m[2]]['seq'].$m[1];
1854      foreach((array)@$MarkupTable[$id]['dep'] as $i=>$m)
1855        Markup($i,"$m$id");
1856      unset($GLOBALS['MarkupTable'][$id]['dep']);
1857    }
1858  }
1859  if ($pat && !isset($MarkupTable[$id]['pat'])) {
1860    $MarkupTable[$id]['pat'] = $pat;
1861    $MarkupTable[$id]['rep'] = $rep;
1862
1863    $oldpat = preg_match('!/[^/]*e[^/]*$!', $pat);
1864    if (IsEnabled($EnableMarkupDiag, 0) || $oldpat) {
1865      $exmark = $oldpat ? '!' : ' ';
1866      if (function_exists('debug_backtrace')) {
1867        $dbg = debug_backtrace();
1868        $dbginfo = $dbg[$tracelev];
1869        $MarkupTable[$id]['dbg'] = "$exmark file: {$dbginfo['file']}, "
1870          . "line: {$dbginfo['line']}, pat: {$dbginfo['args'][2]}";
1871      }
1872      else
1873        $MarkupTable[$id]['dbg'] = "$exmark id: '$id', pat: '$pat'";
1874    }
1875  }
1876}
1877
1878function Markup_e($id, $when, $pat, $rep, $template = 'markup_e') {
1879  if (!is_callable($rep)) $rep = PCCF($rep, $template);
1880  Markup($id, $when, $pat, $rep, 1);
1881}
1882
1883function DisableMarkup() {
1884  global $MarkupTable;
1885  $idlist = func_get_args();
1886  unset($GLOBALS['MarkupRules']);
1887  while (count($idlist)>0) {
1888    $id = array_shift($idlist);
1889    if (is_array($id)) { $idlist = array_merge($idlist, $id); continue; }
1890    $MarkupTable[$id] = array('cmd' => 'none', 'pat'=>'');
1891  }
1892}
1893
1894function mpcmp($a,$b) { return @strcmp($a['seq'].'=',$b['seq'].'='); }
1895function BuildMarkupRules() {
1896  global $MarkupTable,$MarkupRules,$LinkPattern;
1897  if (!$MarkupRules) {
1898    uasort($MarkupTable,'mpcmp');
1899    foreach($MarkupTable as $id=>$m)
1900      if (@$m['pat'] && @$m['seq']) {
1901        $MarkupRules[str_replace('\\L',$LinkPattern,$m['pat'])]
1902          = array($m['rep'], $id);
1903      }
1904  }
1905  return $MarkupRules;
1906}
1907
1908
1909function MarkupToHTML($pagename, $text, $opt = NULL) {
1910  # convert wiki markup text to HTML output
1911  global $MarkupRules, $MarkupFrame, $MarkupFrameBase, $WikiWordCount,
1912    $K0, $K1, $RedoMarkupLine, $MarkupToHTML;
1913  $MarkupToHTML['pagename'] = $pagename;
1914
1915  StopWatch('MarkupToHTML begin');
1916  array_unshift($MarkupFrame, array_merge($MarkupFrameBase, (array)$opt));
1917  $MarkupFrame[0]['wwcount'] = $WikiWordCount;
1918  foreach((array)$text as $l)
1919    $lines[] = $MarkupFrame[0]['escape'] ? PVSE($l) : $l;
1920  $lines[] = '(:closeall:)';
1921  $out = '';
1922  while (count($lines)>0) {
1923    $x = array_shift($lines);
1924    $RedoMarkupLine=0;
1925    $markrules = BuildMarkupRules();
1926    foreach($markrules as $p=>$r) {
1927      list($r, $id) = (array)$r;
1928      $MarkupToHTML['markupid'] = $id;
1929      if ($p[0] == '/') {
1930        if (is_callable($r)) $x = preg_replace_callback($p,$r,$x);
1931        else $x=preg_replace($p,$r,$x); # simple text OR called by old addon|skin|recipe needing update, see pmwiki.org/Troubleshooting
1932      }
1933      elseif (strstr($x,$p)!==false) $x=eval($r);
1934      if (isset($php_errormsg)) ### TODO: $php_errormsg removed since PHP 8
1935        { echo "ERROR: pat=$p $php_errormsg"; unset($php_errormsg); }
1936      if ($RedoMarkupLine) { $lines=array_merge((array)$x,$lines); continue 2; }
1937    }
1938    if ($x>'') $out .= "$x\n";
1939  }
1940  foreach((array)(@$MarkupFrame[0]['posteval']) as $v) eval($v);
1941  array_shift($MarkupFrame);
1942  StopWatch('MarkupToHTML end');
1943  return $out;
1944}
1945
1946function HandleBrowse($pagename, $auth = 'read') {
1947  # handle display of a page
1948  global $DefaultPageTextFmt, $PageNotFoundHeaderFmt, $HTTPHeaders,
1949    $EnableHTMLCache, $NoHTMLCache, $PageCacheFile, $LastModTime, $IsHTMLCached,
1950    $FmtV, $HandleBrowseFmt, $PageStartFmt, $PageEndFmt, $PageRedirectFmt;
1951  $page = RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT);
1952  if (!$page) Abort("?cannot read $pagename");
1953  PCache($pagename,$page);
1954  if (PageExists($pagename)) $text = @$page['text'];
1955  else {
1956    SDV($DefaultPageTextFmt,'(:include $[{$SiteGroup}.PageNotFound]:)');
1957    $text = FmtPageName($DefaultPageTextFmt, $pagename);
1958    SDV($PageNotFoundHeaderFmt, 'HTTP/1.1 404 Not Found');
1959    SDV($HTTPHeaders['status'], $PageNotFoundHeaderFmt);
1960  }
1961  $opt = array();
1962  SDV($PageRedirectFmt,"<p><i>($[redirected from] <a rel='nofollow'
1963    href='{\$PageUrl}?action=edit'>{\$FullName}</a>)</i></p>\n");
1964  if (@!$_GET['from']) { $opt['redirect'] = 1; $PageRedirectFmt = ''; }
1965  else {
1966    $frompage = MakePageName($pagename, $_GET['from']);
1967    $PageRedirectFmt = (!$frompage) ? ''
1968      : FmtPageName($PageRedirectFmt, $frompage);
1969  }
1970  if (@$EnableHTMLCache && !$NoHTMLCache && $PageCacheFile &&
1971      @filemtime($PageCacheFile) > $LastModTime) {
1972    list($ctext) = unserialize(file_get_contents($PageCacheFile));
1973    $FmtV['$PageText'] = "<!--cached-->$ctext";
1974    $IsHTMLCached = 1;
1975    StopWatch("HandleBrowse: using cached copy");
1976  } else {
1977    $IsHTMLCached = 0;
1978    $text = '(:groupheader:)'.@$text.'(:groupfooter:)';
1979    $t1 = time();
1980    $FmtV['$PageText'] = MarkupToHTML($pagename, $text, $opt);
1981    if (@$EnableHTMLCache > 0 && !$NoHTMLCache && $PageCacheFile
1982        && (time() - $t1 + 1) >= $EnableHTMLCache) {
1983      $fp = @fopen("$PageCacheFile,new", "x");
1984      if ($fp) {
1985        StopWatch("HandleBrowse: caching page");
1986        fwrite($fp, serialize(array($FmtV['$PageText']))); fclose($fp);
1987        rename("$PageCacheFile,new", $PageCacheFile);
1988      }
1989    }
1990  }
1991  SDV($HandleBrowseFmt,array(&$PageStartFmt, &$PageRedirectFmt, '$PageText',
1992    &$PageEndFmt));
1993  PrintFmt($pagename,$HandleBrowseFmt);
1994}
1995
1996
1997## UpdatePage goes through all of the steps needed to update a page,
1998## preserving page history, computing link targets, page titles,
1999## and other page attributes.  It does this by calling each entry
2000## in $EditFunctions.  $pagename is the name of the page to be updated,
2001## $page is the old version of the page (used for page history),
2002## $new is the new version of the page to be saved, and $fnlist is
2003## an optional list of functions to use instead of $EditFunctions.
2004function UpdatePage(&$pagename, &$page, &$new, $fnlist = NULL) {
2005  global $EditFunctions, $IsPagePosted;
2006  StopWatch("UpdatePage: begin $pagename");
2007  if (is_null($fnlist)) $fnlist = $EditFunctions;
2008  $IsPagePosted = false;
2009  foreach((array)$fnlist as $fn) {
2010    StopWatch("UpdatePage: $fn ($pagename)");
2011    $fn($pagename, $page, $new);
2012  }
2013  StopWatch("UpdatePage: end $pagename");
2014  return $IsPagePosted;
2015}
2016
2017# AutoCheckToken verifies if the posted content was sent
2018# from the website forms, to prevent CSRF
2019function AutoCheckToken() {
2020  # TODO: Work in progress (Jan 2021), releasing for
2021  return true;
2022
2023  global $EnablePost, $AutoCheckTokenActions, $EnablePmToken,
2024    $FmtV, $action, $BlockMessageFmt, $MessagesFmt;
2025
2026  # a quick way to disable tokens
2027  if(! IsEnabled($EnablePmToken, 1)) return true;
2028
2029  SDVA($AutoCheckTokenActions, array( # 1=POST, 2=GET, 0=disabled
2030    'edit' => 1,
2031    'postattr' => 1,
2032    'postupload' => 1,
2033    'approvesites' => 2,
2034    'approveurls' => 2,
2035  ));
2036  $tname = $FmtV['$TokenName'];
2037  $x = @$AutoCheckTokenActions[$action];
2038  if (!$x) return true;
2039  elseif ($x==1) {
2040    if ( count($_POST) < 1 || pmtoken(''.@$_POST[$tname]) ) return true;
2041  }
2042  elseif($x==2 && pmtoken(''.@$_GET[$tname])) return true;
2043
2044  $EnablePost = 0;
2045  $MessagesFmt[] = $BlockMessageFmt;
2046  $MessagesFmt[] = XL('Token invalid or missing.');
2047  return false;
2048}
2049
2050# EditTemplate allows a site administrator to pre-populate new pages
2051# with the contents of another page.
2052function EditTemplate($pagename, &$page, &$new) {
2053  global $EditTemplatesFmt;
2054  if (@$new['text'] > '') return;
2055  if (@$_REQUEST['template'] && PageExists($_REQUEST['template'])) {
2056    $p = RetrieveAuthPage($_REQUEST['template'], 'read', false,
2057             READPAGE_CURRENT);
2058    if ($p['text'] > '') $new['text'] = $p['text'];
2059    return;
2060  }
2061  foreach((array)$EditTemplatesFmt as $t) {
2062    $p = RetrieveAuthPage(FmtPageName($t,$pagename), 'read', false,
2063             READPAGE_CURRENT);
2064    if (@$p['text'] > '') { $new['text'] = $p['text']; return; }
2065  }
2066}
2067
2068# RestorePage handles returning to the version of text as of
2069# the version given by $restore or $_REQUEST['restore'].
2070function RestorePage($pagename,&$page,&$new,$restore=NULL) {
2071  if (is_null($restore)) $restore=@$_REQUEST['restore'];
2072  if (!$restore) return;
2073  $t = $page['text'];
2074  $nl = (substr($t,-1)=="\n");
2075  $t = explode("\n",$t);
2076  if ($nl) array_pop($t);
2077  krsort($page); reset($page);
2078  foreach($page as $k=>$v) {
2079    if ($k<$restore) break;
2080    if (strncmp($k, 'diff:', 5) != 0) continue;
2081    foreach(explode("\n",$v) as $x) {
2082      if (preg_match('/^(\\d+)(,(\\d+))?([adc])(\\d+)/',$x,$match)) {
2083        $a1 = $a2 = $match[1];
2084        if ($match[3]) $a2=$match[3];
2085        $b1 = $match[5];
2086        if ($match[4]=='d') array_splice($t,$b1,$a2-$a1+1);
2087        if ($match[4]=='c') array_splice($t,$b1-1,$a2-$a1+1);
2088        continue;
2089      }
2090      if (strncmp($x,'< ',2) == 0) { $nlflag=true; continue; }
2091      if (preg_match('/^> (.*)$/',$x,$match)) {
2092        $nlflag=false;
2093        array_splice($t,$b1-1,0,$match[1]); $b1++;
2094      }
2095      if ($x=='\\ No newline at end of file') $nl=$nlflag;
2096    }
2097  }
2098  if ($nl) $t[]='';
2099  $new['text']=implode("\n",$t);
2100  $new['=preview'] = $new['text'];
2101  PCache($pagename, $new);
2102  return $new['text'];
2103}
2104
2105## ReplaceOnSave performs text replacements on the text being posted.
2106## Patterns held in $ROEPatterns are replaced on every edit request,
2107## patterns held in $ROSPatterns are replaced only when the page
2108## is being posted (as signaled by $EnablePost).
2109function ReplaceOnSave($pagename,&$page,&$new) {
2110  global $EnablePost, $ROSPatterns, $ROEPatterns;
2111  $new['text'] = ProcessROESPatterns(@$new['text'], $ROEPatterns);
2112  if ($EnablePost) {
2113    $new['text'] = ProcessROESPatterns($new['text'], $ROSPatterns);
2114  }
2115  $new['=preview'] = $new['text'];
2116  PCache($pagename, $new);
2117}
2118function ProcessROESPatterns($text, $patterns) {
2119  global $EnableROSEscape;
2120  if (IsEnabled($EnableROSEscape, 0)) $text = MarkupEscape($text);
2121  $text = PPRA((array)@$patterns, $text);
2122  if (IsEnabled($EnableROSEscape, 0)) $text = MarkupRestore($text);
2123  return $text;
2124}
2125
2126function SaveAttributes($pagename,&$page,&$new) {
2127  global $EnablePost, $LinkTargets, $SaveAttrPatterns, $PCache,
2128    $SaveProperties;
2129  if (!$EnablePost) return;
2130  $text = PPRA($SaveAttrPatterns, $new['text']);
2131  $LinkTargets = array();
2132  $new['=html'] = MarkupToHTML($pagename,$text);
2133  $new['targets'] = implode(',',array_keys((array)$LinkTargets));
2134  $p = & $PCache[$pagename];
2135  foreach((array)$SaveProperties as $k) {
2136    if (@$p["=p_$k"]) $new[$k] = $p["=p_$k"];
2137    else unset($new[$k]);
2138  }
2139  unset($new['excerpt']);
2140}
2141
2142function PostPage($pagename, &$page, &$new) {
2143  global $DiffKeepDays, $DiffFunction, $DeleteKeyPattern, $EnablePost,
2144    $Now, $Charset, $Author, $WikiDir, $IsPagePosted, $DiffKeepNum;
2145  SDV($DiffKeepDays,3650);
2146  SDV($DiffKeepNum,20);
2147  SDV($DeleteKeyPattern,"^\\s*delete\\s*$");
2148  $IsPagePosted = false;
2149  if ($EnablePost) {
2150    $new['charset'] = $Charset; # kept for now, may be needed if custom PageStore
2151    $new['author'] = @$Author;
2152    $new["author:$Now"] = @$Author;
2153    $new["host:$Now"] = $_SERVER['REMOTE_ADDR'];
2154    $diffclass = preg_replace('/\\W/','',@$_POST['diffclass']);
2155    if ($page['time']>0 && function_exists(@$DiffFunction))
2156      $new["diff:$Now:{$page['time']}:$diffclass"] =
2157        $DiffFunction($new['text'],@$page['text']);
2158    $keepgmt = $Now-$DiffKeepDays * 86400;
2159    $keepnum = array();
2160    $keys = array_keys($new);
2161    foreach($keys as $k)
2162      if (preg_match("/^\\w+:(\\d+)/",$k,$match)) {
2163        $keepnum[$match[1]] = 1;
2164        if (count($keepnum)>$DiffKeepNum && $match[1]<$keepgmt)
2165          unset($new[$k]);
2166      }
2167    if (preg_match("/$DeleteKeyPattern/",$new['text'])){
2168      if (@$new['passwdattr']>'' && !CondAuth($pagename, 'attr'))
2169        Abort('$[The page has an "attr" attribute and cannot be deleted.]');
2170      else  $WikiDir->delete($pagename);
2171    }
2172    else WritePage($pagename,$new);
2173    $IsPagePosted = true;
2174  }
2175}
2176
2177function PostRecentChanges($pagename,$page,$new,$Fmt=null) {
2178  global $IsPagePosted, $RecentChangesFmt, $RCDelimPattern, $RCLinesMax,
2179    $EnableRCDiffBytes;
2180  if (!$IsPagePosted && $Fmt==null) return;
2181  if ($Fmt==null) $Fmt = $RecentChangesFmt;
2182  foreach($Fmt as $rcfmt=>$pgfmt) {
2183    $rcname = FmtPageName($rcfmt,$pagename);  if (!$rcname) continue;
2184    $pgtext = FmtPageName($pgfmt,$pagename);  if (!$pgtext) continue;
2185    if (@$seen[$rcname]++) continue;
2186
2187    if (IsEnabled($EnableRCDiffBytes, 0)) {
2188      $pgtext = PPRA(array(
2189        '/\\(([+-])(\\d+)\\)(\\s*=\\]\\s*)$/'=>'$3%diffmarkup%{$1($1$2)$1}%%',
2190        '/\\(\\+(0\\)\\+\\}%%)$/'=>'(&#177;$1'), $pgtext);
2191    }
2192    $rcpage = ReadPage($rcname);
2193    $rcelim = preg_quote(preg_replace("/$RCDelimPattern.*$/",' ',$pgtext),'/');
2194    $rcpage['text'] = preg_replace("/^.*$rcelim.*\n/m", '', @$rcpage['text']);
2195    if (!preg_match("/$RCDelimPattern/",$rcpage['text']))
2196      $rcpage['text'] .= "$pgtext\n";
2197    else
2198      $rcpage['text'] = preg_replace("/([^\n]*$RCDelimPattern.*\n)/",
2199        str_replace("$", "\\$", $pgtext) . "\n$1", $rcpage['text'], 1);
2200    if (@$RCLinesMax > 0)
2201      $rcpage['text'] = implode("\n", array_slice(
2202          explode("\n", $rcpage['text'], $RCLinesMax + 1), 0, $RCLinesMax));
2203    WritePage($rcname, $rcpage);
2204  }
2205}
2206
2207function AutoCreateTargets($pagename, &$page, &$new) {
2208  global $IsPagePosted, $AutoCreate, $LinkTargets;
2209  if (!$IsPagePosted) return;
2210  foreach((array)@$AutoCreate as $pat => $init) {
2211    if (is_null($init)) continue;
2212    foreach(preg_grep($pat, array_keys((array)@$LinkTargets)) as $aname) {
2213      if (PageExists($aname)) continue;
2214      $x = RetrieveAuthPage($aname, 'edit', false, READPAGE_CURRENT);
2215      if (!$x) continue;
2216      WritePage($aname, $init);
2217    }
2218  }
2219}
2220
2221function PreviewPage($pagename,&$page,&$new) {
2222  global $IsPageSaved, $FmtV, $ROSPatterns;
2223  if (@$_REQUEST['preview']) {
2224    $text = ProcessROESPatterns($new['text'], $ROSPatterns);
2225    $text = '(:groupheader:)'.$text.'(:groupfooter:)';
2226    $FmtV['$PreviewText'] = MarkupToHTML($pagename,$text);
2227  }
2228}
2229
2230function HandleEdit($pagename, $auth = 'edit') {
2231  global $IsPagePosted, $EditFields, $ChangeSummary, $EditFunctions,
2232    $EnablePost, $FmtV, $Now, $EditRedirectFmt, $EnableRCDiffBytes,
2233    $PageEditForm, $HandleEditFmt, $PageStartFmt, $PageEditFmt, $PageEndFmt;
2234  SDV($EditRedirectFmt, '$FullName');
2235  if (@$_POST['cancel'])
2236    { Redirect(FmtPageName($EditRedirectFmt, $pagename)); return; }
2237  Lock(2);
2238  $page = RetrieveAuthPage($pagename, $auth, true);
2239  if (!$page) Abort("?cannot edit $pagename");
2240  $new = $page;
2241  foreach((array)$EditFields as $k)
2242    if (isset($_POST[$k])) $new[$k]=str_replace("\r",'',stripmagic($_POST[$k]));
2243
2244  if (IsEnabled($EnableRCDiffBytes, 0) && isset($new['text'])) {
2245    $bytes = strlen($new['text']) - strlen(@$page['text']);
2246    if ($bytes>=0) $bytes = "+$bytes";
2247    $ChangeSummary = rtrim($ChangeSummary) . " ($bytes)";
2248  }
2249  $new['csum'] = $ChangeSummary;
2250  if ($ChangeSummary) $new["csum:$Now"] = $ChangeSummary;
2251  $EnablePost &= (bool)preg_grep('/^post/', array_keys(@$_POST));
2252  $new['=preview'] = @$new['text'];
2253  PCache($pagename, $new);
2254  UpdatePage($pagename, $page, $new);
2255  Lock(0);
2256  if ($IsPagePosted && !@$_POST['postedit'])
2257    { Redirect(FmtPageName($EditRedirectFmt, $pagename)); return; }
2258  $FmtV['$DiffClassMinor'] =
2259    (@$_POST['diffclass']=='minor') ?  "checked='checked'" : '';
2260  $FmtV['$EditText'] =
2261    str_replace('$','&#036;',PHSC(@$new['text'],ENT_NOQUOTES));
2262  $FmtV['$EditBaseTime'] = $Now;
2263  $FmtV['$TokenValue'] = pmtoken();
2264  if (@$PageEditForm) {
2265    $efpage = FmtPageName($PageEditForm, $pagename);
2266    $form = RetrieveAuthPage($efpage, 'read', false, READPAGE_CURRENT);
2267    if (!$form || !@$form['text'])
2268      Abort("?unable to retrieve edit form $efpage", 'editform');
2269    $FmtV['$EditForm'] = MarkupToHTML($pagename, $form['text']);
2270  }
2271  SDV($PageEditFmt, "<div id='wikiedit'>
2272    <h2 class='wikiaction'>$[Editing {\$FullName}]</h2>
2273    <form method='post' rel='nofollow' action='\$PageUrl?action=edit'>
2274    <input type='hidden' name='action' value='edit' />
2275    <input type='hidden' name='n' value='\$FullName' />
2276    <input type='hidden' name='basetime' value='\$EditBaseTime' />
2277    <input type='hidden' name='\$TokenName' value='\$TokenValue' />
2278    \$EditMessageFmt
2279    <textarea id='text' name='text' rows='25' cols='60'
2280      onkeydown='if (event.keyCode==27) event.returnValue=false;'
2281      >\$EditText</textarea><br />
2282    <input type='submit' name='post' value=' $[Save] ' />");
2283  SDV($HandleEditFmt, array(&$PageStartFmt, &$PageEditFmt, &$PageEndFmt));
2284  PrintFmt($pagename, $HandleEditFmt);
2285}
2286
2287function HandleSource($pagename, $auth = 'read') {
2288  global $HTTPHeaders;
2289  $page = RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT);
2290  if (!$page) Abort("?cannot source $pagename");
2291  foreach ($HTTPHeaders as $h) {
2292    $h = preg_replace('!^Content-type:\\s+text/html!i',
2293             'Content-type: text/plain', $h);
2294    header($h);
2295  }
2296  echo @$page['text'];
2297}
2298
2299## PmWikiAuth provides password-protection of pages using PHP sessions.
2300## It is normally called from RetrieveAuthPage.  Since RetrieveAuthPage
2301## can be called a lot within a single page execution (i.e., for every
2302## page accessed), we cache the results of site passwords and
2303## GroupAttribute pages to be able to speed up subsequent calls.
2304function PmWikiAuth($pagename, $level, $authprompt=true, $since=0) {
2305  global $DefaultPasswords, $GroupAttributesFmt, $AllowPassword,
2306    $AuthCascade, $FmtV, $AuthPromptFmt, $PageStartFmt, $PageEndFmt,
2307    $AuthId, $AuthList, $NoHTMLCache;
2308  static $acache;
2309  SDV($GroupAttributesFmt,'$Group/GroupAttributes');
2310  SDV($AllowPassword,'nopass');
2311  $page = ReadPage($pagename, $since);
2312  if (!$page) { return false; }
2313  if (!isset($acache))
2314    SessionAuth($pagename, (@$_POST['authpw'])
2315                           ? array('authpw' => array($_POST['authpw'] => 1))
2316                           : '');
2317  if (@$AuthId) {
2318    $AuthList["id:$AuthId"] = 1;
2319    $AuthList["id:-$AuthId"] = -1;
2320    $AuthList["id:*"] = 1;
2321  }
2322  ## To allow @_site_edit in GroupAttributes, we cache it first
2323  if (!isset($acache['@site'])) {
2324    foreach($DefaultPasswords as $k => $v) {
2325      $x = array(2, array(), '');
2326      $acache['@site'][$k] = IsAuthorized($v, 'site', $x);
2327      $AuthList["@_site_$k"] = $acache['@site'][$k][0] ? 1 : 0;
2328    }
2329  }
2330  $gn = FmtPageName($GroupAttributesFmt, $pagename);
2331  if (!isset($acache[$gn])) {
2332    $gp = ReadPage($gn, READPAGE_CURRENT);
2333    foreach($DefaultPasswords as $k => $v) {
2334      $acache[$gn][$k] = IsAuthorized(@$gp["passwd$k"], 'group',
2335                                      $acache['@site'][$k]);
2336    }
2337  }
2338  foreach($DefaultPasswords as $k => $v)
2339    list($page['=auth'][$k], $page['=passwd'][$k], $page['=pwsource'][$k]) =
2340      IsAuthorized(@$page["passwd$k"], 'page', $acache[$gn][$k]);
2341  foreach($AuthCascade as $k => $t) {
2342    if ($page['=auth'][$k]+0 == 2) {
2343      $page['=auth'][$k] = $page['=auth'][$t];
2344      if ($page['=passwd'][$k] = $page['=passwd'][$t])         # assign
2345        $page['=pwsource'][$k] = "cascade:$t";
2346    }
2347  }
2348  if (@$page['=auth']['admin'])
2349    foreach($page['=auth'] as $lv=>$a) @$page['=auth'][$lv] = 3;
2350  if (@$page['=passwd']['read']) $NoHTMLCache |= 2;
2351  if ($level=='ALWAYS' || @$page['=auth'][$level]) return $page;
2352  if (!$authprompt) return false;
2353  $GLOBALS['AuthNeeded'] = (@$_POST['authpw'])
2354    ? $page['=pwsource'][$level] . ' ' . $level : '';
2355  PCache($pagename, $page);
2356  $postvars = '';
2357  foreach($_POST as $k=>$v) {
2358    if ($k == 'authpw' || $k == 'authid') continue;
2359    $k = PHSC(stripmagic($k), ENT_QUOTES);
2360    if (is_array($v)) {
2361      foreach($v as $vk=>$vv) {
2362        $vk = PHSC(stripmagic($vk), ENT_QUOTES);
2363        $vv = str_replace('$', '&#036;',
2364                PHSC(stripmagic($vv), ENT_COMPAT));
2365        $postvars .= "<input type='hidden' name='{$k}[{$vk}]' value=\"$vv\" />\n";
2366      }
2367    }
2368    else {
2369      $v = str_replace('$', '&#036;',
2370              PHSC(stripmagic($v), ENT_COMPAT));
2371      $postvars .= "<input type='hidden' name='$k' value=\"$v\" />\n";
2372    }
2373  }
2374  $FmtV['$PostVars'] = $postvars;
2375  $r = str_replace("'", '%37', stripmagic($_SERVER['REQUEST_URI']));
2376  SDV($AuthPromptFmt,array(&$PageStartFmt,
2377    "<p><b>$[Password required]</b></p>
2378      <form name='authform' action='$r' method='post'>
2379        $[Password]: <input tabindex='1' type='password' name='authpw'
2380          value='' />
2381        <input type='submit' value='$[OK]' />\$PostVars</form>
2382        <script language='javascript' type='text/javascript'><!--
2383          document.authform.authpw.focus() //--></script>", &$PageEndFmt));
2384  PrintFmt($pagename,$AuthPromptFmt);
2385  exit;
2386}
2387
2388function IsAuthorized($chal, $source, &$from) {
2389  global $AuthList, $AuthPw, $AllowPassword;
2390  if (!$chal) return $from;
2391  $auth = 0;
2392  $passwd = array();
2393  foreach((array)$chal as $c) {
2394    $x = '';
2395    $pwchal = preg_split('/([, ]|\\w+:)/', $c, -1, PREG_SPLIT_DELIM_CAPTURE);
2396    foreach($pwchal as $pw) {
2397      if ($pw == ',' || $pw == '') continue;
2398      else if ($pw == ' ') { $x = ''; continue; }
2399      else if (substr($pw, -1, 1) == ':') { $x = $pw; continue; }
2400      else if ($pw[0] != '@' && $x > '') $pw = $x . $pw;
2401      if (!$pw) continue;
2402      $passwd[] = $pw;
2403      if ($auth < 0) continue;
2404      if ($x || $pw[0] == '@') {
2405        if (@$AuthList[$pw]) $auth = $AuthList[$pw];
2406        continue;
2407      }
2408      if ($AllowPassword && pmcrypt($AllowPassword, $pw) == $pw) # nopass
2409        { $auth=1; continue; }
2410      foreach((array)$AuthPw as $pwresp)                         # password
2411        if (pmcrypt($pwresp, $pw) == $pw) { $auth=1; continue; }
2412    }
2413  }
2414  if (!$passwd) return $from;
2415  if ($auth < 0) $auth = 0;
2416  return array($auth, $passwd, $source);
2417}
2418
2419
2420## SessionAuth works with PmWikiAuth to manage authorizations
2421## as stored in sessions.  First, it can be used to set session
2422## variables by calling it with an $auth argument.  It then
2423## uses the authid, authpw, and authlist session variables
2424## to set the corresponding values of $AuthId, $AuthPw, and $AuthList
2425## as needed.
2426function SessionAuth($pagename, $auth = NULL) {
2427  global $AuthId, $AuthList, $AuthPw, $SessionEncode, $SessionDecode,
2428    $EnableSessionPasswords, $EnableAuthPostRegenerateSID;
2429  static $called;
2430
2431  @$called++;
2432  $sn = session_name(); # in PHP5.3, $_REQUEST doesn't contain $_COOKIE
2433  if (!$auth && ($called > 1 || (!@$_REQUEST[$sn] && !@$_COOKIE[$sn]))) return;
2434
2435  $sid = session_id();
2436  @session_start();
2437  if($called == 1 && isset($_POST['authpw']) && $_POST['authpw']
2438    && IsEnabled($EnableAuthPostRegenerateSID, true)) {
2439    session_regenerate_id();
2440  }
2441
2442  foreach((array)$auth as $k => $v) {
2443    if ($k == 'authpw') {
2444      foreach((array)$v as $pw => $pv) {
2445        if ($SessionEncode) $pw = $SessionEncode($pw);
2446        $_SESSION[$k][$pw] = $pv;
2447      }
2448    }
2449    else if ($k) $_SESSION[$k] = (array)$v + (array)@$_SESSION[$k];
2450  }
2451
2452  if (!isset($AuthId)) $AuthId = @$_SESSION['authid'] ? @end($_SESSION['authid']) : '';
2453  $AuthPw = array_map($SessionDecode, array_keys((array)@$_SESSION['authpw']));
2454  if (!IsEnabled($EnableSessionPasswords, 1)) $_SESSION['authpw'] = array();
2455  $AuthList = array_merge($AuthList, (array)@$_SESSION['authlist']);
2456
2457  if (!$sid) @session_write_close();
2458}
2459
2460
2461function PasswdVar($pagename, $level) {
2462  global $PCache, $PasswdVarAuth, $FmtV;
2463  $page = $PCache[$pagename];
2464  if (!isset($page['=passwd'][$level])) {
2465    $page = RetrieveAuthPage($pagename, 'ALWAYS', false, READPAGE_CURRENT);
2466    if ($page) PCache($pagename, $page);
2467  }
2468  SDV($PasswdVarAuth, 'attr');
2469  if ($PasswdVarAuth && !@$page['=auth'][$PasswdVarAuth]) return XL('(protected)');
2470  $pwsource = $page['=pwsource'][$level];
2471  if (strncmp($pwsource, 'cascade:', 8) == 0) {
2472    $FmtV['$PWCascade'] = substr($pwsource, 8);
2473    return FmtPageName('$[(using $PWCascade password)]', $pagename);
2474  }
2475  $setting = PHSC(implode(' ', preg_replace('/^(?!@|\\w+:).+$/', '****',
2476                                       (array)$page['=passwd'][$level])));
2477  if ($pwsource == 'group' || $pwsource == 'site') {
2478    $FmtV['$PWSource'] = $pwsource;
2479    $setting = FmtPageName('$[(set by $PWSource)] ', $pagename)
2480       . PHSC($setting);
2481  }
2482  return $setting;
2483}
2484
2485
2486function PrintAttrForm($pagename) {
2487  global $PageAttributes, $PCache, $FmtV;
2488  $FmtV['$TokenValue'] = pmtoken();
2489  echo FmtPageName("<form action='\$PageUrl' method='post'>
2490    <input type='hidden' name='action' value='postattr' />
2491    <input type='hidden' name='\$TokenName' value='\$TokenValue' />
2492    <input type='hidden' name='n' value='\$FullName' />
2493    <table>",$pagename);
2494  $page = $PCache[$pagename];
2495  foreach($PageAttributes as $attr=>$p) {
2496    if (!$p) continue;
2497    if (strncmp($attr, 'passwd', 6) == 0) {
2498      $setting = PageVar($pagename, '$Passwd'.ucfirst(substr($attr, 6)));
2499      $value = '';
2500    } else { $setting = $value = PHSC(@$page[$attr]); }
2501    $prompt = FmtPageName($p,$pagename);
2502    echo "<tr><td>$prompt</td>
2503      <td><input type='text' name='$attr' value='$value' /></td>
2504      <td>$setting</td></tr>";
2505  }
2506  echo FmtPageName("</table><input type='submit' value='$[Save]' /></form>",
2507         $pagename);
2508}
2509
2510function HandleAttr($pagename, $auth = 'attr') {
2511  global $HandleAttrFmt,$PageAttrFmt,$PageStartFmt,$PageEndFmt;
2512  $page = RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT);
2513  if (!$page) { Abort("?unable to read $pagename"); }
2514  PCache($pagename,$page);
2515  XLSDV('en', array('EnterAttributes' =>
2516    "Enter new attributes for this page below.  Leaving a field blank
2517    will leave the attribute unchanged.  To clear an attribute, enter
2518    'clear'."));
2519  SDV($PageAttrFmt,"<div class='wikiattr'>
2520    <h2 class='wikiaction'>$[{\$FullName} Attributes]</h2>
2521    <p>$[EnterAttributes]</p></div>");
2522  SDV($HandleAttrFmt,array(&$PageStartFmt,&$PageAttrFmt,
2523    'function:PrintAttrForm',&$PageEndFmt));
2524  PrintFmt($pagename,$HandleAttrFmt);
2525}
2526
2527function HandlePostAttr($pagename, $auth = 'attr') {
2528  global $PageAttributes, $EnablePostAttrClearSession;
2529  if(! AutoCheckToken()) {
2530    Abort('? $[Token invalid or missing.]');
2531  }
2532  Lock(2);
2533  $page = RetrieveAuthPage($pagename, $auth, true);
2534  if (!$page) { Abort("?unable to read $pagename"); }
2535  foreach($PageAttributes as $attr=>$p) {
2536    $v = stripmagic(@$_POST[$attr]);
2537    if ($v == '') continue;
2538    if ($v=='clear') unset($page[$attr]);
2539    else if (strncmp($attr, 'passwd', 6) != 0) $page[$attr] = $v;
2540    else {
2541      $a = array();
2542      preg_match_all('/"[^"]*"|\'[^\']*\'|\\S+/', $v, $match);
2543      foreach($match[0] as $pw)
2544        $a[] = preg_match('/^(@|\\w+:)/', $pw) ? $pw
2545                   : pmcrypt(preg_replace('/^([\'"])(.*)\\1$/', '$2', $pw));
2546      if ($a) $page[$attr] = implode(' ',$a);
2547    }
2548  }
2549  WritePage($pagename,$page);
2550  Lock(0);
2551  if (IsEnabled($EnablePostAttrClearSession, 1)) {
2552    @session_start();
2553    unset($_SESSION['authid']);
2554    unset($_SESSION['authlist']);
2555    $_SESSION['authpw'] = array();
2556  }
2557  Redirect($pagename);
2558  exit;
2559}
2560
2561
2562function HandleLogoutA($pagename, $auth = 'read') {
2563  global $LogoutRedirectFmt, $LogoutCookies;
2564  SDV($LogoutRedirectFmt, '$FullName');
2565  SDV($LogoutCookies, array());
2566  @session_start();
2567  $_SESSION = array();
2568  if ( session_id() != '' || isset($_COOKIE[session_name()]) )
2569    pmsetcookie(session_name(), '', time()-43200, '/');
2570  foreach ($LogoutCookies as $c)
2571    if (isset($_COOKIE[$c])) pmsetcookie($c, '', time()-43200, '/');
2572  session_destroy();
2573  Redirect(FmtPageName($LogoutRedirectFmt, $pagename));
2574}
2575
2576
2577function HandleLoginA($pagename, $auth = 'login') {
2578  global $AuthId, $DefaultPasswords;
2579  unset($DefaultPasswords['admin']);
2580  $prompt = @(!$_POST['authpw'] || ($AuthId != $_POST['authid']));
2581  $page = RetrieveAuthPage($pagename, $auth, $prompt, READPAGE_CURRENT);
2582  Redirect($pagename);
2583}
2584
2585