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