Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 103 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 103 |
|
0.00% |
0 / 13 |
1640 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onParserFirstCallInit | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
parserFunction | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
getCategorySidebarBox | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onSkinBuildSidebar | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onSkinAfterPortlet | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
parserHook | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
90 | |||
onOutputPageRenderCategoryLink | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getDataForJs | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onSpecialTrackingCategories__preprocess | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onSpecialTrackingCategories__generateCatLink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onCategoryViewer__doCategoryQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
onCategoryViewer__generateLink | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | /** |
3 | * © 2006-2008 Daniel Kinzler and others |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Extensions |
22 | * @author Daniel Kinzler, brightbyte.de |
23 | */ |
24 | |
25 | namespace MediaWiki\Extension\CategoryTree; |
26 | |
27 | use MediaWiki\Config\Config; |
28 | use MediaWiki\Context\RequestContext; |
29 | use MediaWiki\Hook\CategoryViewer__doCategoryQueryHook; |
30 | use MediaWiki\Hook\CategoryViewer__generateLinkHook; |
31 | use MediaWiki\Hook\ParserFirstCallInitHook; |
32 | use MediaWiki\Hook\SkinBuildSidebarHook; |
33 | use MediaWiki\Hook\SpecialTrackingCategories__generateCatLinkHook; |
34 | use MediaWiki\Hook\SpecialTrackingCategories__preprocessHook; |
35 | use MediaWiki\Html\Html; |
36 | use MediaWiki\Linker\LinkRenderer; |
37 | use MediaWiki\Linker\LinkTarget; |
38 | use MediaWiki\Output\Hook\OutputPageRenderCategoryLinkHook; |
39 | use MediaWiki\Output\OutputPage; |
40 | use MediaWiki\Page\ProperPageIdentity; |
41 | use MediaWiki\Parser\Parser; |
42 | use MediaWiki\Parser\Sanitizer; |
43 | use MediaWiki\ResourceLoader as RL; |
44 | use MediaWiki\SpecialPage\SpecialPage; |
45 | use MediaWiki\Title\Title; |
46 | use MediaWiki\Title\TitleFormatter; |
47 | use Skin; |
48 | use Wikimedia\Rdbms\IConnectionProvider; |
49 | use Wikimedia\Rdbms\IResultWrapper; |
50 | |
51 | /** |
52 | * Hooks for the CategoryTree extension, an AJAX based gadget |
53 | * to display the category structure of a wiki |
54 | * |
55 | * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
56 | */ |
57 | class Hooks implements |
58 | SpecialTrackingCategories__preprocessHook, |
59 | SpecialTrackingCategories__generateCatLinkHook, |
60 | SkinBuildSidebarHook, |
61 | ParserFirstCallInitHook, |
62 | OutputPageRenderCategoryLinkHook, |
63 | CategoryViewer__doCategoryQueryHook, |
64 | CategoryViewer__generateLinkHook |
65 | { |
66 | private CategoryCache $categoryCache; |
67 | private Config $config; |
68 | private IConnectionProvider $dbProvider; |
69 | private LinkRenderer $linkRenderer; |
70 | private TitleFormatter $titleFormatter; |
71 | |
72 | public function __construct( |
73 | CategoryCache $categoryCache, |
74 | Config $config, |
75 | IConnectionProvider $dbProvider, |
76 | LinkRenderer $linkRenderer, |
77 | TitleFormatter $titleFormatter |
78 | ) { |
79 | $this->categoryCache = $categoryCache; |
80 | $this->config = $config; |
81 | $this->dbProvider = $dbProvider; |
82 | $this->linkRenderer = $linkRenderer; |
83 | $this->titleFormatter = $titleFormatter; |
84 | } |
85 | |
86 | /** |
87 | * @param Parser $parser |
88 | */ |
89 | public function onParserFirstCallInit( $parser ) { |
90 | if ( !$this->config->get( 'CategoryTreeAllowTag' ) ) { |
91 | return; |
92 | } |
93 | $parser->setHook( 'categorytree', [ $this, 'parserHook' ] ); |
94 | $parser->setFunctionHook( 'categorytree', [ $this, 'parserFunction' ] ); |
95 | } |
96 | |
97 | /** |
98 | * Entry point for the {{#categorytree}} tag parser function. |
99 | * This is a wrapper around Hooks::parserHook |
100 | * @param Parser $parser |
101 | * @param string ...$params |
102 | * @return array|string |
103 | */ |
104 | public function parserFunction( Parser $parser, ...$params ) { |
105 | // first user-supplied parameter must be category name |
106 | if ( !$params ) { |
107 | // no category specified, return nothing |
108 | return ''; |
109 | } |
110 | $cat = array_shift( $params ); |
111 | |
112 | // build associative arguments from flat parameter list |
113 | $argv = []; |
114 | foreach ( $params as $p ) { |
115 | if ( preg_match( '/^\s*(\S.*?)\s*=\s*(.*?)\s*$/', $p, $m ) ) { |
116 | $k = $m[1]; |
117 | // strip any quotes enclosing the value |
118 | $v = preg_replace( '/^"\s*(.*?)\s*"$/', '$1', $m[2] ); |
119 | } else { |
120 | $k = trim( $p ); |
121 | $v = true; |
122 | } |
123 | |
124 | $argv[$k] = $v; |
125 | } |
126 | |
127 | if ( $parser->getOutputType() === Parser::OT_PREPROCESS ) { |
128 | return Html::rawElement( 'categorytree', $argv, $cat ); |
129 | } else { |
130 | // now handle just like a <categorytree> tag |
131 | $html = $this->parserHook( $cat, $argv, $parser ); |
132 | return [ $html, 'noparse' => true, 'isHTML' => true ]; |
133 | } |
134 | } |
135 | |
136 | /** |
137 | * Obtain a category sidebar link based on config |
138 | * @return bool|string of link |
139 | */ |
140 | private function getCategorySidebarBox() { |
141 | if ( !$this->config->get( 'CategoryTreeSidebarRoot' ) ) { |
142 | return false; |
143 | } |
144 | return $this->parserHook( |
145 | $this->config->get( 'CategoryTreeSidebarRoot' ), |
146 | $this->config->get( 'CategoryTreeSidebarOptions' ) |
147 | ); |
148 | } |
149 | |
150 | /** |
151 | * Hook implementation for injecting a category tree into the sidebar. |
152 | * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name. |
153 | * @param Skin $skin |
154 | * @param array &$sidebar |
155 | */ |
156 | public function onSkinBuildSidebar( $skin, &$sidebar ) { |
157 | $html = $this->getCategorySidebarBox(); |
158 | if ( $html ) { |
159 | $sidebar['categorytree-portlet'] = []; |
160 | CategoryTree::setHeaders( $skin->getOutput() ); |
161 | } |
162 | } |
163 | |
164 | /** |
165 | * Hook implementation for injecting a category tree link into the sidebar. |
166 | * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name. |
167 | * @param Skin $skin |
168 | * @param string $portlet |
169 | * @param string &$html |
170 | */ |
171 | public function onSkinAfterPortlet( $skin, $portlet, &$html ) { |
172 | if ( $portlet === 'categorytree-portlet' ) { |
173 | $box = $this->getCategorySidebarBox(); |
174 | if ( $box ) { |
175 | $html .= $box; |
176 | } |
177 | } |
178 | } |
179 | |
180 | /** |
181 | * Entry point for the <categorytree> tag parser hook. |
182 | * This loads CategoryTree and calls CategoryTree::getTag() |
183 | * @param string|null $cat |
184 | * @param array $argv |
185 | * @param Parser|null $parser |
186 | * @return bool|string |
187 | */ |
188 | public function parserHook( |
189 | ?string $cat, |
190 | array $argv, |
191 | ?Parser $parser = null |
192 | ) { |
193 | if ( $parser ) { |
194 | $parserOutput = $parser->getOutput(); |
195 | $parserOutput->addModuleStyles( [ 'ext.categoryTree.styles' ] ); |
196 | $parserOutput->addModules( [ 'ext.categoryTree' ] ); |
197 | |
198 | $disableCache = $this->config->get( 'CategoryTreeDisableCache' ); |
199 | if ( $disableCache === true ) { |
200 | $parserOutput->updateCacheExpiry( 0 ); |
201 | } elseif ( is_int( $disableCache ) ) { |
202 | $parserOutput->updateCacheExpiry( $disableCache ); |
203 | } |
204 | } |
205 | |
206 | $ct = new CategoryTree( $argv, $this->config, $this->dbProvider, $this->linkRenderer ); |
207 | |
208 | $attr = Sanitizer::validateTagAttributes( $argv, 'div' ); |
209 | |
210 | $hideroot = isset( $argv['hideroot'] ) |
211 | ? OptionManager::decodeBoolean( $argv['hideroot'] ) : false; |
212 | $onlyroot = isset( $argv['onlyroot'] ) |
213 | ? OptionManager::decodeBoolean( $argv['onlyroot'] ) : false; |
214 | $depthArg = isset( $argv['depth'] ) ? (int)$argv['depth'] : 1; |
215 | |
216 | $depth = $ct->optionManager->capDepth( $depthArg ); |
217 | if ( $onlyroot ) { |
218 | $depth = 0; |
219 | $message = '<span class="error">' |
220 | . wfMessage( 'categorytree-onlyroot-message' )->inContentLanguage()->parse() |
221 | . '</span>'; |
222 | if ( $parser ) { |
223 | $parser->getOutput()->addWarningMsg( 'categorytree-deprecation-warning' ); |
224 | $parser->addTrackingCategory( 'categorytree-deprecation-category' ); |
225 | } |
226 | } else { |
227 | $message = ''; |
228 | } |
229 | |
230 | return $message . |
231 | $ct->getTag( $cat ?? '', $hideroot, $attr, $depth ); |
232 | } |
233 | |
234 | /** |
235 | * OutputPageRenderCategoryLink hook |
236 | * @param OutputPage $out |
237 | * @param ProperPageIdentity $categoryTitle |
238 | * @param string $text |
239 | * @param ?string &$link |
240 | * @return void |
241 | */ |
242 | public function onOutputPageRenderCategoryLink( |
243 | OutputPage $out, |
244 | ProperPageIdentity $categoryTitle, |
245 | string $text, |
246 | ?string &$link |
247 | ): void { |
248 | if ( !$this->config->get( 'CategoryTreeHijackPageCategories' ) ) { |
249 | // Not enabled, don't do anything |
250 | return; |
251 | } |
252 | if ( !$categoryTitle->exists() ) { |
253 | // Category doesn't exist. Let the normal LinkRenderer generate the link. |
254 | return; |
255 | } |
256 | |
257 | CategoryTree::setHeaders( $out ); |
258 | |
259 | $options = $this->config->get( 'CategoryTreePageCategoryOptions' ); |
260 | $link = $this->parserHook( |
261 | $this->titleFormatter->getPrefixedText( $categoryTitle ), |
262 | $options |
263 | ); |
264 | } |
265 | |
266 | /** |
267 | * Get exported data for the "ext.categoryTree" ResourceLoader module. |
268 | * |
269 | * @internal For use in extension.json only. |
270 | * @param RL\Context $context |
271 | * @param Config $config |
272 | * @return array Data to be serialised as data.json |
273 | */ |
274 | public static function getDataForJs( RL\Context $context, Config $config ) { |
275 | // Look, this is pretty bad but CategoryTree is just whacky, it needs to be rewritten |
276 | $optionManager = new OptionManager( $config->get( 'CategoryTreeCategoryPageOptions' ), $config ); |
277 | |
278 | return [ |
279 | 'defaultCtOptions' => $optionManager->getOptionsAsJsStructure(), |
280 | ]; |
281 | } |
282 | |
283 | /** |
284 | * Hook handler for the SpecialTrackingCategories::preprocess hook |
285 | * @param SpecialPage $specialPage SpecialTrackingCategories object |
286 | * @param array $trackingCategories [ 'msg' => LinkTarget, 'cats' => LinkTarget[] ] |
287 | * @phan-param array<string,array{msg:LinkTarget,cats:LinkTarget[]}> $trackingCategories |
288 | */ |
289 | public function onSpecialTrackingCategories__preprocess( |
290 | $specialPage, |
291 | $trackingCategories |
292 | ) { |
293 | $categoryTargets = []; |
294 | foreach ( $trackingCategories as $data ) { |
295 | foreach ( $data['cats'] as $catTitle ) { |
296 | $categoryTargets[] = $catTitle; |
297 | } |
298 | } |
299 | $this->categoryCache->doQuery( $categoryTargets ); |
300 | } |
301 | |
302 | /** |
303 | * Hook handler for the SpecialTrackingCategories::generateCatLink hook |
304 | * @param SpecialPage $specialPage SpecialTrackingCategories object |
305 | * @param LinkTarget $catTitle LinkTarget object of the linked category |
306 | * @param string &$html Result html |
307 | */ |
308 | public function onSpecialTrackingCategories__generateCatLink( $specialPage, |
309 | $catTitle, &$html |
310 | ) { |
311 | $cat = $this->categoryCache->getCategory( $catTitle ); |
312 | |
313 | $html .= CategoryTree::createCountString( $specialPage->getContext(), $cat, 0 ); |
314 | } |
315 | |
316 | /** |
317 | * @param string $type |
318 | * @param IResultWrapper $res |
319 | */ |
320 | public function onCategoryViewer__doCategoryQuery( $type, $res ) { |
321 | if ( $type === 'subcat' && $res ) { |
322 | $this->categoryCache->fillFromQuery( $res ); |
323 | } |
324 | } |
325 | |
326 | /** |
327 | * @param string $type |
328 | * @param Title $title |
329 | * @param string $html |
330 | * @param string &$link |
331 | * @return bool |
332 | */ |
333 | public function onCategoryViewer__generateLink( $type, $title, $html, &$link ) { |
334 | if ( $type !== 'subcat' || $link !== null ) { |
335 | return true; |
336 | } |
337 | |
338 | $request = RequestContext::getMain()->getRequest(); |
339 | if ( $request->getCheck( 'notree' ) ) { |
340 | return true; |
341 | } |
342 | |
343 | $options = $this->config->get( 'CategoryTreeCategoryPageOptions' ); |
344 | $mode = $request->getRawVal( 'mode' ); |
345 | if ( $mode !== null ) { |
346 | $options['mode'] = $mode; |
347 | } |
348 | $tree = new CategoryTree( $options, $this->config, $this->dbProvider, $this->linkRenderer ); |
349 | |
350 | $cat = $this->categoryCache->getCategory( $title ); |
351 | |
352 | $link = $tree->renderNodeInfo( $title, $cat ); |
353 | |
354 | CategoryTree::setHeaders( RequestContext::getMain()->getOutput() ); |
355 | return false; |
356 | } |
357 | } |