Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 13
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parserFunction
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 getCategorySidebarBox
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinBuildSidebar
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinAfterPortlet
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 parserHook
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 onOutputPageMakeCategoryLinks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getDataForJs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onSpecialTrackingCategories__preprocess
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onSpecialTrackingCategories__generateCatLink
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onCategoryViewer__doCategoryQuery
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 onCategoryViewer__generateLink
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
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
25namespace MediaWiki\Extension\CategoryTree;
26
27use MediaWiki\Config\Config;
28use MediaWiki\Hook\CategoryViewer__doCategoryQueryHook;
29use MediaWiki\Hook\CategoryViewer__generateLinkHook;
30use MediaWiki\Hook\OutputPageMakeCategoryLinksHook;
31use MediaWiki\Hook\ParserFirstCallInitHook;
32use MediaWiki\Hook\SkinBuildSidebarHook;
33use MediaWiki\Hook\SpecialTrackingCategories__generateCatLinkHook;
34use MediaWiki\Hook\SpecialTrackingCategories__preprocessHook;
35use MediaWiki\Html\Html;
36use MediaWiki\Linker\LinkRenderer;
37use MediaWiki\Linker\LinkTarget;
38use MediaWiki\Output\OutputPage;
39use MediaWiki\Parser\Sanitizer;
40use MediaWiki\ResourceLoader as RL;
41use MediaWiki\SpecialPage\SpecialPage;
42use MediaWiki\Title\Title;
43use Parser;
44use PPFrame;
45use RequestContext;
46use Skin;
47use Wikimedia\Rdbms\IConnectionProvider;
48use Wikimedia\Rdbms\IResultWrapper;
49
50/**
51 * Hooks for the CategoryTree extension, an AJAX based gadget
52 * to display the category structure of a wiki
53 *
54 * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
55 */
56class Hooks implements
57    SpecialTrackingCategories__preprocessHook,
58    SpecialTrackingCategories__generateCatLinkHook,
59    SkinBuildSidebarHook,
60    ParserFirstCallInitHook,
61    OutputPageMakeCategoryLinksHook,
62    CategoryViewer__doCategoryQueryHook,
63    CategoryViewer__generateLinkHook
64{
65
66    /** @var CategoryCache */
67    private $categoryCache;
68
69    /** @var Config */
70    private $config;
71
72    /** @var IConnectionProvider */
73    private IConnectionProvider $dbProvider;
74
75    /** @var LinkRenderer */
76    private $linkRenderer;
77
78    /**
79     * @param CategoryCache $categoryCache
80     * @param Config $config
81     * @param IConnectionProvider $dbProvider
82     * @param LinkRenderer $linkRenderer
83     */
84    public function __construct(
85        CategoryCache $categoryCache,
86        Config $config,
87        IConnectionProvider $dbProvider,
88        LinkRenderer $linkRenderer
89    ) {
90        $this->categoryCache = $categoryCache;
91        $this->config = $config;
92        $this->dbProvider = $dbProvider;
93        $this->linkRenderer = $linkRenderer;
94    }
95
96    /**
97     * @param Parser $parser
98     */
99    public function onParserFirstCallInit( $parser ) {
100        if ( !$this->config->get( 'CategoryTreeAllowTag' ) ) {
101            return;
102        }
103        $parser->setHook( 'categorytree', [ $this, 'parserHook' ] );
104        $parser->setFunctionHook( 'categorytree', [ $this, 'parserFunction' ] );
105    }
106
107    /**
108     * Entry point for the {{#categorytree}} tag parser function.
109     * This is a wrapper around Hooks::parserHook
110     * @param Parser $parser
111     * @param string ...$params
112     * @return array|string
113     */
114    public function parserFunction( Parser $parser, ...$params ) {
115        // first user-supplied parameter must be category name
116        if ( !$params ) {
117            // no category specified, return nothing
118            return '';
119        }
120        $cat = array_shift( $params );
121
122        // build associative arguments from flat parameter list
123        $argv = [];
124        foreach ( $params as $p ) {
125            if ( preg_match( '/^\s*(\S.*?)\s*=\s*(.*?)\s*$/', $p, $m ) ) {
126                $k = $m[1];
127                // strip any quotes enclosing the value
128                $v = preg_replace( '/^"\s*(.*?)\s*"$/', '$1', $m[2] );
129            } else {
130                $k = trim( $p );
131                $v = true;
132            }
133
134            $argv[$k] = $v;
135        }
136
137        if ( $parser->getOutputType() === Parser::OT_PREPROCESS ) {
138            return Html::rawElement( 'categorytree', $argv, $cat );
139        } else {
140            // now handle just like a <categorytree> tag
141            $html = $this->parserHook( $cat, $argv, $parser );
142            return [ $html, 'noparse' => true, 'isHTML' => true ];
143        }
144    }
145
146    /**
147     * Obtain a category sidebar link based on config
148     * @return bool|string of link
149     */
150    private function getCategorySidebarBox() {
151        if ( !$this->config->get( 'CategoryTreeSidebarRoot' ) ) {
152            return false;
153        }
154        return $this->parserHook(
155            $this->config->get( 'CategoryTreeSidebarRoot' ),
156            $this->config->get( 'CategoryTreeSidebarOptions' )
157        );
158    }
159
160    /**
161     * Hook implementation for injecting a category tree into the sidebar.
162     * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name.
163     * @param Skin $skin
164     * @param array &$sidebar
165     */
166    public function onSkinBuildSidebar( $skin, &$sidebar ) {
167        $html = $this->getCategorySidebarBox();
168        if ( $html ) {
169            $sidebar['categorytree-portlet'] = [];
170            CategoryTree::setHeaders( $skin->getOutput() );
171        }
172    }
173
174    /**
175     * Hook implementation for injecting a category tree link into the sidebar.
176     * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name.
177     * @param Skin $skin
178     * @param string $portlet
179     * @param string &$html
180     */
181    public function onSkinAfterPortlet( $skin, $portlet, &$html ) {
182        if ( $portlet === 'categorytree-portlet' ) {
183            $box = $this->getCategorySidebarBox();
184            if ( $box ) {
185                $html .= $box;
186            }
187        }
188    }
189
190    /**
191     * Entry point for the <categorytree> tag parser hook.
192     * This loads CategoryTreeFunctions.php and calls CategoryTree::getTag()
193     * @param string $cat
194     * @param array $argv
195     * @param Parser|null $parser
196     * @param PPFrame|null $frame
197     * @param bool $allowMissing
198     * @return bool|string
199     */
200    public function parserHook(
201        $cat,
202        array $argv,
203        Parser $parser = null,
204        PPFrame $frame = null,
205        $allowMissing = false
206    ) {
207        if ( $parser ) {
208            $parserOutput = $parser->getOutput();
209            $parserOutput->addModuleStyles( [ 'ext.categoryTree.styles' ] );
210            $parserOutput->addModules( [ 'ext.categoryTree' ] );
211        }
212
213        $ct = new CategoryTree( $argv, $this->config, $this->dbProvider, $this->linkRenderer );
214
215        $attr = Sanitizer::validateTagAttributes( $argv, 'div' );
216
217        $hideroot = isset( $argv['hideroot'] )
218            ? OptionManager::decodeBoolean( $argv['hideroot'] ) : null;
219        $onlyroot = isset( $argv['onlyroot'] )
220            ? OptionManager::decodeBoolean( $argv['onlyroot'] ) : null;
221        $depthArg = isset( $argv['depth'] ) ? (int)$argv['depth'] : null;
222
223        $depth = $ct->optionManager->capDepth( $depthArg );
224        if ( $onlyroot ) {
225            $depth = 0;
226        }
227
228        return $ct->getTag( $parser, $cat, $hideroot, $attr, $depth, $allowMissing );
229    }
230
231    /**
232     * OutputPageMakeCategoryLinks hook, override category links
233     * @param OutputPage $out
234     * @param array $categories
235     * @param array &$links
236     * @return bool
237     */
238    public function onOutputPageMakeCategoryLinks( $out, $categories, &$links ) {
239        if ( !$this->config->get( 'CategoryTreeHijackPageCategories' ) ) {
240            // Not enabled, don't do anything
241            return true;
242        }
243
244        $options = $this->config->get( 'CategoryTreePageCategoryOptions' );
245        foreach ( $categories as $category => $type ) {
246            $links[$type][] = $this->parserHook( $category, $options, null, null, true );
247        }
248        CategoryTree::setHeaders( $out );
249
250        return false;
251    }
252
253    /**
254     * Get exported data for the "ext.categoryTree" ResourceLoader module.
255     *
256     * @internal For use in extension.json only.
257     * @param RL\Context $context
258     * @param Config $config
259     * @return array Data to be serialised as data.json
260     */
261    public static function getDataForJs( RL\Context $context, Config $config ) {
262        // Look, this is pretty bad but CategoryTree is just whacky, it needs to be rewritten
263        $optionManager = new OptionManager( $config->get( 'CategoryTreeCategoryPageOptions' ), $config );
264
265        return [
266            'defaultCtOptions' => $optionManager->getOptionsAsJsStructure(),
267        ];
268    }
269
270    /**
271     * Hook handler for the SpecialTrackingCategories::preprocess hook
272     * @param SpecialPage $specialPage SpecialTrackingCategories object
273     * @param array $trackingCategories [ 'msg' => LinkTarget, 'cats' => LinkTarget[] ]
274     * @phan-param array<string,array{msg:LinkTarget,cats:LinkTarget[]}> $trackingCategories
275     */
276    public function onSpecialTrackingCategories__preprocess(
277        $specialPage,
278        $trackingCategories
279    ) {
280        $categoryTargets = [];
281        foreach ( $trackingCategories as $data ) {
282            foreach ( $data['cats'] as $catTitle ) {
283                $categoryTargets[] = $catTitle;
284            }
285        }
286        $this->categoryCache->doQuery( $categoryTargets );
287    }
288
289    /**
290     * Hook handler for the SpecialTrackingCategories::generateCatLink hook
291     * @param SpecialPage $specialPage SpecialTrackingCategories object
292     * @param LinkTarget $catTitle LinkTarget object of the linked category
293     * @param string &$html Result html
294     */
295    public function onSpecialTrackingCategories__generateCatLink( $specialPage,
296        $catTitle, &$html
297    ) {
298        $cat = $this->categoryCache->getCategory( $catTitle );
299
300        $html .= CategoryTree::createCountString( $specialPage->getContext(), $cat, 0 );
301    }
302
303    /**
304     * @param string $type
305     * @param IResultWrapper $res
306     */
307    public function onCategoryViewer__doCategoryQuery( $type, $res ) {
308        if ( $type === 'subcat' && $res ) {
309            $this->categoryCache->fillFromQuery( $res );
310            CategoryTree::setHeaders( RequestContext::getMain()->getOutput() );
311        }
312    }
313
314    /**
315     * @param string $type
316     * @param Title $title
317     * @param string $html
318     * @param string &$link
319     * @return bool
320     */
321    public function onCategoryViewer__generateLink( $type, $title, $html, &$link ) {
322        if ( $type !== 'subcat' || $link !== null ) {
323            return true;
324        }
325
326        $request = RequestContext::getMain()->getRequest();
327        if ( $request->getCheck( 'notree' ) ) {
328            return true;
329        }
330
331        $options = $this->config->get( 'CategoryTreeCategoryPageOptions' );
332        $mode = $request->getRawVal( 'mode' );
333        if ( $mode !== null ) {
334            $options['mode'] = $mode;
335        }
336        $tree = new CategoryTree( $options, $this->config, $this->dbProvider, $this->linkRenderer );
337
338        $cat = $this->categoryCache->getCategory( $title );
339
340        $link = $tree->renderNodeInfo( $title, $cat );
341        return false;
342    }
343}