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