Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
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 / 103
0.00% covered (danger)
0.00%
0 / 13
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
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 / 28
0.00% covered (danger)
0.00%
0 / 1
90
 onOutputPageRenderCategoryLink
0.00% covered (danger)
0.00%
0 / 10
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 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onCategoryViewer__generateLink
0.00% covered (danger)
0.00%
0 / 14
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\Context\RequestContext;
29use MediaWiki\Hook\CategoryViewer__doCategoryQueryHook;
30use MediaWiki\Hook\CategoryViewer__generateLinkHook;
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\Hook\OutputPageRenderCategoryLinkHook;
39use MediaWiki\Output\OutputPage;
40use MediaWiki\Page\ProperPageIdentity;
41use MediaWiki\Parser\Parser;
42use MediaWiki\Parser\Sanitizer;
43use MediaWiki\ResourceLoader as RL;
44use MediaWiki\SpecialPage\SpecialPage;
45use MediaWiki\Title\Title;
46use MediaWiki\Title\TitleFormatter;
47use Skin;
48use Wikimedia\Rdbms\IConnectionProvider;
49use 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 */
57class 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}