Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 8
650
0.00% covered (danger)
0.00%
0 / 1
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onFuncIsIn
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 onParserAfterTidy
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 completeImplicitIsIn
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 onOutputPageParserOutput
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 makeTrail
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 getParentRegion
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getParserOutput
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\GeoCrumbs;
4
5use MediaWiki\Hook\ParserAfterTidyHook;
6use MediaWiki\Hook\ParserFirstCallInitHook;
7use MediaWiki\Html\Html;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Output\Hook\OutputPageParserOutputHook;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\Parser\Parser;
12use MediaWiki\Parser\ParserOutput;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15
16class Hooks implements
17    ParserFirstCallInitHook,
18    ParserAfterTidyHook,
19    OutputPageParserOutputHook
20{
21
22    /**
23     * @param Parser $parser
24     */
25    public function onParserFirstCallInit( $parser ) {
26        $parser->setFunctionHook( 'isin', [ self::class, 'onFuncIsIn' ] );
27    }
28
29    /**
30     * @param Parser $parser
31     * @param string $article
32     * @return string
33     */
34    public static function onFuncIsIn( Parser $parser, $article ) {
35        // Tribute to Evan!
36        $article = urldecode( $article );
37
38        $page = $parser->getPage();
39        $title = Title::newFromText( $article, $page ? $page->getNamespace() : NS_MAIN );
40        if ( $title ) {
41            $article = [ 'id' => $title->getArticleID() ];
42            $parser->getOutput()->setExtensionData( 'GeoCrumbIsIn', $article );
43        }
44
45        return '';
46    }
47
48    /**
49     * Assumes that mRevisionId is only set for primary wiki text when a new revision is saved.
50     * We need this in order to save IsIn info appropriately.
51     * We could add this at onSkinTemplateOutputPageBeforeExec too, but then it won't be in
52     * ParserCache, available for other articles.
53     *
54     * @param Parser $parser
55     * @param string &$text
56     */
57    public function onParserAfterTidy( $parser, &$text ) {
58        $page = $parser->getPage();
59        if ( $page && MediaWikiServices::getInstance()->getNamespaceInfo()->isContent( $page->getNamespace() ) ) {
60            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable The cast cannot return null here
61            self::completeImplicitIsIn( $parser->getOutput(), Title::castFromPageReference( $page ) );
62        }
63    }
64
65    /**
66     * Generates an IsIn from title for subpages.
67     *
68     * @param ParserOutput $parserOutput
69     * @param Title $title
70     */
71    public static function completeImplicitIsIn( $parserOutput, Title $title ) {
72        // only do implicitly if none is defined through parser hook
73        $existing = $parserOutput->getExtensionData( 'GeoCrumbIsIn' );
74        if ( $existing !== null ) {
75            return;
76        }
77
78        // if we're dealing with a subpage, the parent should be in breadcrumb
79        $parent = $title->getBaseTitle();
80
81        if ( !$parent->equals( $title ) ) {
82            $article = [ 'id' => $parent->getArticleID() ];
83            $parserOutput->setExtensionData( 'GeoCrumbIsIn', $article );
84        }
85    }
86
87    /**
88     * @param OutputPage $out
89     * @param ParserOutput $parserOutput
90     */
91    public function onOutputPageParserOutput( $out, $parserOutput ): void {
92        $breadCrumbs = self::makeTrail( $out->getTitle(), $parserOutput, $out->getUser() );
93
94        if ( count( $breadCrumbs ) > 1 ) {
95            $breadCrumbs = Html::rawElement( 'span', [ 'class' => 'ext-geocrumbs-breadcrumbs' ],
96                implode( wfMessage( 'geocrumbs-delimiter' )->inContentLanguage()->escaped(), $breadCrumbs )
97            );
98            $out->addSubtitle( $breadCrumbs );
99        }
100    }
101
102    /**
103     * @param Title $title
104     * @param ParserOutput $parserOutput
105     * @param User $user
106     * @return array
107     */
108    public static function makeTrail( Title $title, ParserOutput $parserOutput, User $user ): array {
109        if ( $title->getArticleID() <= 0 ) {
110            return [];
111        }
112
113        $breadCrumbs = [];
114        $idStack = [];
115        $langConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
116            ->getLanguageConverter( $title->getPageLanguage() );
117
118        for ( $i = 0; $title && $i < 20; $i++ ) {
119            $linkText = $langConverter->convert( $title->getSubpageText() );
120
121            // do not link the final breadcrumb
122            if ( $i === 0 ) {
123                $link = $linkText;
124            } else {
125                $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
126                $link = $linkRenderer->makeLink( $title, $linkText );
127            }
128
129            // mark redirects with italics.
130            if ( $title->isRedirect() ) {
131                $link = Html::rawElement( 'i', [], $link );
132            }
133
134            // enclose the links with <bdi> tags. T318507
135            $link = Html::rawElement( 'bdi', [], $link );
136
137            array_unshift( $breadCrumbs, $link );
138
139            // avoid cyclic trails
140            if ( in_array( $title->getArticleID(), $idStack ) ) {
141                $breadCrumbs[0] = Html::rawElement( 'strike', [], $breadCrumbs[0] );
142                break;
143            }
144            $idStack[] = $title->getArticleID();
145
146            $parserOutput ??= self::getParserOutput( $title->getArticleID(), $user );
147            if ( $parserOutput ) {
148                $title = self::getParentRegion( $parserOutput );
149                // Reset so we can fetch parser output for the parent page
150                $parserOutput = null;
151            } else {
152                $title = null;
153            }
154        }
155
156        return $breadCrumbs;
157    }
158
159    /**
160     * @param ParserOutput $parserOutput
161     * @return Title|null
162     */
163    public static function getParentRegion( ParserOutput $parserOutput ) {
164        $article = $parserOutput->getExtensionData( 'GeoCrumbIsIn' );
165        if ( $article ) {
166            return Title::newFromID( $article['id'] );
167        }
168        return null;
169    }
170
171    /**
172     * @param int $pageId
173     * @param User $user
174     * @return bool|ParserOutput false if not found
175     */
176    public static function getParserOutput( int $pageId, User $user ) {
177        if ( $pageId <= 0 ) {
178            return false;
179        }
180
181        $page = MediaWikiServices::getInstance()
182            ->getWikiPageFactory()
183            ->newFromID( $pageId );
184        if ( !$page ) {
185            return false;
186        }
187        return $page->getParserOutput( $page->makeParserOptions( $user ) );
188    }
189}