pagelist.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. <?php if (!defined('PmWiki')) exit();
  2. /* Copyright 2004-2020 Patrick R. Michaud (pmichaud@pobox.com)
  3. This file is part of PmWiki; you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published
  5. by the Free Software Foundation; either version 2 of the License, or
  6. (at your option) any later version. See pmwiki.php for full details.
  7. This script implements (:pagelist:) and friends -- it's one
  8. of the nastiest scripts you'll ever encounter. Part of the reason
  9. for this is that page listings are so powerful and flexible, so
  10. that adds complexity. They're also expensive, so we have to
  11. optimize them wherever we can.
  12. The core function is FmtPageList(), which will generate a
  13. listing according to a wide variety of options. FmtPageList takes
  14. care of initial option processing, and then calls a "FPL"
  15. (format page list) function to obtain the formatted output.
  16. The FPL function is chosen by the 'fmt=' option to (:pagelist:).
  17. Each FPL function calls MakePageList() to obtain the list
  18. of pages, formats the list somehow, and returns the results
  19. to FmtPageList. FmtPageList then returns the output to
  20. the caller, and calls Keep() (preserves HTML) or PRR() (re-evaluate
  21. as markup) as appropriate for the output being returned.
  22. Script maintained by Petko YOTOV www.pmwiki.org/petko
  23. */
  24. ## $PageIndexFile is the index file for term searches and link= option
  25. if (IsEnabled($EnablePageIndex, 1)) {
  26. SDV($PageIndexFile, "$WorkDir/.pageindex");
  27. $EditFunctions[] = 'PostPageIndex';
  28. }
  29. SDV($StrFoldFunction, 'strtolower');
  30. SDV($PageListSortCmpFunction, 'strcasecmp');
  31. ## $SearchPatterns holds patterns for list= option
  32. SDV($SearchPatterns['all'], array());
  33. SDVA($SearchPatterns['normal'], array(
  34. 'recent' => '!\.(All)?Recent(Changes|Uploads)$!',
  35. 'group' => '!\.Group(Print)?(Header|Footer|Attributes)$!',
  36. 'self' => str_replace('.', '\\.', "!^$pagename$!")));
  37. # The list=grouphomes search pattern requires to scan
  38. # all PageStore directories to get the pagenames.
  39. # This takes (a tiny amoint of) time, so we only do it when needed.
  40. function EnablePageListGroupHomes() {
  41. global $SearchPatterns;
  42. if(isset($SearchPatterns['grouphomes'])) return;
  43. $groups = $homes = array();
  44. foreach(ListPages() as $pn) {
  45. list($g, $n) = explode(".", $pn);
  46. @$groups[$g]++;
  47. }
  48. foreach($groups as $g => $cnt) {
  49. $homes[] = MakePageName("$g.$g", "$g.");
  50. }
  51. $SearchPatterns['grouphomes'] = array('/^('.implode('|', $homes).')$/');
  52. }
  53. ## $FPLFormatOpt is a list of options associated with fmt=
  54. ## values. 'default' is used for any undefined values of fmt=.
  55. SDVA($FPLFormatOpt, array(
  56. 'default' => array('fn' => 'FPLTemplate', 'fmt' => '#default'),
  57. 'bygroup' => array('fn' => 'FPLTemplate', 'template' => '#bygroup',
  58. 'class' => 'fplbygroup'),
  59. 'simple' => array('fn' => 'FPLTemplate', 'template' => '#simple',
  60. 'class' => 'fplsimple'),
  61. 'group' => array('fn' => 'FPLTemplate', 'template' => '#group',
  62. 'class' => 'fplgroup'),
  63. 'title' => array('fn' => 'FPLTemplate', 'template' => '#title',
  64. 'class' => 'fpltitle', 'order' => 'title'),
  65. 'count' => array('fn' => 'FPLCountA'),
  66. ));
  67. SDV($SearchResultsFmt, "<div class='wikisearch'>\$[SearchFor]
  68. <div class='vspace'></div>\$MatchList
  69. <div class='vspace'></div>\$[SearchFound]</div>");
  70. SDV($SearchQuery, str_replace('$', '&#036;',
  71. PHSC(stripmagic(@$_REQUEST['q']), ENT_NOQUOTES)));
  72. XLSDV('en', array(
  73. 'SearchFor' => 'Results of search for <em>$Needle</em>:',
  74. 'SearchFound' =>
  75. '$MatchCount pages found out of $MatchSearched pages searched.'));
  76. SDV($PageListArgPattern, '((?:\\$:?)?\\w[-\\w]*)[:=]');
  77. Markup('pagelist', 'directives',
  78. '/\\(:pagelist(\\s+.*?)?:\\)/i', "MarkupPageList");
  79. Markup('searchbox', 'directives',
  80. '/\\(:searchbox(\\s.*?)?:\\)/', "MarkupPageList");
  81. Markup('searchresults', 'directives',
  82. '/\\(:searchresults(\\s+.*?)?:\\)/i', "MarkupPageList");
  83. function MarkupPageList($m) {
  84. extract($GLOBALS["MarkupToHTML"]); # get $pagename, $markupid
  85. switch ($markupid) {
  86. case 'pagelist':
  87. return FmtPageList('$MatchList', $pagename, array('o' => $m[1].' '));
  88. case 'searchbox':
  89. return SearchBox($pagename,
  90. ParseArgs(@$m[1], $GLOBALS['PageListArgPattern']));
  91. case 'searchresults':
  92. return FmtPageList($GLOBALS['SearchResultsFmt'],
  93. $pagename, array('req' => 1, 'request'=>1, 'o' => @$m[1]));
  94. }
  95. }
  96. # called from PageListIf and FPLExpandItemVars
  97. class cb_pl_expandvars extends PPRC {
  98. function pl_expandvars($m) {
  99. $pn = $this->vars;
  100. return PVSE(PageVar($pn, $m[2], $m[1]));
  101. }
  102. }
  103. SDV($SaveAttrPatterns['/\\(:(searchresults|pagelist)(\\s+.*?)?:\\)/i'], ' ');
  104. SDV($HandleActions['search'], 'HandleSearchA');
  105. SDV($HandleAuth['search'], 'read');
  106. SDV($ActionTitleFmt['search'], '| $[Search Results]');
  107. SDVA($PageListFilters, array(
  108. 'PageListCache' => 80,
  109. 'PageListProtect' => 90,
  110. 'PageListSources' => 100,
  111. 'PageListPasswords' => 120,
  112. 'PageListIf' => 140,
  113. 'PageListTermsTargets' => 160,
  114. 'PageListVariables' => 180,
  115. 'PageListSort' => 900,
  116. ));
  117. function CorePageListSorts($x, $y, $o) {
  118. global $PCache;
  119. if($o == 'title')
  120. return @strcasecmp($PCache[$x]['=title'],$PCache[$y]['=title']);
  121. return @($PCache[$x][$o]-$PCache[$y][$o]);
  122. }
  123. foreach(array('random', 'size', 'time', 'ctime', 'title') as $o)
  124. SDV($PageListSortCmp[$o], 'CorePageListSorts');
  125. define('PAGELIST_PRE' , 1);
  126. define('PAGELIST_ITEM', 2);
  127. define('PAGELIST_POST', 4);
  128. ## SearchBox generates the output of the (:searchbox:) markup.
  129. ## If $SearchBoxFmt is defined, that is used, otherwise a searchbox
  130. ## is generated. Options include group=, size=, label=.
  131. function SearchBox($pagename, $opt) {
  132. global $SearchBoxFmt, $SearchBoxInputType, $SearchBoxOpt, $SearchQuery, $EnablePathInfo;
  133. if (isset($SearchBoxFmt)) return Keep(FmtPageName($SearchBoxFmt, $pagename));
  134. SDVA($SearchBoxOpt, array('size' => '40',
  135. 'label' => FmtPageName('$[Search]', $pagename),
  136. 'value' => str_replace("'", "&#039;", $SearchQuery)));
  137. $opt = array_merge((array)$SearchBoxOpt, @$_GET, (array)$opt);
  138. $opt['action'] = 'search';
  139. $target = (@$opt['target'])
  140. ? MakePageName($pagename, $opt['target']) : $pagename;
  141. $opt['n'] = IsEnabled($EnablePathInfo, 0) ? '' : $target;
  142. $out = FmtPageName(" class='wikisearch' action='\$PageUrl' method='get'>",
  143. $target);
  144. foreach($opt as $k => $v) {
  145. if ($v == '' || is_array($v)) continue;
  146. $v = str_replace("'", "&#039;", $v);
  147. $opt[$k] = $v;
  148. if(preg_match('/^(q|label|value|size|placeholder|aria-\\w+)$/', $k)) continue;
  149. $k = str_replace("'", "&#039;", $k);
  150. $out .= "<input type='hidden' name='$k' value='$v' />";
  151. }
  152. SDV($SearchBoxInputType, 'text');
  153. $out .= "<input type='$SearchBoxInputType' name='q' value='{$opt['value']}' ";
  154. $attrs = preg_grep('/^(placeholder|aria-\\w+)/', array_keys($opt));
  155. foreach ($attrs as $k) $out .= " $k='{$opt[$k]}' ";
  156. $out .= " class='inputbox searchbox' size='{$opt['size']}' /><input type='submit'
  157. class='inputbutton searchbutton' value='{$opt['label']}' />";
  158. return '<form '.Keep($out).'</form>';
  159. }
  160. ## FmtPageList combines options from markup, request form, and url,
  161. ## calls the appropriate formatting function, and returns the string.
  162. function FmtPageList($outfmt, $pagename, $opt) {
  163. global $GroupPattern, $FmtV, $PageListArgPattern,
  164. $FPLFormatOpt, $FPLFunctions;
  165. # get any form or url-submitted request
  166. $rq = PHSC(stripmagic(@$_REQUEST['q']), ENT_NOQUOTES);
  167. # build the search string
  168. $FmtV['$Needle'] = $opt['o'] . ' ' . $rq;
  169. # Handle "group/" at the beginning of the form-submitted request
  170. if (preg_match("!^($GroupPattern(\\|$GroupPattern)*)?/!i", $rq, $match)) {
  171. $opt['group'] = @$match[1];
  172. $rq = substr($rq, strlen(@$match[1])+1);
  173. }
  174. $opt = array_merge($opt, ParseArgs($opt['o'], $PageListArgPattern));
  175. # merge markup options with form and url
  176. if (@$opt['request'] && @$_REQUEST) {
  177. $rkeys = preg_grep('/^=/', array_keys($_REQUEST), PREG_GREP_INVERT);
  178. if ($opt['request'] != '1') {
  179. list($incl, $excl) = GlobToPCRE($opt['request']);
  180. if ($excl) $rkeys = array_diff($rkeys, preg_grep("/$excl/", $rkeys));
  181. if ($incl) $rkeys = preg_grep("/$incl/", $rkeys);
  182. }
  183. $cleanrequest = array();
  184. foreach($rkeys as $k) {
  185. $cleanrequest[$k] = stripmagic($_REQUEST[$k]);
  186. if(substr($k, 0, 4)=='ptv_') # defined separately in forms
  187. $cleanrequest['$:'.substr($k, 4)] = stripmagic($_REQUEST[$k]);
  188. }
  189. $opt = array_merge($opt, ParseArgs($rq, $PageListArgPattern), $cleanrequest);
  190. }
  191. # non-posted blank search requests return nothing
  192. if (@($opt['req'] && !$opt['-'] && !$opt[''] && !$opt['+'] && !$opt['q']))
  193. return '';
  194. # terms and group to be included and excluded
  195. $GLOBALS['SearchIncl'] = array_merge((array)@$opt[''], (array)@$opt['+']);
  196. $GLOBALS['SearchExcl'] = (array)@$opt['-'];
  197. $GLOBALS['SearchGroup'] = @$opt['group'];
  198. $fmt = @$opt['fmt']; if (!$fmt) $fmt = 'default';
  199. $fmtopt = @$FPLFormatOpt[$fmt];
  200. if (!is_array($fmtopt)) {
  201. if ($fmtopt) $fmtopt = array('fn' => $fmtopt);
  202. elseif (@$FPLFunctions[$fmt])
  203. $fmtopt = array('fn' => $FPLFunctions[$fmt]);
  204. else $fmtopt = $FPLFormatOpt['default'];
  205. }
  206. $fmtfn = @$fmtopt['fn'];
  207. if (!is_callable($fmtfn)) $fmtfn = $FPLFormatOpt['default']['fn'];
  208. $matches = array();
  209. $opt = array_merge($fmtopt, $opt);
  210. $out = $fmtfn($pagename, $matches, $opt);
  211. $FmtV['$MatchCount'] = count($matches);
  212. if ($outfmt != '$MatchList')
  213. { $FmtV['$MatchList'] = $out; $out = FmtPageName($outfmt, $pagename); }
  214. if ($out[0] == '<') $out = Keep($out);
  215. return PRR($out);
  216. }
  217. ## MakePageList generates a list of pages using the specifications given
  218. ## by $opt.
  219. function MakePageList($pagename, $opt, $retpages = 1) {
  220. global $MakePageListOpt, $PageListFilters, $PCache;
  221. StopWatch('MakePageList pre');
  222. SDVA($MakePageListOpt, array('list' => 'default'));
  223. $opt = array_merge((array)$MakePageListOpt, (array)$opt);
  224. if (!@$opt['order'] && !@$opt['trail']) $opt['order'] = 'name';
  225. $opt['order'] = preg_replace('/[^-\\w:$]+/', ',', @$opt['order']);
  226. ksort($opt); $opt['=key'] = md5(serialize($opt));
  227. $itemfilters = array(); $postfilters = array();
  228. asort($PageListFilters);
  229. $opt['=phase'] = PAGELIST_PRE; $list=array(); $pn=NULL; $page=NULL;
  230. foreach($PageListFilters as $fn => $v) {
  231. if ($v<0) continue;
  232. $ret = $fn($list, $opt, $pagename, $page);
  233. if ($ret & PAGELIST_ITEM) $itemfilters[] = $fn;
  234. if ($ret & PAGELIST_POST) $postfilters[] = $fn;
  235. }
  236. StopWatch("MakePageList items count=".count($list).", filters=".implode(',',$itemfilters));
  237. $opt['=phase'] = PAGELIST_ITEM;
  238. $matches = array(); $opt['=readc'] = 0;
  239. foreach((array)$list as $pn) {
  240. $page = array();
  241. foreach((array)$itemfilters as $fn)
  242. if (!$fn($list, $opt, $pn, $page)) continue 2;
  243. $page['pagename'] = $page['name'] = $pn;
  244. PCache($pn, $page);
  245. $matches[] = $pn;
  246. }
  247. $list = $matches;
  248. StopWatch("MakePageList post count=".count($list).", readc={$opt['=readc']}");
  249. $opt['=phase'] = PAGELIST_POST; $pn=NULL; $page=NULL;
  250. foreach((array)$postfilters as $fn)
  251. $fn($list, $opt, $pagename, $page);
  252. if ($retpages)
  253. for($i=0; $i<count($list); $i++)
  254. $list[$i] = &$PCache[$list[$i]];
  255. StopWatch('MakePageList end');
  256. return $list;
  257. }
  258. function PageListProtect(&$list, &$opt, $pn, &$page) {
  259. global $EnablePageListProtect;
  260. switch ($opt['=phase']) {
  261. case PAGELIST_PRE:
  262. if (!IsEnabled($EnablePageListProtect, 1) && @$opt['readf'] < 1000)
  263. return 0;
  264. StopWatch("PageListProtect enabled");
  265. $opt['=protectexclude'] = array();
  266. $opt['=protectsafe'] = (array)@$opt['=protectsafe'];
  267. return PAGELIST_ITEM|PAGELIST_POST;
  268. case PAGELIST_ITEM:
  269. if (@$opt['=protectsafe'][$pn]) return 1;
  270. $page = RetrieveAuthPage($pn, 'ALWAYS', false, READPAGE_CURRENT);
  271. $opt['=readc']++;
  272. if (!$page['=auth']['read']) $opt['=protectexclude'][$pn] = 1;
  273. if (!$page['=passwd']['read']) $opt['=protectsafe'][$pn] = 1;
  274. else NoCache();
  275. return 1;
  276. case PAGELIST_POST:
  277. $excl = array_keys($opt['=protectexclude']);
  278. $safe = array_keys($opt['=protectsafe']);
  279. StopWatch("PageListProtect excluded=" .count($excl)
  280. . ", safe=" . count($safe));
  281. $list = array_diff($list, $excl);
  282. return 1;
  283. }
  284. }
  285. function PageListSources(&$list, &$opt, $pn, &$page) {
  286. global $SearchPatterns;
  287. StopWatch('PageListSources begin');
  288. if ($opt['list'] == 'grouphomes') EnablePageListGroupHomes();
  289. ## add the list= option to our list of pagename filter patterns
  290. $opt['=pnfilter'] = array_merge((array)@$opt['=pnfilter'],
  291. (array)@$SearchPatterns[$opt['list']]);
  292. if (@$opt['group']) $opt['=pnfilter'][] = FixGlob($opt['group'], '$1$2.*');
  293. if (@$opt['name']) $opt['=pnfilter'][] = FixGlob($opt['name'], '$1*.$2');
  294. if (@$opt['trail']) {
  295. $trail = ReadTrail($pn, $opt['trail']);
  296. $tlist = array();
  297. foreach($trail as $tstop) {
  298. $n = $tstop['pagename'];
  299. $tlist[] = $n;
  300. $tstop['parentnames'] = array();
  301. PCache($n, $tstop);
  302. }
  303. foreach($trail as $tstop)
  304. $PCache[$tstop['pagename']]['parentnames'][] =
  305. @$trail[$tstop['parent']]['pagename'];
  306. if (!@$opt['=cached']) $list = MatchPageNames($tlist, $opt['=pnfilter']);
  307. } else if (!@$opt['=cached']) $list = ListPages($opt['=pnfilter']);
  308. StopWatch("PageListSources end count=".count($list));
  309. return 0;
  310. }
  311. function PageListPasswords(&$list, &$opt, $pn, &$page) {
  312. if ($opt['=phase'] == PAGELIST_PRE)
  313. return (@$opt['passwd'] > '' && !@$opt['=cached']) ? PAGELIST_ITEM : 0;
  314. if (!$page) { $page = ReadPage($pn, READPAGE_CURRENT); $opt['=readc']++; }
  315. if (!$page) return 0;
  316. return (boolean)preg_grep('/^passwd/', array_keys($page));
  317. }
  318. function PageListIf(&$list, &$opt, $pn, &$page) {
  319. global $Conditions, $Cursor;
  320. ## See if we have any "if" processing to perform
  321. if ($opt['=phase'] == PAGELIST_PRE)
  322. return (@$opt['if'] > '') ? PAGELIST_ITEM : 0;
  323. $condspec = $opt['if'];
  324. $Cursor['='] = $pn;
  325. $varpat = '\\{([=*]|!?[-\\w.\\/\\x80-\\xff]*)(\\$:?\\w+)\\}';
  326. while (preg_match("/$varpat/", $condspec, $match)) {
  327. $cb = new cb_pl_expandvars($pn);
  328. $condspec = preg_replace_callback("/$varpat/",
  329. array($cb, 'pl_expandvars'), $condspec);
  330. }
  331. if (!preg_match("/^\\s*(!?)\\s*(\\S*)\\s*(.*?)\\s*$/", $condspec, $match))
  332. return 0;
  333. list($x, $not, $condname, $condparm) = $match;
  334. if (!isset($Conditions[$condname])) return 1;
  335. $tf = (int)@eval("return ({$Conditions[$condname]});");
  336. return (boolean)($tf xor $not);
  337. }
  338. function PageListTermsTargets(&$list, &$opt, $pn, &$page) {
  339. global $FmtV;
  340. static $reindex = array();
  341. $fold = $GLOBALS['StrFoldFunction'];
  342. switch ($opt['=phase']) {
  343. case PAGELIST_PRE:
  344. $FmtV['$MatchSearched'] = count($list);
  345. $incl = array(); $excl = array();
  346. foreach((array)@$opt[''] as $i) { $incl[] = $fold($i); }
  347. foreach((array)@$opt['+'] as $i) { $incl[] = $fold($i); }
  348. foreach((array)@$opt['-'] as $i) { $excl[] = $fold($i); }
  349. $indexterms = PageIndexTerms($incl);
  350. foreach($incl as $i) {
  351. $delim = (!preg_match('/[^\\w\\x80-\\xff]/', $i)) ? '$' : '/';
  352. $opt['=inclp'][] = $delim . preg_quote($i,$delim) . $delim . 'i';
  353. }
  354. if ($excl)
  355. $opt['=exclp'][] = '$'.implode('|', array_map('preg_quote',$excl)).'$i';
  356. if (@$opt['link']) {
  357. $link = MakePageName($pn, $opt['link']);
  358. $opt['=linkp'] = "/(^|,)$link(,|$)/i";
  359. $indexterms[] = " $link ";
  360. }
  361. if (@$opt['=cached']) return 0;
  362. if ($indexterms) {
  363. StopWatch("PageListTermsTargets begin count=".count($list));
  364. $xlist = PageIndexGrep($indexterms, true);
  365. $list = array_diff($list, $xlist);
  366. StopWatch("PageListTermsTargets end count=".count($list));
  367. }
  368. if (@$opt['=inclp'] || @$opt['=exclp'] || @$opt['=linkp'])
  369. return PAGELIST_ITEM|PAGELIST_POST;
  370. return 0;
  371. case PAGELIST_ITEM:
  372. if (!$page) { $page = ReadPage($pn, READPAGE_CURRENT); $opt['=readc']++; }
  373. if (!$page) return 0;
  374. if (@$opt['=linkp'] && !preg_match($opt['=linkp'], @$page['targets']))
  375. { $reindex[] = $pn; return 0; }
  376. if (@$opt['=inclp'] || @$opt['=exclp']) {
  377. $text = $fold($pn."\n".@$page['targets']."\n".@$page['text']);
  378. foreach((array)@$opt['=exclp'] as $i)
  379. if (preg_match($i, $text)) return 0;
  380. foreach((array)@$opt['=inclp'] as $i)
  381. if (!preg_match($i, $text)) {
  382. if ($i[0] == '$') $reindex[] = $pn;
  383. return 0;
  384. }
  385. }
  386. return 1;
  387. case PAGELIST_POST:
  388. if ($reindex) PageIndexQueueUpdate($reindex);
  389. $reindex = array();
  390. return 0;
  391. }
  392. }
  393. function PageListVariables(&$list, &$opt, $pn, &$page) {
  394. global $PageListVarFoldFn, $StrFoldFunction;
  395. $fold = empty($PageListVarFoldFn)
  396. ? $StrFoldFunction : $PageListVarFoldFn;
  397. switch ($opt['=phase']) {
  398. case PAGELIST_PRE:
  399. $varlist = preg_grep('/^\\$/', array_keys($opt));
  400. if (!$varlist) return 0;
  401. foreach($varlist as $v) {
  402. list($inclp, $exclp) = GlobToPCRE($opt[$v]);
  403. if ($inclp) $opt['=varinclp'][$v] = $fold("/$inclp/i");
  404. if ($exclp) $opt['=varexclp'][$v] = $fold("/$exclp/i");
  405. }
  406. return PAGELIST_ITEM;
  407. case PAGELIST_ITEM:
  408. if (@$opt['=varinclp'])
  409. foreach($opt['=varinclp'] as $v => $pat)
  410. if (!preg_match($pat, $fold(PageVar($pn, $v)))) return 0;
  411. if (@$opt['=varexclp'])
  412. foreach($opt['=varexclp'] as $v => $pat)
  413. if (preg_match($pat, $fold(PageVar($pn, $v)))) return 0;
  414. return 1;
  415. }
  416. }
  417. function PageListSort(&$list, &$opt, $pn, &$page) {
  418. global $PageListSortCmp, $PCache, $PageListSortRead;
  419. SDVA($PageListSortRead, array('name' => 0, 'group' => 0, 'random' => 0,
  420. 'title' => 0));
  421. switch ($opt['=phase']) {
  422. case PAGELIST_PRE:
  423. $ret = 0;
  424. foreach(preg_split('/[^-\\w:$]+/', @$opt['order'], -1, PREG_SPLIT_NO_EMPTY)
  425. as $o) {
  426. $ret |= PAGELIST_POST;
  427. $r = '+';
  428. if ($o[0] == '-') { $r = '-'; $o = substr($o, 1); }
  429. $opt['=order'][$o] = $r;
  430. if ($o[0] != '$' &&
  431. (!isset($PageListSortRead[$o]) || $PageListSortRead[$o]))
  432. $ret |= PAGELIST_ITEM;
  433. }
  434. StopWatch(@"PageListSort pre ret=$ret order={$opt['order']}");
  435. return $ret;
  436. case PAGELIST_ITEM:
  437. if (!$page) { $page = ReadPage($pn, READPAGE_CURRENT); $opt['=readc']++; }
  438. return 1;
  439. }
  440. ## case PAGELIST_POST
  441. StopWatch('PageListSort begin');
  442. $order = $opt['=order'];
  443. if (@$order['title'])
  444. foreach($list as $pn) $PCache[$pn]['=title'] = PageVar($pn, '$Title');
  445. if (@$order['group'])
  446. foreach($list as $pn) $PCache[$pn]['group'] = PageVar($pn, '$Group');
  447. if (@$order['random'])
  448. { NoCache(); foreach($list as $pn) $PCache[$pn]['random'] = rand(); }
  449. foreach(preg_grep('/^\\$/', array_keys($order)) as $o)
  450. foreach($list as $pn)
  451. $PCache[$pn][$o] = PageVar($pn, $o);
  452. foreach($PageListSortCmp as $o=>$f)
  453. if(! is_callable($f)) # DEPRECATED
  454. $PageListSortCmp[$o] = create_function( # called by old addon needing update, see pmwiki.org/CustomPagelistSortOrder
  455. '$x,$y', "global \$PCache; return {$f};");
  456. StopWatch('PageListSort sort');
  457. if (count($opt['=order'])) {
  458. $PCache['=pagelistoptorder'] = $opt['=order'];
  459. uasort($list, 'PageListUASort');
  460. }
  461. StopWatch('PageListSort end');
  462. }
  463. function PageListUASort($x,$y) {
  464. global $PCache, $PageListSortCmp, $PageListSortCmpFunction;
  465. foreach($PCache['=pagelistoptorder'] as $o => $r) {
  466. $sign = ($r == '-') ? -1 : 1;
  467. if (@$PageListSortCmp[$o] && is_callable($PageListSortCmp[$o]))
  468. $c = $PageListSortCmp[$o]($x, $y, $o);
  469. else
  470. $c = @$PageListSortCmpFunction($PCache[$x][$o],$PCache[$y][$o]);
  471. if ($c) return $sign*$c;
  472. }
  473. return 0;
  474. }
  475. function PageListCache(&$list, &$opt, $pn, &$page) {
  476. global $PageListCacheDir, $LastModTime, $PageIndexFile;
  477. if (@!$PageListCacheDir) return 0;
  478. if (isset($opt['cache']) && !$opt['cache']) return 0;
  479. $key = $opt['=key'];
  480. $cache = "$PageListCacheDir/$key,cache";
  481. switch ($opt['=phase']) {
  482. case PAGELIST_PRE:
  483. if (!file_exists($cache) || filemtime($cache) <= $LastModTime)
  484. return PAGELIST_POST;
  485. StopWatch("PageListCache begin load key=$key");
  486. list($list, $opt['=protectsafe']) =
  487. unserialize(file_get_contents($cache));
  488. $opt['=cached'] = 1;
  489. StopWatch("PageListCache end load");
  490. return 0;
  491. case PAGELIST_POST:
  492. StopWatch("PageListCache begin save key=$key");
  493. $fp = @fopen($cache, "w");
  494. if ($fp) {
  495. fputs($fp, serialize(array($list, $opt['=protectsafe'])));
  496. fclose($fp);
  497. }
  498. StopWatch("PageListCache end save");
  499. return 0;
  500. }
  501. return 0;
  502. }
  503. ## HandleSearchA performs ?action=search. It's basically the same
  504. ## as ?action=browse, except it takes its contents from Site.Search.
  505. function HandleSearchA($pagename, $level = 'read') {
  506. global $PageSearchForm, $FmtV, $HandleSearchFmt,
  507. $PageStartFmt, $PageEndFmt;
  508. SDV($HandleSearchFmt,array(&$PageStartFmt, '$PageText', &$PageEndFmt));
  509. SDV($PageSearchForm, '$[{$SiteGroup}/Search]');
  510. $form = RetrieveAuthPage($pagename, $level, true, READPAGE_CURRENT);
  511. if (!$form) Abort("?unable to read $pagename");
  512. PCache($pagename, $form);
  513. $text = preg_replace('/\\[([=@])(.*?)\\1\\]/s', ' ', @$form['text']);
  514. if (!preg_match('/\\(:searchresults(\\s.*?)?:\\)/', $text))
  515. foreach((array)$PageSearchForm as $formfmt) {
  516. $form = ReadPage(FmtPageName($formfmt, $pagename), READPAGE_CURRENT);
  517. if ($form['text']) break;
  518. }
  519. $text = @$form['text'];
  520. if (!$text) $text = '(:searchresults:)';
  521. $FmtV['$PageText'] = MarkupToHTML($pagename,$text);
  522. PrintFmt($pagename, $HandleSearchFmt);
  523. }
  524. ########################################################################
  525. ## The functions below provide different formatting options for
  526. ## the output list, controlled by the fmt= parameter and the
  527. ## $FPLFormatOpt hash.
  528. ########################################################################
  529. ## This helper function handles the count= parameter for extracting
  530. ## a range of pagelist in the list.
  531. function CalcRange($range, $n) {
  532. if ($n < 1) return array(0, 0);
  533. if (strpos($range, '..') === false) {
  534. if ($range > 0) return array(1, min($range, $n));
  535. if ($range < 0) return array(max($n + $range + 1, 1), $n);
  536. return array(1, $n);
  537. }
  538. list($r0, $r1) = explode('..', $range);
  539. if ($r0 < 0) $r0 += $n + 1;
  540. if ($r1 < 0) $r1 += $n + 1;
  541. else if ($r1 == 0) $r1 = $n;
  542. if ($r0 < 1 && $r1 < 1) return array($n+1, $n+1);
  543. return array(max($r0, 1), max($r1, 1));
  544. }
  545. ## FPLCountA handles fmt=count
  546. function FPLCountA($pagename, &$matches, $opt) {
  547. $matches = array_values(MakePageList($pagename, $opt, 0));
  548. return count($matches);
  549. }
  550. SDVA($FPLTemplateFunctions, array(
  551. 'FPLTemplateLoad' => 100,
  552. 'FPLTemplateDefaults' => 200,
  553. 'FPLTemplatePageList' => 300,
  554. 'FPLTemplateSliceList' => 400,
  555. 'FPLTemplateFormat' => 500
  556. ));
  557. function FPLTemplate($pagename, &$matches, $opt) {
  558. global $FPLTemplateFunctions;
  559. StopWatch("FPLTemplate: Chain begin");
  560. asort($FPLTemplateFunctions, SORT_NUMERIC);
  561. $fnlist = $FPLTemplateFunctions;
  562. $output = '';
  563. foreach($FPLTemplateFunctions as $fn=>$i) {
  564. if ($i<0) continue;
  565. StopWatch("FPLTemplate: $fn");
  566. $fn($pagename, $matches, $opt, $tparts, $output);
  567. }
  568. StopWatch("FPLTemplate: Chain end");
  569. return $output;
  570. }
  571. ## Loads a template section
  572. function FPLTemplateLoad($pagename, $matches, $opt, &$tparts){
  573. global $Cursor, $FPLTemplatePageFmt, $RASPageName, $PageListArgPattern;
  574. SDV($FPLTemplatePageFmt, array('{$FullName}',
  575. '{$SiteGroup}.LocalTemplates', '{$SiteGroup}.PageListTemplates'));
  576. $template = @$opt['template'];
  577. if (!$template) $template = @$opt['fmt'];
  578. $ttext = RetrieveAuthSection($pagename, $template, $FPLTemplatePageFmt);
  579. $ttext = PVSE(Qualify($RASPageName, $ttext));
  580. ## save any escapes
  581. $ttext = MarkupEscape($ttext);
  582. ## remove any anchor markups to avoid duplications
  583. $ttext = preg_replace('/\\[\\[#[A-Za-z][-.:\\w]*\\]\\]/', '', $ttext);
  584. ## extract portions of template
  585. $tparts = preg_split('/\\(:(template)\\s+([-!]?)\\s*(\\w+)\\s*(.*?):\\)/i',
  586. $ttext, -1, PREG_SPLIT_DELIM_CAPTURE);
  587. }
  588. ## Merge parameters from (:template default :) with those in the (:pagelist:)
  589. function FPLTemplateDefaults($pagename, $matches, &$opt, &$tparts){
  590. global $PageListArgPattern;
  591. $i = 0;
  592. while ($i < count($tparts)) {
  593. if ($tparts[$i] != 'template') { $i++; continue; }
  594. if ($tparts[$i+2] != 'defaults' && $tparts[$i+2] != 'default') { $i+=5; continue; }
  595. $pvars = $GLOBALS['MarkupTable']['{$var}']; # expand {$PVars}
  596. $ttext = preg_replace_callback($pvars['pat'], $pvars['rep'], $tparts[$i+3]);
  597. $opt = array_merge(ParseArgs($ttext, $PageListArgPattern), $opt);
  598. array_splice($tparts, $i, 4);
  599. }
  600. SDVA($opt, array('class' => 'fpltemplate', 'wrap' => 'div'));
  601. }
  602. ## get the list of pages
  603. function FPLTemplatePageList($pagename, &$matches, &$opt){
  604. $matches = array_unique(array_merge((array)$matches, MakePageList($pagename, $opt, 0)));
  605. ## count matches before any slicing and save value as template var {$$PageListCount}
  606. $opt['PageListCount'] = count($matches);
  607. }
  608. ## extract page subset according to 'count=' parameter
  609. function FPLTemplateSliceList($pagename, &$matches, $opt){
  610. if (@$opt['count']) {
  611. list($r0, $r1) = CalcRange($opt['count'], count($matches));
  612. if ($r1 < $r0)
  613. $matches = array_reverse(array_slice($matches, $r1-1, $r0-$r1+1));
  614. else
  615. $matches = array_slice($matches, $r0-1, $r1-$r0+1);
  616. }
  617. }
  618. function FPLTemplateFormat($pagename, $matches, $opt, $tparts, &$output){
  619. global $Cursor, $FPLTemplateMarkupFunction, $PCache;
  620. SDV($FPLTemplateMarkupFunction, 'MarkupToHTML');
  621. $savecursor = $Cursor;
  622. $pagecount = $groupcount = $grouppagecount = $traildepth = $eachcount = 0;
  623. $pseudovars = array('{$$PageCount}' => &$pagecount,
  624. '{$$EachCount}' => &$eachcount,
  625. '{$$GroupCount}' => &$groupcount,
  626. '{$$GroupPageCount}' => &$grouppagecount,
  627. '{$$PageTrailDepth}' => &$traildepth);
  628. foreach(preg_grep('/^[\\w$]/', array_keys($opt)) as $k)
  629. if (!is_array($opt[$k]))
  630. $pseudovars["{\$\$$k}"] = PHSC($opt[$k], ENT_NOQUOTES);
  631. $vk = array_keys($pseudovars);
  632. $vv = array_values($pseudovars);
  633. $lgroup = $lcontrol = ''; $out = '';
  634. if (count($matches)==0) {
  635. $t = 0;
  636. while($t < count($tparts)) {
  637. if ($tparts[$t]=='template' && $tparts[$t+2]=='none') {
  638. $out .= MarkupRestore(FPLExpandItemVars($tparts[$t+4], $matches, 0, $pseudovars));
  639. $t+=4;
  640. }
  641. $t++;
  642. }
  643. } # else:
  644. foreach($matches as $i => $pn) {
  645. $traildepth = intval(@$PCache[$pn]['depth']);
  646. $group = PageVar($pn, '$Group');
  647. if ($group != $lgroup) { $groupcount++; $grouppagecount = 0; $lgroup = $group; }
  648. $grouppagecount++; $pagecount++; $eachcount++;
  649. $t = 0;
  650. while ($t < count($tparts)) {
  651. if ($tparts[$t] != 'template') { $item = $tparts[$t]; $t++; }
  652. else {
  653. list($neg, $when, $control, $item) = array_slice($tparts, $t+1, 4); $t+=5;
  654. if ($when=='none') continue;
  655. if (!$control) {
  656. if ($when == 'first' && ($neg xor ($i != 0))) continue;
  657. if ($when == 'last' && ($neg xor ($i != count($matches) - 1))) continue;
  658. } else {
  659. $currcontrol = FPLExpandItemVars($control, $matches, $i, $pseudovars);
  660. if($currcontrol != $lcontrol) { $eachcount=1; $lcontrol = $currcontrol; }
  661. if ($when == 'first' || !isset($last[$t])) {
  662. $curr = FPLExpandItemVars($control, $matches, $i, $pseudovars);
  663. if ($when == 'first' && ($neg xor (($i != 0) && ($last[$t] == $curr))))
  664. { $last[$t] = $curr; continue; }
  665. $last[$t] = $curr;
  666. }
  667. if ($when == 'last') {
  668. $next = FPLExpandItemVars($control, $matches, $i+1, $pseudovars);
  669. if ($neg xor ($next == $last[$t] && $i != count($matches) - 1)) continue;
  670. $last[$t] = $next;
  671. }
  672. }
  673. }
  674. $item = FPLExpandItemVars($item, $matches, $i, $pseudovars);
  675. $out .= MarkupRestore($item);
  676. }
  677. }
  678. $class = preg_replace('/[^-a-zA-Z0-9\\x80-\\xff]/', ' ', @$opt['class']);
  679. if ($class) $class = " class='$class'";
  680. $wrap = @$opt['wrap'];
  681. if ($wrap != 'inline') {
  682. $out = $FPLTemplateMarkupFunction($pagename, $out, array('escape' => 0, 'redirect'=>1));
  683. if ($wrap != 'none') $out = "<div$class>$out</div>";
  684. }
  685. $Cursor = $savecursor;
  686. $output .= $out;
  687. }
  688. ## This function moves repeated code blocks out of FPLTemplateFormat()
  689. function FPLExpandItemVars($item, $matches, $idx, $psvars) {
  690. global $Cursor, $EnableUndefinedTemplateVars;
  691. $Cursor['<'] = $Cursor['&lt;'] = (string)@$matches[$idx-1];
  692. $Cursor['='] = $pn = (string)@$matches[$idx];
  693. $Cursor['>'] = $Cursor['&gt;'] = (string)@$matches[$idx+1];
  694. $item = str_replace(array_keys($psvars), array_values($psvars), $item);
  695. $cb = new cb_pl_expandvars($pn);
  696. $item = preg_replace_callback('/\\{(=|&[lg]t;)(\\$:?\\w[-\\w]*)\\}/',
  697. array($cb, 'pl_expandvars'), $item);
  698. if (! IsEnabled($EnableUndefinedTemplateVars, 0))
  699. $item = preg_replace("/\\{\\$\\$\\w+\\}/", '', $item);
  700. return $item;
  701. }
  702. ########################################################################
  703. ## The functions below optimize searches by maintaining a file of
  704. ## words and link cross references (the "page index").
  705. ########################################################################
  706. ## PageIndexTerms($terms) takes an array of strings and returns a
  707. ## normalized list of associated search terms. This reduces the
  708. ## size of the index and speeds up searches.
  709. function PageIndexTerms($terms) {
  710. global $StrFoldFunction;
  711. $w = array();
  712. foreach((array)$terms as $t) {
  713. $w = array_merge($w, preg_split('/[^\\w\\x80-\\xff]+/',
  714. $StrFoldFunction($t), -1, PREG_SPLIT_NO_EMPTY));
  715. }
  716. return $w;
  717. }
  718. ## The PageIndexUpdate($pagelist) function updates the page index
  719. ## file with terms and target links for the pages in $pagelist.
  720. ## The optional $dir parameter allows this function to be called
  721. ## via register_shutdown_function (which sometimes changes directories
  722. ## on us).
  723. function PageIndexUpdate($pagelist = NULL, $dir = '') {
  724. global $EnableReadOnly, $PageIndexUpdateList, $PageIndexFile,
  725. $PageIndexTime, $Now;
  726. if (IsEnabled($EnableReadOnly, 0)) return;
  727. $abort = ignore_user_abort(true);
  728. if ($dir) { flush(); chdir($dir); }
  729. if (is_null($pagelist))
  730. { $pagelist = (array)$PageIndexUpdateList; $PageIndexUpdateList = array(); }
  731. if (!$pagelist || !$PageIndexFile) return;
  732. SDV($PageIndexTime, 10);
  733. $c = count($pagelist); $updatecount = 0;
  734. StopWatch("PageIndexUpdate begin ($c pages to update)");
  735. $pagelist = (array)$pagelist;
  736. $timeout = time() + $PageIndexTime;
  737. $cmpfn = 'PageIndexUpdateSort';
  738. Lock(2);
  739. $ofp = fopen("$PageIndexFile,new", 'w');
  740. foreach($pagelist as $pn) {
  741. if (@$updated[$pn]) continue;
  742. @$updated[$pn]++;
  743. if (time() > $timeout) continue;
  744. $page = ReadPage($pn, READPAGE_CURRENT);
  745. if ($page) {
  746. $targets = str_replace(',', ' ', @$page['targets']);
  747. $terms = PageIndexTerms(array(@$page['text'], $targets, $pn));
  748. usort($terms, $cmpfn);
  749. $x = '';
  750. foreach($terms as $t) { if (strpos($x, $t) === false) $x .= " $t"; }
  751. fputs($ofp, "$pn:$Now: $targets :$x\n");
  752. }
  753. $updatecount++;
  754. }
  755. $ifp = @fopen($PageIndexFile, 'r');
  756. if ($ifp) {
  757. while (!feof($ifp)) {
  758. $line = fgets($ifp, 4096);
  759. while (substr($line, -1, 1) != "\n" && !feof($ifp))
  760. $line .= fgets($ifp, 4096);
  761. $i = strpos($line, ':');
  762. if ($i === false) continue;
  763. $n = substr($line, 0, $i);
  764. if (@$updated[$n]) continue;
  765. fputs($ofp, $line);
  766. }
  767. fclose($ifp);
  768. }
  769. fclose($ofp);
  770. if (file_exists($PageIndexFile)) unlink($PageIndexFile);
  771. rename("$PageIndexFile,new", $PageIndexFile);
  772. fixperms($PageIndexFile);
  773. StopWatch("PageIndexUpdate end ($updatecount updated)");
  774. ignore_user_abort($abort);
  775. }
  776. function PageIndexUpdateSort($a,$b) {return strlen($b)-strlen($a);}
  777. ## PageIndexQueueUpdate specifies pages to be updated in
  778. ## the index upon shutdown (via register_shutdown function).
  779. function PageIndexQueueUpdate($pagelist) {
  780. global $PageIndexUpdateList;
  781. if (!@$PageIndexUpdateList)
  782. register_shutdown_function('PageIndexUpdate', NULL, getcwd());
  783. $PageIndexUpdateList = array_merge((array)@$PageIndexUpdateList,
  784. (array)$pagelist);
  785. $c1 = @count($pagelist); $c2 = count($PageIndexUpdateList);
  786. StopWatch("PageIndexQueueUpdate: queued $c1 pages ($c2 total)");
  787. }
  788. ## PageIndexGrep returns a list of pages that match the strings
  789. ## provided. Note that some search terms may need to be normalized
  790. ## in order to get the desired results (see PageIndexTerms above).
  791. ## Also note that this just works for the index; if the index is
  792. ## incomplete, then so are the results returned by this list.
  793. ## (MakePageList above already knows how to deal with this.)
  794. function PageIndexGrep($terms, $invert = false) {
  795. global $PageIndexFile;
  796. if (!$PageIndexFile) return array();
  797. StopWatch('PageIndexGrep begin');
  798. $pagelist = array();
  799. $fp = @fopen($PageIndexFile, 'r');
  800. if ($fp) {
  801. $terms = (array)$terms;
  802. while (!feof($fp)) {
  803. $line = fgets($fp, 4096);
  804. while (substr($line, -1, 1) != "\n" && !feof($fp))
  805. $line .= fgets($fp, 4096);
  806. $i = strpos($line, ':');
  807. if (!$i) continue;
  808. $add = true;
  809. foreach($terms as $t)
  810. if (strpos($line, $t) === false) { $add = false; break; }
  811. if ($add xor $invert) $pagelist[] = substr($line, 0, $i);
  812. }
  813. fclose($fp);
  814. }
  815. StopWatch('PageIndexGrep end');
  816. return $pagelist;
  817. }
  818. ## PostPageIndex is inserted into $EditFunctions to update
  819. ## the linkindex whenever a page is saved.
  820. function PostPageIndex($pagename, &$page, &$new) {
  821. global $IsPagePosted;
  822. if ($IsPagePosted) PageIndexQueueUpdate($pagename);
  823. }