Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 185 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
SubPageList3 | |
0.00% |
0 / 185 |
|
0.00% |
0 / 10 |
6162 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
renderSubpageList3 | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
error | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
geterrors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
options | |
0.00% |
0 / 71 |
|
0.00% |
0 / 1 |
1640 | |||
render | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getTitles | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
90 | |||
makeListItem | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
makeList | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
210 | |||
parse | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SubPageList3; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Title\Title; |
10 | use Parser; |
11 | use PPFrame; |
12 | use Wikimedia\Rdbms\IExpression; |
13 | use Wikimedia\Rdbms\LikeValue; |
14 | |
15 | /** |
16 | * SubPageList3 class |
17 | */ |
18 | class SubPageList3 { |
19 | /** |
20 | * @var Parser |
21 | */ |
22 | private $parser; |
23 | |
24 | /** |
25 | * @var PPFrame|bool |
26 | */ |
27 | private $frame; |
28 | |
29 | /** |
30 | * @var Title |
31 | */ |
32 | private $title; |
33 | |
34 | /** |
35 | * @var Title |
36 | */ |
37 | private $ptitle; |
38 | |
39 | /** |
40 | * @var string |
41 | */ |
42 | private $namespace = ''; |
43 | |
44 | /** |
45 | * @var string token object |
46 | */ |
47 | private $token = '*'; |
48 | |
49 | /** |
50 | * @var int error display on or off |
51 | * @default 0 hide errors |
52 | */ |
53 | private $debug = 0; |
54 | |
55 | /** |
56 | * contain the error messages |
57 | * @var array contain the errors messages |
58 | */ |
59 | private $errors = []; |
60 | |
61 | /** |
62 | * order type |
63 | * Can be: |
64 | * - asc |
65 | * - desc |
66 | * @var string order type |
67 | */ |
68 | private $order = 'asc'; |
69 | |
70 | /** |
71 | * column that's used as order method |
72 | * Can be: |
73 | * - title: alphabetic order of a page title |
74 | * - lastedit: Timestamp numeric order of the last edit of a page |
75 | * @var string order method |
76 | * @private |
77 | */ |
78 | private $ordermethod = 'title'; |
79 | |
80 | /** |
81 | * mode of the output |
82 | * Can be: |
83 | * - unordered: UL list as output |
84 | * - ordered: OL list as output |
85 | * - bar: uses ยท as a delimiter producing a horizontal bar menu |
86 | * @var string mode of output |
87 | * @default unordered |
88 | */ |
89 | private $mode = 'unordered'; |
90 | |
91 | /** |
92 | * parent of the listed pages |
93 | * Can be: |
94 | * - -1: the current page title |
95 | * - string: title of the specific title |
96 | * e.g. if you are in Mainpage/ it will list all subpages of Mainpage/ |
97 | * @var mixed parent of listed pages |
98 | * @default -1 current |
99 | */ |
100 | private $parent = -1; |
101 | |
102 | /** |
103 | * style of the path (title) |
104 | * Can be: |
105 | * - full: normal, e.g. Mainpage/Entry/Sub |
106 | * - notparent: the path without the $parent item, e.g. Entry/Sub |
107 | * - no: no path, only the page title, e.g. Sub |
108 | * @var string style of the path (title) |
109 | * @default normal |
110 | * @see $parent |
111 | */ |
112 | private $showpath = 'no'; |
113 | |
114 | /** |
115 | * whether to show next sublevel only, or all sublevels |
116 | * Can be: |
117 | * - 0 / no / false |
118 | * - 1 / yes / true |
119 | * @var mixed show one sublevel only |
120 | * @default 0 |
121 | * @see $parent |
122 | */ |
123 | private $kidsonly = 0; |
124 | |
125 | /** |
126 | * whether to show parent as the top item |
127 | * Can be: |
128 | * - 0 / no / false |
129 | * - 1 / yes / true |
130 | * @var mixed show one sublevel only |
131 | * @default 0 |
132 | * @see $parent |
133 | */ |
134 | private $showparent = 0; |
135 | |
136 | /** |
137 | * Text to show when parent has no subpages to list |
138 | * when null (by default) shows default message |
139 | * @var string|null |
140 | * @default null |
141 | */ |
142 | private $nosubpages = null; |
143 | |
144 | /** |
145 | * Default limit of descendants |
146 | * @var int |
147 | * @default 200 |
148 | */ |
149 | private const DESCENDANTS_LIMIT_DEFAULT = 200; |
150 | |
151 | /** @var Config */ |
152 | private $config; |
153 | |
154 | /** |
155 | * Constructor function of the class |
156 | * @param Parser $parser the parser object |
157 | * @param Config $config |
158 | * @param PPFrame|bool $frame |
159 | * @see SubpageList |
160 | */ |
161 | private function __construct( Parser $parser, Config $config, $frame = false ) { |
162 | $this->parser = $parser; |
163 | $this->frame = $frame; |
164 | $this->title = $parser->getTitle(); |
165 | $this->config = $config; |
166 | } |
167 | |
168 | /** |
169 | * Function called by the Hook, returns the wiki text |
170 | * |
171 | * @param string $input |
172 | * @param array $args |
173 | * @param Parser $parser |
174 | * @param PPFrame $frame |
175 | * @return string |
176 | */ |
177 | public static function renderSubpageList3( $input, array $args, Parser $parser, PPFrame $frame ) { |
178 | $config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'SubPageList3' ); |
179 | $list = new SubpageList3( $parser, $config, $frame ); |
180 | $list->options( $args ); |
181 | |
182 | # $parser->disableCache(); |
183 | return $list->render(); |
184 | } |
185 | |
186 | /** |
187 | * adds error to the $errors container |
188 | * but only if $debug is true or 1 |
189 | * @param string $message the errors message |
190 | * @see $errors |
191 | * @see $debug |
192 | */ |
193 | private function error( $message ) { |
194 | if ( $this->debug ) { |
195 | $this->errors[] = "<strong>Error [Subpage List 3]:</strong> $message"; |
196 | } |
197 | } |
198 | |
199 | /** |
200 | * returns all errors as a string |
201 | * @return string all errors separated by a newline |
202 | */ |
203 | private function geterrors() { |
204 | return implode( "\n", $this->errors ); |
205 | } |
206 | |
207 | /** |
208 | * parse the options that the user has entered |
209 | * a bit long way, but because that it's easy to add alias |
210 | * @param array $options the options inserts by the user as array |
211 | * @see $debug |
212 | * @see $order |
213 | * @see $ordermethod |
214 | * @see $mode |
215 | * @see $parent |
216 | * @see $showpath |
217 | * @see $kidsonly |
218 | * @see $showparent |
219 | */ |
220 | private function options( $options ) { |
221 | if ( isset( $options['debug'] ) ) { |
222 | if ( in_array( $options['debug'], [ 'true', 1, '1' ], true ) ) { |
223 | $this->debug = 1; |
224 | } elseif ( in_array( $options['debug'], [ 'false', 0, '0' ], true ) ) { |
225 | $this->debug = 0; |
226 | } else { |
227 | $this->error( wfMessage( 'spl3_debug', 'debug' )->escaped() ); |
228 | } |
229 | } |
230 | if ( isset( $options['sort'] ) ) { |
231 | switch ( strtolower( $options['sort'] ) ) { |
232 | case 'asc': |
233 | $this->order = 'asc'; |
234 | break; |
235 | case 'desc': |
236 | $this->order = 'desc'; |
237 | break; |
238 | default: |
239 | $this->error( wfMessage( 'spl3_debug', 'sort' )->escaped() ); |
240 | } |
241 | } |
242 | if ( isset( $options['sortby'] ) ) { |
243 | switch ( strtolower( $options['sortby'] ) ) { |
244 | case 'title': |
245 | $this->ordermethod = 'title'; |
246 | break; |
247 | case 'lastedit': |
248 | $this->ordermethod = 'lastedit'; |
249 | break; |
250 | default: |
251 | $this->error( wfMessage( 'spl3_debug', 'sortby' )->escaped() ); |
252 | } |
253 | } |
254 | if ( isset( $options['liststyle'] ) ) { |
255 | switch ( strtolower( $options['liststyle'] ) ) { |
256 | case 'ordered': |
257 | $this->mode = 'ordered'; |
258 | $this->token = '#'; |
259 | break; |
260 | case 'unordered': |
261 | $this->mode = 'unordered'; |
262 | $this->token = '*'; |
263 | break; |
264 | case 'bar': |
265 | $this->mode = 'bar'; |
266 | $this->token = ' ยท '; |
267 | break; |
268 | default: |
269 | $this->error( wfMessage( 'spl3_debug', 'liststyle' )->escaped() ); |
270 | } |
271 | } |
272 | if ( isset( $options['parent'] ) ) { |
273 | if ( intval( $options['parent'] ) == -1 ) { |
274 | $this->parent = -1; |
275 | } elseif ( is_string( $options['parent'] ) ) { |
276 | $this->parent = $this->parse( $options['parent'] ); |
277 | } else { |
278 | $this->error( wfMessage( 'spl3_debug', 'parent' )->escaped() ); |
279 | } |
280 | } |
281 | if ( isset( $options['showpath'] ) ) { |
282 | $showPath = strtolower( $options['showpath'] ); |
283 | if ( $showPath === 'no' || $showPath === '0' || $showPath === 'false' ) { |
284 | $this->showpath = 'no'; |
285 | } elseif ( $showPath === 'notparent' ) { |
286 | $this->showpath = 'notparent'; |
287 | } elseif ( in_array( $showPath, [ 'full', 'yes', '1', 'true' ], true ) ) { |
288 | $this->showpath = 'full'; |
289 | } else { |
290 | $this->error( wfMessage( 'spl3_debug', 'showpath' )->escaped() ); |
291 | } |
292 | } |
293 | if ( isset( $options['kidsonly'] ) ) { |
294 | if ( $options['kidsonly'] == 'true' || $options['kidsonly'] == 'yes' |
295 | || intval( $options['kidsonly'] ) == 1 |
296 | ) { |
297 | $this->kidsonly = 1; |
298 | } elseif ( $options['kidsonly'] == 'false' || $options['kidsonly'] == 'no' |
299 | || intval( $options['kidsonly'] ) == 0 |
300 | ) { |
301 | $this->kidsonly = 0; |
302 | } else { |
303 | $this->error( wfMessage( 'spl3_debug', 'kidsonly' )->escaped() ); |
304 | } |
305 | } |
306 | if ( isset( $options['showparent'] ) ) { |
307 | if ( $options['showparent'] == 'true' || $options['showparent'] == 'yes' |
308 | || intval( $options['showparent'] ) == 1 |
309 | ) { |
310 | $this->showparent = 1; |
311 | } elseif ( $options['showparent'] == 'false' || $options['showparent'] == 'no' |
312 | || intval( $options['showparent'] ) == 0 |
313 | ) { |
314 | $this->showparent = 0; |
315 | } else { |
316 | $this->error( wfMessage( 'spl3_debug', 'showparent' )->escaped() ); |
317 | } |
318 | } |
319 | |
320 | $this->nosubpages = $options['nosubpages'] ?? null; |
321 | } |
322 | |
323 | /** |
324 | * produce output using this class |
325 | * @return string html output |
326 | */ |
327 | private function render() { |
328 | $pages = $this->getTitles(); |
329 | $class = 'subpagelist'; |
330 | if ( $pages != null && count( $pages ) > 0 ) { |
331 | $list = $this->makeList( $pages ); |
332 | $html = $this->parse( $list ); |
333 | } else { |
334 | if ( $this->nosubpages !== null ) { |
335 | $out = $this->nosubpages; |
336 | } else { |
337 | $plink = "[[" . $this->parent . "]]"; |
338 | $out = "''" . wfMessage( 'spl3_nosubpages', $plink )->text() . "''\n"; |
339 | } |
340 | $html = $this->parse( $out ); |
341 | $class .= ' subpagelist-empty'; |
342 | } |
343 | $html = $this->geterrors() . $html; |
344 | return Html::rawElement( 'div', [ 'class' => $class ], $html ); |
345 | } |
346 | |
347 | /** |
348 | * return the page titles of the subpages in an array |
349 | * @return array|null all titles, null on failure |
350 | */ |
351 | private function getTitles() { |
352 | if ( $this->parent !== -1 ) { |
353 | $this->ptitle = Title::newFromText( $this->parent ); |
354 | $user = MediaWikiServices::getInstance()->getUserFactory() |
355 | ->newFromUserIdentity( $this->parser->getUserIdentity() ); |
356 | // note that non-existent pages may nevertheless have valid subpages |
357 | // on the other hand, not checking that the page exists can let input |
358 | // through which causes database errors |
359 | if ( |
360 | $this->ptitle instanceof Title && |
361 | $this->ptitle->exists() && |
362 | $user->definitelyCan( 'read', $this->ptitle ) |
363 | ) { |
364 | $parent = $this->ptitle->getDBkey(); |
365 | $this->parent = $parent; |
366 | $this->namespace = $this->ptitle->getNsText(); |
367 | $nsi = $this->ptitle->getNamespace(); |
368 | } else { |
369 | $this->error( wfMessage( 'spl3_debug', 'parent' )->escaped() ); |
370 | return null; |
371 | } |
372 | } else { |
373 | $this->ptitle = $this->title; |
374 | $parent = $this->title->getDBkey(); |
375 | $this->parent = $parent; |
376 | $this->namespace = $this->title->getNsText(); |
377 | $nsi = $this->title->getNamespace(); |
378 | } |
379 | |
380 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
381 | $queryBuilder = $dbr->newSelectQueryBuilder() |
382 | ->select( [ 'page_namespace', 'page_title' ] ) |
383 | ->from( 'page' ) |
384 | // don't let lists cross namespaces or include redirects |
385 | ->where( [ |
386 | 'page_namespace' => $nsi, |
387 | 'page_is_redirect' => 0, |
388 | $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $parent . '/', $dbr->anyString() ) ), |
389 | ] ) |
390 | ->caller( __METHOD__ ); |
391 | |
392 | $order = strtoupper( $this->order ); |
393 | if ( $this->ordermethod == 'title' ) { |
394 | $queryBuilder->orderBy( 'page_title', $order ); |
395 | } elseif ( $this->ordermethod == 'lastedit' ) { |
396 | $queryBuilder->orderBy( 'page_touched', $order ); |
397 | } |
398 | |
399 | $res = $queryBuilder->fetchResultSet(); |
400 | |
401 | $titles = []; |
402 | foreach ( $res as $row ) { |
403 | $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); |
404 | if ( $title ) { |
405 | $titles[] = $title; |
406 | } |
407 | } |
408 | |
409 | return $titles; |
410 | } |
411 | |
412 | /** |
413 | * create one list item |
414 | * cases: |
415 | * - full: full, e.g. Mainpage/Entry/Sub |
416 | * - notparent: the path without the $parent item, e.g. Entry/Sub |
417 | * - no: no path, only the page title, e.g. Sub |
418 | * @param Title $title the title of a page |
419 | * @return string the prepared string |
420 | * @see $showpath |
421 | */ |
422 | private function makeListItem( $title ) { |
423 | switch ( $this->showpath ) { |
424 | case 'no': |
425 | $linktitle = substr( strrchr( $title->getText(), "/" ), 1 ); |
426 | break; |
427 | case 'notparent': |
428 | $linktitle = substr( strstr( $title->getText(), "/" ), 1 ); |
429 | break; |
430 | case 'full': |
431 | $linktitle = $title->getText(); |
432 | break; |
433 | default: |
434 | throw new LogicException( "Can not happen" ); |
435 | } |
436 | return ' [[' . $title->getPrefixedText() . '|' . $linktitle . ']]'; |
437 | } |
438 | |
439 | /** |
440 | * create whole list using makeListItem |
441 | * @param array $titles Array all page titles |
442 | * @return string the whole list |
443 | * @see SubPageList::makeListItem |
444 | */ |
445 | private function makeList( $titles ) { |
446 | $descendantsLimitRaw = $this->config->get( 'SubPageListDescendantsLimit' ); |
447 | $descendantsLimit = is_int( $descendantsLimitRaw ) ? $descendantsLimitRaw : self::DESCENDANTS_LIMIT_DEFAULT; |
448 | $c = 0; |
449 | $list = []; |
450 | # add parent item |
451 | if ( $this->showparent ) { |
452 | $pn = '[[' . $this->ptitle->getPrefixedText() . '|' . $this->ptitle->getText() . ']]'; |
453 | if ( $this->mode != 'bar' ) { |
454 | $pn = $this->token . $pn; |
455 | } |
456 | $ss = trim( $pn ); |
457 | $list[] = $ss; |
458 | // flag for bar token to be added on next item |
459 | $c++; |
460 | } |
461 | # add descendants |
462 | $parlv = substr_count( $this->ptitle->getPrefixedText(), '/' ); |
463 | foreach ( $titles as $title ) { |
464 | $lv = substr_count( $title, '/' ) - $parlv; |
465 | if ( $this->kidsonly != 1 || $lv < 2 ) { |
466 | if ( $this->showparent ) { |
467 | $lv++; |
468 | } |
469 | $ss = ""; |
470 | if ( $this->mode == 'bar' ) { |
471 | if ( $c > 0 ) { |
472 | $ss .= $this->token; |
473 | } |
474 | } else { |
475 | for ( $i = 0; $i < $lv; $i++ ) { |
476 | $ss .= $this->token; |
477 | } |
478 | } |
479 | $ss .= $this->makeListItem( $title ); |
480 | // make sure we don't get any <pre></pre> tags |
481 | $ss = trim( $ss ); |
482 | $list[] = $ss; |
483 | } |
484 | $c++; |
485 | if ( $c > $descendantsLimit ) { |
486 | break; |
487 | } |
488 | } |
489 | $retval = ''; |
490 | if ( count( $list ) > 0 ) { |
491 | $retval = implode( "\n", $list ); |
492 | if ( $this->mode == 'bar' ) { |
493 | $retval = implode( "", $list ); |
494 | } |
495 | // Workaround for bug where the first items */# in a list would remain unparsed |
496 | $retval = "\n" . $retval; |
497 | } |
498 | |
499 | return $retval; |
500 | } |
501 | |
502 | /** |
503 | * Wrapper function parse, call the other functions |
504 | * @param string $text the content |
505 | * @return string the parsed output |
506 | */ |
507 | private function parse( $text ) { |
508 | return $this->parser->recursiveTagParse( $text, $this->frame ); |
509 | } |
510 | } |