Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.80% covered (danger)
9.80%
5 / 51
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
9.80% covered (danger)
9.80%
5 / 51
14.29% covered (danger)
14.29%
1 / 7
255.74
0.00% covered (danger)
0.00%
0 / 1
 getHelpGuiderUrl
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getTourNames
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addTour
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 onRedirectSpecialArticleRedirectParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\GuidedTour;
4
5use MediaWiki\Json\FormatJson;
6use MediaWiki\Output\Hook\BeforePageDisplayHook;
7use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook;
8use MediaWiki\Output\OutputPage;
9use MediaWiki\Registration\ExtensionRegistry;
10use MediaWiki\ResourceLoader as RL;
11use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
12use MediaWiki\ResourceLoader\ResourceLoader;
13use MediaWiki\Skin\Skin;
14use MediaWiki\SpecialPage\Hook\RedirectSpecialArticleRedirectParamsHook;
15use MediaWiki\Title\Title;
16
17/**
18 * Use a hook to include this extension's functionality in pages
19 * (if the page is called with a tour)
20 *
21 * @author Terry Chay tchay@wikimedia.org
22 * @author Matthew Flaschen mflaschen@wikimedia.org
23 * @author Luke Welling lwelling@wikimedia.org
24 */
25class Hooks implements
26    BeforePageDisplayHook,
27    ResourceLoaderRegisterModulesHook,
28    RedirectSpecialArticleRedirectParamsHook,
29    MakeGlobalVariablesScriptHook
30{
31    // Tour cookie name.  It will be prefixed automatically.
32    public const COOKIE_NAME = '-mw-tour';
33
34    private const TOUR_PARAM = 'tour';
35
36    /**
37     * ResourceLoader callback that adds the page name of a GuidedTour local
38     * documentation page, to demonstrate showing tour content from pages. This is
39     * a hack pending forcontent messages: https://phabricator.wikimedia.org/T27349
40     *
41     * If the page does not exist, it will not be set.
42     *
43     * @param RL\Context $context
44     * @return array
45     */
46    public static function getHelpGuiderUrl( RL\Context $context ) {
47        $data = [];
48
49        $pageName = $context->msg( 'guidedtour-help-guider-url' )
50            ->inContentLanguage()->plain();
51        $pageTitle = Title::newFromText( $pageName );
52        if ( $pageTitle !== null && $pageTitle->exists() ) {
53            $data['pageName'] = $pageName;
54        }
55
56        return $data;
57    }
58
59    /**
60     * Parses tour cookie, returning an array of tour names (empty if there is no
61     * cookie or the format is invalid)
62     *
63     * Example cookie.  This happens to have multiple tours, but cookies with any
64     * number of tours are accepted:
65     *
66     * {
67     *     version: 1,
68     *     tours: {
69     *         firsttour: {
70     *             step: 4
71     *         },
72     *         secondtour: {
73     *             step: 2
74     *         },
75     *         thirdtour: {
76     *             step: 3,
77     *             firstArticleId: 38333
78     *         }
79     *     }
80     * }
81     *
82     * This only supports new-style cookies.  Old cookies will be converted on the
83     * client-side, then the tour module will be loaded.
84     *
85     * @param string $cookieValue cookie value
86     *
87     * @return array array of tour names, empty if no cookie or cookie is invalid
88     */
89    public static function getTourNames( $cookieValue ) {
90        if ( $cookieValue !== null ) {
91            $parsed = FormatJson::decode( $cookieValue, true );
92            if ( isset( $parsed['tours'] ) ) {
93                return array_keys( $parsed['tours'] );
94            }
95        }
96
97        return [];
98    }
99
100    /**
101     * Adds a built-in or wiki tour.
102     *
103     * If user JS is disallowed on this page, it does nothing.
104     *
105     * If the built-in one exists as a module, it will add that.
106     *
107     * Otherwise, it will add the general guided tour module, which will take care of
108     * loading the on-wiki tour
109     *
110     * It will not attempt to add tours that violate the naming rules.
111     *
112     * @param OutputPage $out output page
113     * @param string $tourName the tour name, such as 'gettingstarted'
114     *
115     * @return bool true if a module was added, false otherwise
116     */
117    public static function addTour( $out, $tourName ) {
118        $isUserJsAllowed = $out->getAllowedModules( RL\Module::TYPE_SCRIPTS )
119            >= RL\Module::ORIGIN_USER_INDIVIDUAL;
120
121        // Exclude '-' because MediaWiki message keys use it as a separator after the tourname.
122        // Exclude '.' because module names use it as a separator.
123        // "User JS" refers to on-wiki JavaScript. In theory we could still add
124        // extension-defined tours, but it's more conservative not to.
125        if ( $isUserJsAllowed && $tourName !== null && strpbrk( $tourName, '-.' ) === false ) {
126            $tourModuleName = "ext.guidedTour.tour.$tourName";
127            if ( $out->getResourceLoader()->getModule( $tourModuleName ) ) {
128                // Add the tour itself for extension-defined tours.
129                $out->addModules( $tourModuleName );
130            } else {
131                /*
132                  Otherwise, add the main module, which attempts to import an
133                  on-wiki tour.
134                */
135                $out->addModules( 'ext.guidedTour' );
136            }
137            return true;
138        }
139
140        return false;
141    }
142
143    /**
144     * Handler for BeforePageDisplay hook.
145     *
146     * Adds a tour-related module if possible.
147     *
148     * If the query parameter is set, it will use that.
149     * Otherwise, it will use the cookie if that exists.
150     *
151     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
152     *
153     * @param OutputPage $out OutputPage object
154     * @param Skin $skin Skin being used.
155     */
156    public function onBeforePageDisplay( $out, $skin ): void {
157        // test for tour enabled in url first
158        $request = $out->getRequest();
159        $queryTourName = $request->getVal( self::TOUR_PARAM );
160        if ( $queryTourName !== null ) {
161            self::addTour( $out, $queryTourName );
162        } else {
163            $cookieValue = $request->getCookie( self::COOKIE_NAME );
164            $tours = self::getTourNames( $cookieValue );
165            foreach ( $tours as $tourName ) {
166                self::addTour( $out, $tourName );
167            }
168        }
169    }
170
171    /**
172     * Registers VisualEditor tour if VE is installed
173     *
174     * @param ResourceLoader $resourceLoader
175     */
176    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
177        if ( ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) ) {
178            $resourceLoader->register(
179                'ext.guidedTour.tour.firsteditve',
180                [
181                    'scripts' => 'tours/firsteditve.js',
182                    'localBasePath' => __DIR__ . '/../modules',
183                    'remoteExtPath' => 'GuidedTour/modules',
184                    'dependencies' => 'ext.guidedTour',
185                    'messages' => [
186                        'editsection',
187                        'publishchanges',
188                        'guidedtour-tour-firstedit-edit-page-title',
189                        'guidedtour-tour-firsteditve-edit-page-description',
190                        'guidedtour-tour-firstedit-edit-section-title',
191                        'guidedtour-tour-firsteditve-edit-section-description',
192                        'guidedtour-tour-firstedit-save-title',
193                        'guidedtour-tour-firsteditve-save-description',
194                    ],
195                ]
196            );
197        }
198    }
199
200    /**
201     * @param array &$redirectParams
202     */
203    public function onRedirectSpecialArticleRedirectParams( &$redirectParams ) {
204        array_push( $redirectParams, self::TOUR_PARAM, 'step' );
205    }
206
207    /**
208     * @param array &$vars
209     * @param OutputPage $out
210     */
211    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
212        GuidedTourLauncher::onMakeGlobalVariablesScript( $vars, $out );
213    }
214}