1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8require_once 'lib/wiki/pluginslib.php';
9
10function wikiplugin_wantedpages_info()
11{
12	return [
13		'name' => tra('Wanted Pages'),
14		'documentation' => 'PluginWantedPages',
15		'description' => tra('Show location of links to pages not yet created'),
16		'prefs' => [ 'wikiplugin_wantedpages' ],
17		'body' => tr('Custom level regex. A custom filter for wanted pages to be listed (only used when %0). Possible
18			values: a valid regex-expression (PCRE).', '<code>level="custom"</code>'),
19		'iconname' => 'search',
20		'introduced' => 1,
21		'tags' => [ 'basic' ],
22		'params' => [
23			'ignore' => [
24				'required' => false,
25				'name' => tra('Ignore'),
26				'description' => tra('A wildcard pattern of originating pages to be ignored. (refer to PHP function
27					fnmatch() for details)'),
28				'since' => '1',
29				'accepted' => tra('a valid regex-expression (PCRE)'),
30				'default' => '',
31				'advanced' => true,
32			],
33			'splitby' => [
34				'required' => false,
35				'name' => tra('Split By'),
36				'description' => tra('The character by which ignored patterns are separated.'),
37				'since' => '1',
38				'default' => '+',
39				'advanced' => true,
40			],
41			'skipalias' => [
42				'required' => false,
43				'name' => tra('Skip Alias'),
44				'description' => tra('Whether to skip wanted pages that have a defined alias (not skipped by default)'),
45				'since' => '12.1',
46				'default' => 0,
47				'filter' => 'digits',
48				'options' => [
49					['text' => '', 'value' => ''],
50					['text' => tra('Yes'), 'value' => 1],
51					['text' => tra('No'), 'value' => 0],
52				],
53			],
54			'skipext' => [
55				'required' => false,
56				'name' => tra('Skip Extension'),
57				'description' => tra('Whether to include external wikis in the list (not included by default)'),
58				'since' => '1',
59				'default' => 0,
60				'filter' => 'digits',
61				'options' => [
62					['text' => '', 'value' => ''],
63					['text' => tra('Yes'), 'value' => 1],
64					['text' => tra('No'), 'value' => 0],
65				],
66			],
67			'collect' => [
68				'required' => false,
69				'name' => tra('Collect'),
70				'description' => tra('Collect either originating (from) or wanted pages (to) in a cell and display them
71					in the second column.'),
72				'since' => '1',
73				'default' => 'from',
74				'filter' => 'alpha',
75				'options' => [
76					['text' => '', 'value' => ''],
77					['text' => tra('From'), 'value' => 'from'],
78					['text' => tra('To'), 'value' => 'to'],
79				],
80			],
81			'debug' => [
82				'required' => false,
83				'name' => tra('Debug'),
84				'description' => tra('Switch on debug output with details about the items (debug not on by default)'),
85				'since' => '1',
86				'default' => 0,
87				'filter' => 'digits',
88				'advanced' => true,
89				'options' => [
90					['text' => '', 'value' => ''],
91					['text' => tra('No'), 'value' => 0],
92					['text' => tra('Yes'), 'value' => 1],
93					['text' => tra('Memory Saver'), 'value' => 2],
94					],
95			],
96			'table' => [
97				'required' => false,
98				'name' => tra('Table'),
99				'description' => tra('Multiple collected items are separated in distinct table rows (default), or by
100					comma or line break in one cell.'),
101				'since' => '1',
102				'filter' => 'alpha',
103				'default' => 'sep',
104				'accepted' => 'sep, co, br',
105				'options' => [
106					['text' => '', 'value' => ''],
107					['text' => tra('Comma'), 'value' => 'co'],
108					['text' => tra('Line break'), 'value' => 'br'],
109					['text' => tra('Separate Row'), 'value' => 'sep'],
110				],
111			],
112			'level' => [
113				'required' => false,
114				'name' => tra('Level'),
115				'description' => tra('Filter the list of wanted pages according to page_regex or custom filter. The
116					default value is the site\'s __current__ page_regex.'),
117				'since' => '1',
118				'default' => '',
119				'filter' => 'alpha',
120				'advanced' => true,
121				'options' => [
122					['text' => '', 'value' => ''],
123					['text' => tra('Custom'), 'value' => 'custom'],
124					['text' => tra('Full'), 'value' => 'full'],
125					['text' => tra('Strict'), 'value' => 'strict'],
126				],
127			],
128		],
129	];
130}
131
132class WikiPluginWantedPages extends PluginsLib
133{
134
135	function getDefaultArguments()
136	{
137		return [	'ignore' => '', // originating pages to be ignored
138						'splitby' => '+', // split ignored pages by this character
139						'skipalias' => 0, // false, count a page alias as a wanted page
140						'skipext' => 0, // false, display external wiki links
141						'collect' => 'from', // display (and sort) wanted pages in the first column,
142						// collect originating pages in the second column (and separate them by table parameter)
143						'table' => 'sep', // show each line of output in a separate table row
144						'level' => '', // use current page_regex to filter output
145						'debug' => 0]; // false, no debug output; a value of 2
146							// tries to allocate as little memory as possible.
147	}
148
149	function getName()
150	{
151		return 'WantedPages';
152	}
153
154	function getDescription()
155	{
156		return wikiplugin_wantedpages_help();
157	}
158
159	function getVersion()
160	{
161		return preg_replace("/[Revision: $]/", '', "\$Revision: 1.7 $");
162	}
163
164	function run($data, $params)
165	{
166		global $prefs, $page_regex;
167
168		// Grab and handle our Tiki parameters...
169		extract($params, EXTR_SKIP);
170		if (! isset($ignore)) {
171			$ignore = '';
172		}
173		if (! isset($splitby)) {
174			$splitby = '+';
175		}
176		if (! isset($skipalias)) {
177			$skipalias = false;
178		}
179		if (! isset($skipext)) {
180			$skipext = false;
181		}
182		if (! isset($debug)) {
183			$debug = false;
184		}
185		if (! isset($collect)) {
186			$collect = 'from';
187		}
188		if (! isset($table)) {
189			$table = 'sep';
190		}
191		if (! isset($level)) {
192			$level = '';
193		}
194
195		// for regexes and external wiki details, see tikilib.php
196		if ($level == 'strict') {
197			$level_reg = '([A-Za-z0-9_])([\.: A-Za-z0-9_\-])*([A-Za-z0-9_])';
198		} elseif ($level == 'full') {
199			$level_reg = '([A-Za-z0-9_]|[\x80-\xFF])([\.: A-Za-z0-9_\-]|[\x80-\xFF])*([A-Za-z0-9_]|[\x80-\xFF])';
200		} elseif ($level == 'complete') {
201			$level_reg = '([^|\(\)])([^|\(\)](?!\)\)))*?([^|\(\)])';
202		} elseif (($level == 'custom') && ($data != '')) {
203			if (preg_ispreg($data)) { // custom regular expression
204				$level_reg = $data;
205			} elseif ($debug == 2) {
206				echo $data . ': ' . tra('non-valid custom regex') . '<br />';
207			}
208		} else { // default
209			$level_reg = $page_regex;
210		}
211
212		// define the separator
213		if ($table == 'br') {
214			$break = '<br />';
215		} elseif ($table == 'co') {
216			$break = tra(', ');
217		} else {
218			$break = 'sep';
219		}
220
221		// get array of fromPages to be ignored
222		$ignorepages = explode($splitby, $ignore);
223
224		// Currently we only look in wiki pages.
225		// Wiki links in articles, blogs, etc are ignored.
226		$query = 'select distinct tl.`toPage`, tl.`fromPage` from `tiki_links` tl';
227		$query .= ' left join `tiki_pages` tp on (tl.`toPage` = tp.`pageName`)';
228		if ($skipalias) {
229			$query .= ' left join `tiki_object_relations` tor on (tl.`toPage` = tor.`target_itemId`)';
230		}
231
232		$categories = $this->get_jail();
233		if ($categories) {
234			$query .= ' inner join `tiki_objects` as tob on (tob.`itemId`= tl.`fromPage` and tob.`type`= ?) inner join `tiki_category_objects` as tc on (tc.`catObjectId`=tob.`objectId` and tc.`categId` IN(' . implode(', ', array_fill(0, count($categories), '?')) . '))';
235		}
236		$query .= ' where tp.`pageName` is null';
237		if ($skipalias) {
238			$query .= ' and (tor.`relation` is null or tor.`relation` != \'tiki.link.alias\')';
239		}
240		$result = $this->query($query, $categories ? array_merge(['wiki page'], $categories) : []);
241		$tmp = [];
242
243		while ($row = $result->fetchRow()) {
244			foreach ($ignorepages as $ipage) {
245				// test whether a substring ignores this page, ignore case
246				if (fnmatch(TikiLib::strtolower($ipage), TikiLib::strtolower($row['fromPage'])) === true) {
247					if ($debug == 2) { // the "hardcore case"
248						echo $row['toPage'] . ' [from: ' . $row['fromPage'] . ']: ' . tra('ignored') . '<br />';
249					} elseif ($debug) { // add this page to the table
250						$tmp[] = [$row['toPage'], $row['fromPage'], 'ignored'];
251					}
252					continue 2; // no need to test other ignorepages or toPages
253				}
254			} // foreach ignorepage
255
256			// if toPage contains colon, and exloding yields two parts => external Wiki
257			if (($skipext) && (strstr($row['toPage'], ':') !== false)) {
258				$parts = explode(':', $row['toPage']);
259				if (count($parts) == 2) {
260					if ($debug == 2) {
261						echo $row['toPage'] . ' [from: ' . $row['fromPage'] . ']: ' . tra('External Wiki') . '<br />';
262					} elseif ($debug) {
263						$tmp[] = [$row['toPage'], $row['fromPage'], 'External Wiki'];
264					}
265					continue;
266				}
267			} // $skipext
268
269			$dashWikiWord = preg_match("/^(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9_\-\x80-\xFF]+[A-Z][a-z0-9_\-\x80-\xFF]+[A-Za-z0-9\-_\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])$/", $row['toPage']);
270			$WikiWord = preg_match("/^(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9\x80-\xFF]+[A-Z][a-z0-9\x80-\xFF]+[A-Za-z0-9\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])$/", $row['toPage']);
271			// test whether toPage is a valid wiki page under current syntax
272			if ($dashWikiWord && ! $WikiWord) { // a Dashed-WikiWord, can we allow this?
273				if (($prefs['feature_wikiwords'] != 'y') || ($prefs['feature_wikiwords_usedash'] != 'y')) {
274					$tmp = debug_print($row, $debug, tra('dash-WikiWord'));
275					continue;
276				}
277			} elseif ($WikiWord) { // a WikiWord, can we allow this?
278				if ($prefs['feature_wikiwords'] != 'y') {
279					$tmp = debug_print($row, $debug, tra('WikiWord'));
280					continue;
281				}
282			} else { // no WikiWord, we can now filter with the level parameter
283				if (! preg_match("/^($level_reg)$/", $row['toPage'])) {
284					$tmp = debug_print($row, $debug, tra('not in level'));
285					continue;
286				}
287			} // dashWikiWord, WikiWord, normal link
288
289			if (! $debug) { // a normal, valid WantedPage:
290				$tmp[] = [$row['toPage'], $row['fromPage']];
291			} elseif ($debug == 2) {
292				debug_print($row, $debug, tra('valid'));
293			} // in simple debug mode, valid links are ignored
294		} // while (each entry in tiki_links is handled)
295		unset($result); // free memory
296
297		if ($debug == 2) {
298			return(tra('End of debug output.'));
299		}
300
301		$out = [];
302		$linkin  = (! $debug) ? '((' : '~np~'; // this is how toPages are handled
303		$linkout = (! $debug) ? '))' : '~/np~';
304		if (is_array($tmp)) {
305			foreach ($tmp as $row) { // row[toPage, fromPage, reason]
306				if ($debug) { // modified rejected toPages with reason
307					$row[0] = '<em>' . tra($row[2]) . '</em>: ' . $row[0];
308				}
309				$row[0] = $linkin . $row[0] . $linkout; // toPages
310				$row[1] = '((' . $row[1] . '))'; // fromPages
311
312				// two identical keys may exist, they can either be displayed
313				// each in its own table row, or be collected in one cell, separated by
314				// either comma or <br />
315				if ($collect == 'from') {
316					if ($break == 'sep') {
317						// toPages separated in each row, there might be duplicates!!!
318						$out[] = [$row[0], $row[1]];
319					} elseif (! array_key_exists($row[0], $out)) {
320						// multiple fromPages (for one toPage) might be in one row, this is the first
321						$out[$row[0]] = $row[1];
322					} else {
323						// multiple fromPages might be in one row, this is a follow-up
324						$out[$row[0]] = $out[$row[0]] . $break . $row[1];
325					}
326				} else { // $collect == to
327					if ($break == 'sep') {
328						// fromPages separated in each row, there might be duplicates!!!
329						$out[] = [$row[1], $row[0]];
330					} elseif (! array_key_exists($row[1], $out)) {
331						// multiple toPages (for one fromPage) might be in one row, this is the first
332						$out[$row[1]] = $row[0];
333					} else { // multiple toPages might be in one row, this is a follow-up
334						$out[$row[1]] = $out[$row[1]] . $break . $row[0];
335					}
336				}
337			} // foreach (received row) is handled
338			unset($tmp); // free memory
339		}
340
341		// sort the entries
342		if ($break == 'sep') {
343			sort($out);
344		} else {
345			ksort($out);
346		}
347
348		$headerwant = tra('Wanted Page');
349		$headerref = tra('Referenced By Page');
350		$rowbreak = "\n";
351		$endtable = '||';
352		if ($prefs['feature_wiki_tables'] != 'new') {
353			$rowbreak = ' || ';
354			$endtable = '';
355		}
356
357		$sOutput = '||' . '__';
358		if ($collect == 'from') {
359			$sOutput .= $headerwant . '__|__' . $headerref . '__' . $rowbreak;
360			if ($break == 'sep') {
361				foreach ($out as $link) {
362					$sOutput .= $link[0] . ' | ' . $link[1] . $rowbreak;
363				}
364			} else {
365				foreach ($out as $to => $from) {
366					$sOutput .= $to . ' | ' . $from . $rowbreak;
367				}
368			}
369		} else { // $collect == 'to'
370			$sOutput .= $headerref . '__|__' . $headerwant . '__' . $rowbreak;
371			if ($break == 'sep') {
372				foreach ($out as $link) {
373					$sOutput .= $link[0] . ' | ' . $link[1] . $rowbreak;
374				}
375			} else {
376				foreach ($out as $from => $to) {
377					$sOutput .= $from . ' | ' . $to . $rowbreak;
378				}
379			}
380		}
381		$sOutput .= $endtable;
382		return $sOutput;
383	} // run()
384} // class WikiPluginWantedPages
385
386function wikiplugin_wantedpages($data, $params)
387{
388	$plugin = new WikiPluginWantedPages();
389	return $plugin->run($data, $params);
390}
391
392// fnmatch() is not defined on windows or PHP < 4.3.0!!
393// From php help "fnmatch", http://www.php.net/manual/de/function.fnmatch.php
394// comment by "soywiz at gmail dot com 26-Jul-2005 07:07" (as of Jan. 21 2006)
395if (! function_exists('fnmatch')) {
396	function fnmatch($pattern, $string)
397	{
398		for ($op = 0, $npattern = '', $n = 0, $l = strlen($pattern); $n < $l; $n++) {
399			switch ($c = $pattern[$n]) {
400				case '\\':
401					$npattern .= '\\' . @$pattern[++$n];
402					break;
403				case '.':
404				case '+':
405				case '^':
406				case '$':
407				case '(':
408				case ')':
409				case '{':
410				case '}':
411				case '=':
412				case '!':
413				case '<':
414				case '>':
415				case '|':
416																$npattern .= '\\' . $c;
417					break;
418				case '?':
419				case '*':
420					$npattern .= '.' . $c;
421					break;
422				case '[':
423				case ']':
424				default:
425						$npattern .= $c;
426					if ($c == '[') {
427						$op++;
428					} elseif ($c == ']') {
429						if ($op == 0) {
430							return false;
431						}
432						$op--;
433					}
434					break;
435			}
436		}
437		if ($op != 0) {
438			return false;
439		}
440		return preg_match('/' . $npattern . '/i', $string);
441	} // function fnmatch
442} // !exists(fnmatch)
443
444// A small function to determine whether a string is a [valid] preg expression.
445// From php help "Regular Expression Functions (Perl-Compatible)", http://www.php.net/pcre/
446// comment by "alexbodn at 012 dot n@t dot il 09-Jan-2006 11:45" (as of Jan. 21 2006)
447if (! function_exists('preg_ispreg')) {
448	function preg_ispreg($str)
449	{
450		$prefix = '';
451		$sufix = '';
452		if ($str[0] != '^') {
453			$prefix = '^';
454		}
455		if ($str[strlen($str) - 1] != '$') {
456			$sufix = '$';
457		}
458		$estr = preg_replace("'^/'", "\\/", preg_replace("'([^/])/'", "\\1\\/", $str));
459		if (@preg_match("/" . $prefix . $estr . $sufix . "/", $str, $matches)) {
460			return strcmp($str, $matches[0]) != 0;
461		}
462		return true;
463	} // function preg_ispreg
464} //!exists(preg_ispreg)
465
466if (! function_exists('debug_print')) {
467	function debug_print($row, $debug, $message)
468	{
469		if ($debug == 2) {
470			echo $row['toPage'] . ' [from: ' . $row['fromPage'] . ']: ' . $message . '<br />';
471			return;
472		} elseif ($debug) {
473			$tmp[] = [$row['toPage'], $row['fromPage'], $message];
474			return $tmp;
475		}
476	}
477}
478