1<?php
2
3/*****************************************************************
4 * SearchPattern plugin for dokuwiki / Syntax plugin
5 *
6 * Syntax :
7 *   ~~SEARCHPATTERN(:|;|#)\'[pattern or regexp to search]\'~~
8 *   ~~SEARCHPATTERN:\'myString\'~~ -> search "myString" - case sensitive
9 *   ~~SEARCHPATTERN;\'myString\'~~ -> search "myString" - case insensitive
10 *   ~~SEARCHPATTERN#\'myRegex\'~~ -> search myRegex
11 *   ~~SEARCHPATTERN#'/======([^=]+)======/'~~ -> search all headlines and output the headlines
12 *
13 * In combination with dokuwiki todo plugin version (at least v20130408),
14 * it is a lightweight solution for a task management system based on dokuwiki.
15 * use this searchpattern expression for open todos:
16 *     ~~SEARCHPATTERN#'/<todo[^#>]*>.*?<\/todo[\W]*?>/'?? _ToDo ??~~
17 * use this searchpattern expression for completed todos:
18 *     ~~SEARCHPATTERN#'/<todo[^#>]*#[^>]*>.*?<\/todo[\W]*?>/'?? _ToDo ??~~
19 * do not forget the no-cache option
20 *     ~~NOCACHE~~
21 *
22 * Options are passed by adding them between two times '??' after last single quote and before double tilde.
23 * Regex syntax is strictly the same as the one used with PHP "preg_match" function.
24 * !!! In all cases, every single quote inside the string/regex shall be doubled. !!!
25 *
26 * Valid Options (between the double ?? questionmarks):
27 *     -  restricting option (page or namespace exclude)
28 *     +  limit option (only page or namespace included)
29 *     $  output all regex matches
30 *     $<Matches>  output given comma separated regex matches e.g. $3,1 will output match 3 and 1
31 *     _  call a other dokuwiki syntax plugin to format the matching/output
32 *
33 * Example:
34 *     ~~SEARCHPATTERN#'/<todo[^#>]*#[^>]*>.*?<\/todo[\W]*?>/'?? _ToDo ??~~
35 *         this will search all <todo #>Some Todo</todo> tags and call the todo plugin (option _ToDo) for outputting
36 *     ~~SEARCHPATTERN#'/([\W]+)[\W]+([\W]+)[\W]+([\W]+)[\W]+/'?? $1,3 ??~~
37 *         this will only output the 1st and the 3rd regex match
38 *     ~~SEARCHPATTERN#'/<todo[^@>]*@([^\W]+)[^#>]*(#)?[^>]*>(.*?)<\/todo[\W]*?>/'?? $2,1,3 ??~~
39 *         the matches will displayed in the following order: 2nd match, 1st match, 3rd match
40 *         this will output all assigned todos: the # Flag (todo completed), the username and the todo text
41 *         as example input: <todo @leo #>Finished task for leo</todo> <todo>Some task</todo> <todo @leo>Uncompleted task for leo</todo>
42 *                   output:   | # | leo | Finished task for leo    |
43 *                             |   | leo | Uncompleted task for leo |
44 *     ~~SEARCHPATTERN#'/FIXME[ \t]+([^ \t\n\r]+)([ \t]*)?([^ \t\n\r]+)?/i'?? $3,1 ??~~
45 *         will display one or two words after the word FIXME in reverse direction
46 *         as example input:  FIXME firstWord secondWord
47 *                   output:  | secondWord | firstWord |
48 *
49 *
50 * Compatibility:
51 *     Release 2013-03-06 "Weatherwax RC1"
52 *     Release 2012-10-13 "Adora Belle"
53 *
54 * @author     Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com>; Leo Eibler <dokuwiki@sprossenwanne.at>
55 *
56 */
57
58/**
59 * ChangeLog:
60 *
61 * [06/16/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
62 *               bugfix: implement suggestions from Matthieu and use correct 'badopt' configuration from admin panel
63 *               implement new admin option 'dispheadl' to show or hide regex code in result table
64 * [06/16/2013]: by Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com> / http://wiki.splitbrain.org/plugin:searchpattern
65 *               bugfix: incorrect XHTML or PHP warnings in log
66 *               include a better default.php configuration file
67 * [04/15/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
68 *               translation of language file to german
69 * [04/15/2013]: by Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com> / http://wiki.splitbrain.org/plugin:searchpattern
70 *               bugfix: incorrect XHTML or PHP warnings in log
71 *               bugfix: quickaclcheck only handles pages (not namespaces)
72 * [04/12/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
73 *               bugfix: incorrect if statement using $ syntax
74 *               example using FIXME and $3,1
75 *               bugfix: use parameter call_plugin_handler and fallback to lowercase if plugin not found
76 * [04/11/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
77 *               change description / comments and syntax howto about integration with dokuwiki plugin 'todo'
78 *               bugfix: encoding html code (security risk <script>alert('hi')</script>) when using regex and match output $ (dollar) option.
79 *               bugfix: default behavior (output only count) if no $ (dollar) is used
80 * [04/08/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
81 *               add description / comments and syntax howto about integration with dokuwiki plugin 'todo'
82 *               check compatibility with dokuwiki release 2012-10-13 "Adora Belle"  and 2013-03-06 "Weatherwax RC1"
83 *               reformat inline documentation of syntax.php file
84 *               remove getInfo() call because it's done by plugin.info.txt (since dokuwiki 2009-12-25 "Lemming")
85 * [04/07/2013]: by Leo Eibler <dokuwiki@sprossenwanne.at> / http://www.eibler.at
86 *               add regex match output with new option $ (dollar)
87 *               add callback handler method to call other plugin for formatting the output with new option _ (underscore)
88 * [07/05/2010]: by Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com> / http://wiki.splitbrain.org/plugin:searchpattern
89 *               Add capability to exclude pages/namespaces from search, or to restrict it to certain pages/namespaces
90 * [01/05/2010]: by Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com> / http://wiki.splitbrain.org/plugin:searchpattern
91 *               Initial release
92 * [12/10/2009]: by Matthieu Rioteau <matthieu<dot>rioteau<at>skf<dot>com> / http://wiki.splitbrain.org/plugin:searchpattern
93 *               Creation
94 */
95
96if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../../').'/');
97if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
98
99require_once(DOKU_PLUGIN.'syntax.php');
100require_once(DOKU_INC.'inc/search.php');	//needed for use of the "search" function to list pages
101require_once(DOKU_INC.'inc/common.php');	//needed for use of "wl' function
102
103/**
104* Convert a string to a regex so it can be used in PHP "preg_match" function
105*/
106function str2regex($str){
107	$regex = '';	//init
108	for($i = 0; $i < strlen($str); $i++){	//for each char in the string
109		if(!ctype_alnum($str[$i])){	//if char is not alpha-numeric
110			$regex = $regex.'\\';	//escape it with a backslash
111		}
112		$regex = $regex.$str[$i];	//compose regex
113	}
114	return $regex;	//return
115}
116
117/**
118* Callback function for "search" function
119* Look for matches in wiki files and store them
120*/
121function search_pattern(&$data, $base, $file, $type, $lvl, $opts) {
122	global $INFO;
123	$id = pathID($file);	//get current file ID
124	if($type == 'd'){	//if type is directory
125		return true; //recurse but don't process
126	}
127	if(auth_quickaclcheck($id) < AUTH_READ) {	//if user don't have enough rights
128		return false;	//don't processa and don't recurse
129	}
130	if(!preg_match("/.*\.txt$/", $file)) {  //if file is not a true wiki file (.txt)
131		return true;	//don't process
132	}
133	if(isset($opts['page_limit']) || isset($opts['nmsp_limit'])){	//if limiting options have been used
134		$process = false;	//page is not processed by default : will be true if page shall be processed
135		if(isset($opts['page_limit']))
136		{
137			foreach($opts['page_limit'] as $page){	//going through limiting page list
138				if($id == $page){	//if the analyzed page is in the list
139					$process = true;	//process it
140					break;
141				}
142			}
143		}
144		if(!$process){	//if analyzed page wasn't in the page list
145			if(isset($opts['nmsp_limit']))
146			{
147				foreach($opts['nmsp_limit'] as $nmsp){	//going through the limiting namespace list
148					if(substr($id,0,strlen($nmsp)) == $nmsp){	//if analyzed page is in the list
149						$process = true;	// process it
150						break;
151					}
152				}
153			}
154		}
155		if(!$process){	//if page shall not be processed
156			return true;	//recurse but don't process
157		}
158	}
159	if(isset($opts['page_exclude']) || isset($opts['nmsp_exclude'])){	//if restricting option have been used
160		if(isset($opts['page_exclude'])) {
161			foreach($opts['page_exclude'] as $page){	//going through the restricting page list
162				if($id == $page){	//if analyzed page is in the exclusion list
163					return true;	//recurse but don't process
164				}
165			}
166		}
167		if(isset($opts['nmsp_exclude'])) {
168			foreach($opts['nmsp_exclude'] as $nmsp){	//going through the restricting namespace list
169			  if(substr($id,0,strlen($nmsp)) == $nmsp){	//if analyzed page is in the exclusion list
170				return true;	//recurse but don't process
171			  }
172			}
173		}
174	}
175	$filebody = file_get_contents($base.$file);	//get file content
176	if($INFO['id'] == $id){	//if we are processing the page in which plugin has been called
177		$filebody = str_replace($opts['match'], '', $filebody, $count);	//remove the call to avoid counting it
178		$count--;	//hold number of extra pattern occurences that should have been unvolunteerly removed
179	}
180	else{
181		$count = 0;	//for correct calculation
182	}
183	$nboccur = preg_match_all($opts['pattern'], $filebody, $matches) + $count;	//count how many times appears the pattern
184	if($nboccur != 0){	//if it appears at least once
185		$data[$id] = $nboccur;	//store it in data
186		if( count($matches) > 0 ) {
187			// if at least 1 match () is found
188			$data['{'.$id.'}'] = $matches;
189		}
190	}
191	return true;	//return and continue processing
192}
193
194/**
195* All DokuWiki plugins to extend the parser/rendering mechanism
196* Need to inherit from this class
197*/
198class syntax_plugin_searchpattern extends DokuWiki_Syntax_Plugin {
199	/**
200	* Return some info
201	*/
202	/*
203	function getInfo(){
204		// replaced by plugin.info.txt file
205		return array(
206				'author' => 'Matthieu Rioteau',
207				'email'  => 'matthieu<dot>rioteau<at>skf<dot>com',
208				'date'   => '2010-07-05',
209				'name'   => 'SearchPattern plugin ver. 0.2',
210				'desc'   => 'Find a specified pattern inside wiki pages.
211				syntax : ~~SEARCHPATTERN(:|;|#)\'[pattern or regexp to search]\'~~
212				~~SEARCHPATTERN:\'myString\'~~ -> search "myString" - case sensitive
213				~~SEARCHPATTERN;\'myString\'~~ -> search "myString" - case insensitive
214				~~SEARCHPATTERN#\'myRegex\'~~ -> search myRegex
215				Regex syntax is strictly the same as the one used with PHP "preg_match" function.
216				In all cases, every single quote inside the string/regex shall be doubled.
217				Options can be passed to limit pages/namespaces search. See website.',
218				'url'    => 'http://wiki.splitbrain.org/plugin:searchpattern',
219		);
220	}
221	*/
222
223	/**
224	* Plugin is a substitution one : typo is volunteer
225	*/
226	function getType(){
227		return 'substition';
228	}
229
230	/**
231	* Paragraph type is "normal"
232	*/
233	function getPType(){
234		return 'normal';
235	}
236
237	/**
238	* Sort order (don't know where it is used)
239	*/
240	function getSort(){
241		return 250;     //randon number, don't know what to put
242	}
243
244
245	/**
246	* Register the ID pattern to the lexer
247	*/
248	function connectTo($mode) {
249		$this->Lexer->addSpecialPattern('~~SEARCHPATTERN[\;\:\#]\'(?:\'\')*[^\r\v\t\r\n]*[^\']\'(?:\'\')*(?:\?\?[^\r\v\t\r\n\?]+\?\?)?~~', $mode, 'plugin_searchpattern');
250	}
251
252	/**
253	* Handle the match
254	*/
255	function handle($match, $state, $pos, &$handler){
256	global $INFO;
257
258		$params = array();	//store all parameters needed for the render computing
259		$params['match'] = $match;	//store the original matching string
260		$params['reg_err'] = false;  //init needed for future comparison
261		$params['regex_output_matches'] = false; // default behavior if no $ (dollar) is used
262		$params['call_plugin_handler'] = false;
263		if(substr($match,-4,2) == '??'){	//if options are passed
264			$options = substr($match, 0, -4);	//extract options ...
265			$match = substr($options, 0, strrpos($options,'??')).'~~';	//...
266			$options = substr($options, strrpos($options,'??')+2);	//...
267			if($options != ""){
268				$options_list = explode(' ', $options);	//explode options in an array
269				$limit = false;	//init : $limit will be true if a limiting option is passed
270				foreach($options_list as $optid => $option){	//going through the options to clean and pretreat
271					if($option == ""){	//remove empty ones...
272						unset($options_list[$optid]);	//...
273					}
274					if($option[0] == "+"){	//if a limiting option is used
275						$limit = true;	//update flag
276					}
277				}
278				foreach($options_list as $option){	//going through the options to treat them
279					$optact = $option[0];	//action is defined by first character
280					switch($optact){
281						// @date 20130407 by Leo Eibler <dokuwiki@sprossenwanne.at> extend output of matches with $
282						case '$':	//if it is a regex with displaying option of matches
283							if(substr($option,1) == ""){	//if nothing more, we will output all matches
284								$params['regex_output_matches'] = array();
285							} else {
286								// take match numbers as comma separated list
287								$params['regex_output_matches'] = explode( ',', substr($option,1) );
288							}
289							break;
290						case '_':	//call the _searchpatternHandler() method from the given plugin
291							if(substr($option,1) == ""){	//no plugin defined
292								// we cannot call a handler if the plugin name is not defined
293							} else {
294								$params['call_plugin_handler'] = substr($option,1);
295							}
296							break;
297						case '+':	//if it is a limiting option
298							if(substr($option,1) == ""){	//if nothing more
299								$params['page_limit'][] = $INFO['id'];	//add current page to limiting
300							}
301							elseif(substr($option,1) == ":"){	//if ':'
302								$params['nmsp_limit'][] = substr($INFO['id'],0,strrpos($INFO['id'],":")+1);	//add current namespace to limiting
303							}
304							elseif($option[strlen($option)-1] == ":"){	//if parameter is a namespace
305								$params['nmsp_limit'][] = substr($option,1);	//add to limiting namespace list
306							}
307							else{
308								$params['page_limit'][] = substr($option,1);	//add to limiting page list
309							}
310							break;
311						case '-':	//if it is a restricting option
312							if($limit){	//if a limiting option is also used
313								$params['ignored_options'][] = $option;	//restricting options will be ignored -> store them
314							}
315							else{
316								if(substr($option,1) == ""){	// if nothing more
317									$params['page_exclude'][] = $INFO['id'];	//add current page to restricting
318								}
319								elseif(substr($option,1) == ":"){	//if ':'
320									$params['nmsp_exclude'][] = substr($INFO['id'],0,strrpos($INFO['id'],":")+1);	//add current namespace to restricting
321								}
322								elseif($option[strlen($option)-1] == ":"){	//if parameter is a namespace
323									$params['nmsp_exclude'][] = substr($option,1);	//add to restricting namespace list
324								}
325								else{
326									$params['page_exclude'][] = substr($option,1);	//add to page restricting list
327								}
328							}
329							break;
330						default:
331							$params['bad_options'][] = $option;	//store unknown options
332					}
333				}
334			}
335		}
336		$pattern = substr($match, 17, -3);	//extract the pattern we are searching
337		if(preg_match('/[^\']\'(\'\')*[^\']/', $pattern, $matches)){	//if there is at least one non-doubled single quote in the pattern
338			$params['ndq_err'] = true;	//store error
339		}
340		else{
341			$params['ndq_err'] = false;	//or correctness
342		}
343		$pattern=str_replace('\'\'', '\'', $pattern);	//remove doubled quote from pattern
344		$params['or_pattern'] = $pattern;	//and store it as original one
345		switch($match[15]){	//determine the type of search and subsequent treatment
346			case ':':	//this a normal case-sensitive search
347				$params['type'] = 'normal';	//type is normal
348				$params['cs'] = 'cs';	//case-sensitive
349				$pattern = '/'.str2regex($pattern).'/';	//convert pattern to regex
350				break;
351			case ';':	//this a normal not case-sensitive search
352				$params['type'] = 'normal'; //type is normal
353				$params['cs'] = 'not_cs';  //not case-sensitive
354				$pattern = '/'.str2regex($pattern).'/i';	//convert pattern to regex
355				break;
356			case '#':	//this a regex search
357				$params['type'] = 'regex';	//type is regex
358				$params['cs'] = ''; //no case sensitivity for regex
359				if(!preg_match('/^\/.*\/[msixg]*$/', $pattern)){	//if regex is not correctly syntaxed
360					$params['reg_err'] = true;	//store regex error
361				}
362				break;
363			default:	//normally not reachable but we never know
364				$params['type'] = 'undef';
365		}
366		$params['pattern'] = $pattern;	//store the final pattern that we use for searching
367		return $params;	//return with parameters
368	}
369
370	/**
371	* Create output render
372	*/
373	function render($format, &$renderer, $data) {
374		global $conf;
375		if($format == 'xhtml'){
376			if(($data['type'] != 'undef') && ($data['ndq_err'] == false || $this->getConf('ndqerr') == 'warning' || $this->getConf('ndqerr') == 'nowarn') && ($data['type'] != 'regex' || $data['reg_err'] == false) && (!isset($data['bad_options']) || $this->getConf('badopt') == 'warning' || $this->getConf('badopt') == 'nowarn') && (!isset($data['ignored_options']) || $this->getConf('ignopt') == 'warning' || $this->getConf('ignopt') == 'nowarn')){	//if all conditions are ok
377				$data['cond_ok'] = true;
378				//definition : function search(&$data,$base,$func,$opts,$dir='',$lvl=1)
379				search($wikidata, $conf['datadir'], search_pattern, $data);	//browse wiki pages with callback to search_pattern
380			}
381			else{
382				$data['cond_ok'] = false;
383			}
384			$this->report($wikidata, $renderer, $data);
385			return true;	//return
386		}
387		return false;	//return
388	}
389
390	/**
391	* Create the report table
392	*/
393	function report($data, &$renderer, $params){
394		if( $params['cond_ok'] ) {	//if the search has been done
395			// if call_plugin_handler is active
396			$xPlugin = null;
397			if( !empty($params['call_plugin_handler']) ) {
398				// try to load the plugin and check if it is enabled for searchpattern callback handler
399				// therefor a method '_searchpatternHandler' in syntax plugin must exist
400				$badoptWarnErrorCssClass = '';
401				if( $this->getConf('badopt') == 'error' ) {
402					$badoptWarnErrorCssClass = 'sp_error';
403				} else
404				if( $this->getConf('badopt') == 'warning' ) {
405					$badoptWarnErrorCssClass = 'sp_warning';
406				}
407				$callPluginHandlerError = false;
408				if( preg_match( '/[^a-zA-Z0-9]+/i', $params['call_plugin_handler'], $temp ) > 0 ) {
409					//$renderer->doc .= '<div class="sp_main">';
410					if( !empty($badoptWarnErrorCssClass) ) {
411						$renderer->doc .= '<div class="'.$badoptWarnErrorCssClass.'"><span class="'.$badoptWarnErrorCssClass.'_pat">'.htmlspecialchars($params['call_plugin_handler']).'</span> :: '.$this->getLang('call_handler_invalid').'<br /></div>';	//write error message
412					}
413					$callPluginHandlerError = true;
414					//$renderer->doc .= '</div>';	//close error
415				} else {
416					if( plugin_isdisabled( $params['call_plugin_handler'] ) ) {
417						// @date 20130411 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: fallback to lowercase
418						// plugin_isdisabled will return true if plugin not exists - so try with lowercase
419						$params['call_plugin_handler'] = strtolower($params['call_plugin_handler']);
420					}
421					if( !plugin_isdisabled( $params['call_plugin_handler'] ) ) {
422						// @date 20130411 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: use the paramter call_plugin_handler
423						$xPlugin =& plugin_load('syntax', $params['call_plugin_handler'] );
424						if( $xPlugin && method_exists( $xPlugin, '_searchpatternHandler' ) ) {
425							//$renderer->doc .= $xPlugin && $xPlugin->_searchpatternHandler();
426						} else {
427							//$renderer->doc .= '<div class="sp_main">';
428							if( !empty($badoptWarnErrorCssClass) ) {
429								$renderer->doc .= '<div class="'.$badoptWarnErrorCssClass.'"><span class="'.$badoptWarnErrorCssClass.'_pat">'.htmlspecialchars($params['call_plugin_handler']).'</span> :: '.$this->getLang('call_handler_notsupported').'<br /></div>';	//write error message
430							}
431							$callPluginHandlerError = true;
432							//$renderer->doc .= '</div>';	//close error
433						}
434					} else {
435						//$renderer->doc .= '<div class="sp_main">';
436						if( !empty($badoptWarnErrorCssClass) ) {
437							$renderer->doc .= '<div class="'.$badoptWarnErrorCssClass.'"><span class="'.$badoptWarnErrorCssClass.'_pat">'.htmlspecialchars($params['call_plugin_handler']).'</span> :: '.$this->getLang('call_handler_disabled').'<br /></div>';	//write error message
438						}
439						$callPluginHandlerError = true;
440						//$renderer->doc .= '</div>';	//close error
441					}
442				}
443			}
444			if( $callPluginHandlerError && $this->getConf('badopt') == 'nocatch' ) {
445				// there was something wrong to call the plugin handler (maybe not implemented or plugin is disabled).
446				// but the user has configured to ignore this and output the original text
447				$renderer->doc .= htmlspecialchars($params['match']);	//in all remaining cases, display the original text
448				return;
449			}
450			if( $callPluginHandlerError && $this->getConf('badopt') == 'error' ) {
451				return;
452			}
453
454			// now process the data
455			// after processing:
456			//   data should hold array( $page => count of matches ); regex matches are stripped
457			//   matches should hold array( $page => matches from regex );
458			$matches = array();
459			if( $data ) {	//if there are search results
460				// @date 20130407 by Leo Eibler <dokuwiki@sprossenwanne.at> extend output of regex matches () with command $
461				// @date 20130411 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: default behavior (output only count) if no $ (dollar) is used
462				if( is_array($params['regex_output_matches']) && count($params['regex_output_matches']) == 0 ) {
463					$params['regex_output_matches'] = array(); // as default we will use all matches
464				}
465				foreach( $data as $page => $count ) {	//for each result
466					// @date 20130407 by Leo Eibler <dokuwiki@sprossenwanne.at> extend output of regex matches () with command $
467					if( substr( $page, 0, 1 ) == '{' && substr( $page, -1 ) == '}' ) {
468						// skip this one - it's for the regex matching result
469					} else {
470						if( isset( $data['{'.$page.'}'] ) ) {
471							$matches[$page] = $data['{'.$page.'}'];
472							unset($data['{'.$page.'}']);
473						}
474					}
475				}
476			}
477			// now check if the partner plugin handles all of the output itself
478			if( $xPlugin && $xPlugin->_searchpatternHandler( 'wholeoutput', $renderer, $data, $matches, $params ) ) {
479				return;
480			}
481			//
482
483			$renderer->doc .= '<div class="sp_main">';
484			$renderer->doc .= '<table class="inline sp_main_table">';	//create table
485			if( $this->getConf('dispheadl') == 'nodisp' ) {
486			} else {
487				$renderer->doc .= '<tr class="sp_title"><th colspan="2" class="sp_title">'.$this->getLang('src_res').' : <span class="sp_src">'.htmlspecialchars($params['or_pattern']).'</span><br /><span class="sp_src_params">'.$this->getLang($params['cs'].$params['type']).'</span></th></tr>';	//write table title
488			}
489			if($this->getConf('option') == 'disp' && (isset($params['page_limit']) || isset($params['nmsp_limit']) || isset($params['page_exclude']) || isset($params['nmsp_exclude']))){	//if limiting/restricting options are used and we shall display them
490				$renderer->doc .= '<tr class="sp_options"><td colspan="2" class="sp_options">';
491				if(isset($params['page_limit']) || isset($params['nmsp_limit'])){	//if limiting type
492					$renderer->doc .= $this->getLang('restriction').'<ul>';
493					if(isset($params['page_limit'])) {
494						foreach($params['page_limit'] as $page){
495							$renderer->doc .= '<li>'.$page.'</li>';
496						}
497					}
498					if(isset($params['page_limit'])) {
499						foreach($params['nmsp_limit'] as $nmsp){
500							$renderer->doc .= '<li>'.$nmsp.'</li>';
501						}
502					}
503					$renderer->doc .= '</ul>';
504				}
505				if(isset($params['page_exclude']) || $params['nmsp_exclude']){	//if excluding type
506					$renderer->doc .= $this->getLang('exclusion').'<ul>';
507					if(isset($params['page_exclude']))
508					{
509						foreach($params['page_exclude'] as $page){
510							$renderer->doc .= '<li>'.$page.'</li>';
511						}
512					}
513					if(isset($params['nmsp_exclude']))
514					{
515						foreach($params['nmsp_exclude'] as $nmsp){
516							$renderer->doc .= '<li>'.$nmsp.'</li>';
517						}
518					}
519					$renderer->doc .= '</ul>';
520				}
521				$renderer->doc .= '</td></tr>';
522			}
523			if($params['ndq_err'] == true && $this->getConf('ndqerr') == 'warning'){	//if there is a ndq error and we shall warn
524				$renderer->doc .= '<tr class="sp_warning"><td colspan="2" class="sp_warning">'.$this->getLang('ndq_err_warn').'</td></tr>';	//so warn
525			}
526			if(isset($params['bad_options']) && $this->getConf('badopt') == 'warning'){	//if an unknown option is used and we shall warn
527				$renderer->doc .= '<tr class="sp_warning"><td colspan="2" class="sp_warning">'.$this->getLang('badopt_warn').'<br><ul>'; //warn and
528				foreach($params['bad_options'] as $badopt){
529					$renderer->doc .= '<li>'.$badopt.'</li>';	//display the bad options list
530				}
531				$renderer->doc .= '</td></tr>';
532			}
533			if(isset($params['ignored_options']) && $this->getConf('ignopt') == 'warning'){ //if an option has been ignored
534				$renderer->doc .= '<tr class="sp_warning"><td colspan="2" class="sp_warning">'.$this->getLang('ignopt_warn').'<br><ul>'; //warn and
535				foreach($params['ignored_options'] as $ignopt){
536				  $renderer->doc .= '<li>'.$ignopt.'</li>'; //display the ignored options list
537				}
538				$renderer->doc .= '</td></tr>';
539			}
540			if( $data ) {	//if there are search results
541				// @date 20130407 by Leo Eibler <dokuwiki@sprossenwanne.at> extend output of regex matches () with command $
542				// @date 20130411 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: default behavior (output only count) if no $ (dollar) is used
543				if( is_array($params['regex_output_matches']) && count($params['regex_output_matches']) == 0 ) {
544					$params['regex_output_matches'] = array(); // as default we will use all matches
545				}
546				$renderer->doc .= '<tr class="sp_col_head"><th class="sp_col_head">'.$this->getLang('page_name').'</th><th class="sp_col_head">'.$this->getLang('num_match').'</th></tr>';	//write column headers
547				foreach($data as $page => $count){	//for each result
548					$renderer->doc .= '<tr class="sp_result"><td class="sp_page"><a href="'.wl($page).'">'.$page.'</a></td><td class="sp_count">';
549
550					if( $xPlugin && $xPlugin->_searchpatternHandler( 'intable:whole', $renderer, $data, $matches, $params, $page ) ) {
551						// the partner plugin handles all the inner table output itself - skip searchpattern output
552					} else {
553						if( $xPlugin ) {
554							$xPlugin->_searchpatternHandler( 'intable:prefix', $renderer, $data, $matches, $params, $page );
555						}
556						// @date 20130411 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: default behavior (output only count) if no $ (dollar) is used
557						// @date 20130412 by Leo Eibler <dokuwiki@sprossenwanne.at> bugfix: incorrect if statement
558						if( isset( $matches[$page] ) && is_array($params['regex_output_matches']) && count($params['regex_output_matches']) >= 0 ) {
559							$match = $matches[$page];
560							if( count($match) > 1 ) {
561								$renderer->doc .= '<table>';
562								$regex_output_matches = $params['regex_output_matches'];
563								if( count($regex_output_matches) == 0 ) {
564									// no match numbers given, use all existing, but strip 0
565									$regex_output_matches = array_keys( $match );
566									array_shift($regex_output_matches);
567								}
568								for( $i=0; $i<count($match[0]); $i++ ) {
569									if( !isset( $match[0][$i] ) ) {
570										continue;
571									}
572									$renderer->doc .= '<tr class="sp_result">';
573									foreach( $regex_output_matches as $j ) {
574										$renderer->doc .= '<td class="'.( isset($match[$j][$i] ) ? 'sp_count' : 'sp_nores' ).'">';
575										// does the partner plugin handle the inner table output itself?
576										if( $xPlugin ) {
577											if( !$xPlugin->_searchpatternHandler( 'intable:match', $renderer, $data, $matches, $params, $page, $match[$j][$i] ) ) {
578												$renderer->doc .= ( isset($match[$j][$i] ) ? htmlspecialchars($match[$j][$i]) : '' );
579											}
580										} else {
581											$renderer->doc .= ( isset($match[$j][$i] ) ? htmlspecialchars($match[$j][$i]) : '' );
582										}
583										$renderer->doc .= '</td>';
584									}
585									$renderer->doc .= '</tr>';
586								}
587								$renderer->doc .= '</table>';
588							} else {
589								if( $xPlugin ) {
590									if( !$xPlugin->_searchpatternHandler( 'intable:count', $renderer, $data, $matches, $params, $page, $count ) ) {
591										$renderer->doc .= $count;
592									}
593								} else {
594									$renderer->doc .= $count;
595								}
596							}
597						} else {
598							if( $xPlugin ) {
599								if( !$xPlugin->_searchpatternHandler( 'intable:count', $renderer, $data, $matches, $params, $page, $count ) ) {
600									$renderer->doc .= $count;
601								}
602							} else {
603								$renderer->doc .= $count;
604							}
605						}
606						if( $xPlugin ) {
607							$xPlugin->_searchpatternHandler( 'intable:suffix', $renderer, $data, $matches, $params, $page );
608						}
609					} // END the partner plugin handles all the inner table output itself - skip searchpattern output
610					$renderer->doc .= '</td></tr>';	// display it
611				}
612			} else {
613				// no result
614				$renderer->doc .= '<tr class="sp_result"><td colspan="2" class="sp_nores">';
615				if( $xPlugin ) {
616					$xPlugin->_searchpatternHandler( 'intable:prefix', $renderer, $data, $matches, $params, $page );
617				}
618				if( $xPlugin && $xPlugin->_searchpatternHandler( 'intable:noresult', $renderer, $data, $matches, $params, $page ) ) {
619					$renderer->doc .= $this->getLang('no_res');
620				}
621				if( $xPlugin ) {
622					$xPlugin->_searchpatternHandler( 'intable:suffix', $renderer, $data, $matches, $params, $page );
623				}
624				$renderer->doc .= '</td></tr>';	// write there is not any result
625			}
626			$renderer->doc .= '</table></div>';	//close table
627		} else {
628			// error
629			if(($this->getConf('ndqerr') == 'error' && $params['ndq_err'] == true) || ($this->getConf('regerr') == 'error' && $params['reg_err'] == true) || ($this->getConf('badopt') == 'error' && isset($params['bad_options']))){	//if there is an error that shall be displayed
630				$renderer->doc .= '<div class="sp_error"><span class="sp_error_pat">'.htmlspecialchars($params['match']).'</span> :: '.$this->getLang('pat_err').' :<br /><ul class="sp_error">';	//write error message
631				$err_found = false;	//true if the error is known
632				if($params['ndq_err'] == true && $this->getConf('ndqerr') == 'error'){	//if the error is ndq type
633					$renderer->doc .= '<li class="sp_error">'.$this->getLang('ndq_err').'</li>';	//display error
634					$err_found = true;	//we know the error
635				}
636				if($params['type'] == 'regex' && $params['reg_err'] == true && $this->getConf('regerr') == 'error'){	//if the error is reg type
637					$renderer->doc .= '<li class="sp_error">'.$this->getLang('reg_err').'</li>';	//display error
638					$err_found = true;	//we know the error
639				}
640				if(isset($params['bad_options']) && $this->getConf('badopt') == 'error'){	//if the error is a ba doption
641					$renderer->doc .= '<li class="sp_error">'.$this->getLang('badopt_err');	//display error
642					foreach($params['bad_options'] as $badopt){
643						$renderer->doc .= ' '.$badopt;	//display the bad option(s)
644					}
645					$renderer->doc .= '</li>';
646					$err_found = true; //we know the error
647				}
648				if($err_found == false){	//if the error is unknown type
649					$renderer->doc .= '<li class="sp_error">'.$this->getLang('unkw_err').'</li>';	//display a message
650				}
651				$renderer->doc .= '</ul></div>';	//close error
652			}
653			else
654			{
655				$renderer->doc .= htmlspecialchars($params['match']);	//in all remaining cases, display the original text
656			}
657		}
658	}
659
660
661}
662
663?>