Mattflaschen has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/378827 )

Change subject: Allow directly launching tour from server without ?tour= or 
cookies
......................................................................

Allow directly launching tour from server without ?tour= or cookies

Bug: T167262
Change-Id: I313edff5efb51724c75bd95351dab402154f28c6
---
M GuidedTourLauncher.php
M extension.json
M modules/ext.guidedTour.lib.internal.js
M modules/ext.guidedTour.lib/ext.guidedTour.lib.main.js
M tests/phpunit/GuidedTourLauncherTest.php
5 files changed, 289 insertions(+), 133 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/GuidedTour 
refs/changes/27/378827/1

diff --git a/GuidedTourLauncher.php b/GuidedTourLauncher.php
index 6ce257c..79d9096 100644
--- a/GuidedTourLauncher.php
+++ b/GuidedTourLauncher.php
@@ -5,6 +5,44 @@
  */
 class GuidedTourLauncher {
        /**
+        * State used to tell the client to directly launch tours using a 
client-side $wg
+        *
+        * @var array|null $directLaunchState
+        */
+       protected static $directLaunchState = null;
+
+       // This matches the format used on the client-side (e.g.
+       // mw.guidedTour.internal.getInitialUserStateObject,
+       // mw.guidedTour.launchTourFromUserState, etc.
+       /**
+        * Get new state from old state.  The state describes the user's 
progress
+        * in the tour, and which step they are expected to see next.
+        *
+        * @param {array|null} $oldState Previous state
+        * @param {string} $tourName Tour name
+        * @param {string} $step Step to start at
+        * @return {array} New state
+        */
+       public static function getNewState( $oldState, $tourName, $step ) {
+               $newState = $oldState;
+
+               if ( $newState === null ) {
+                       $newState = [];
+               }
+
+               $newState = array_replace_recursive( $newState, [
+                       'version' => 1,
+                       'tours' => [
+                               $tourName => [
+                                       'step' => $step,
+                               ],
+                       ]
+               ] );
+
+               return $newState;
+       }
+
+       /**
         * Adds a tour to the cookie
         *
         * @param {string|null} $oldCookieValue Previous value of cookie
@@ -17,23 +55,41 @@
                        $oldCookieValue = '{}';
                }
 
-               $newCookie = FormatJson::decode( $oldCookieValue, true );
-               if ( $newCookie === null ) {
-                       $newCookie = [];
+               $oldState = FormatJson::decode( $oldCookieValue, true );
+               if ( $oldState === null ) {
+                       $oldState = [];
                }
 
-               $newCookie = array_replace_recursive( $newCookie, [
-                       'version' => 1,
-                       'tours' => [
-                               $tourName => [
-                                       'step' => $step,
-                               ],
-                       ]
-               ] );
+               $newState = self::getNewState( $oldState, $tourName, $step );
 
-               return FormatJson::encode( $newCookie );
+               return FormatJson::encode( $newState );
        }
 
+       public static function launchTour( $tourName, $step ) {
+               global $wgOut;
+
+               self::$directLaunchState = self::getNewState(
+                       self::$directLaunchState,
+                       $tourName,
+                       $step
+               );
+
+               GuidedTourHooks::addTour( $wgOut, $tourName );
+       }
+
+       /**
+        * Adds a client-side $wg variable to control tour launch
+        *
+        * @param array &$vars Array of request-specific JavaScript config 
variables
+        * @param OutputPage $out
+        */
+       public static function addLaunchVariable( array &$vars, OutputPage $out 
) {
+               if ( self::$directLaunchState !== null ) {
+                       $vars['wgGuidedTourLaunchState'] = 
self::$directLaunchState;
+               }
+       }
+
+
        /**
         * Sets a tour to auto-launch on this view using a cookie.
         */
diff --git a/extension.json b/extension.json
index 7aa3ee0..e1545fa 100644
--- a/extension.json
+++ b/extension.json
@@ -166,6 +166,9 @@
                ],
                "EventLoggingRegisterSchemas": [
                        "GuidedTourHooks::addEventLogging"
+               ],
+               "MakeGlobalVariablesScript": [
+                       "GuidedTourLauncher::addLaunchVariable"
                ]
        },
        "manifest_version": 1
diff --git a/modules/ext.guidedTour.lib.internal.js 
b/modules/ext.guidedTour.lib.internal.js
index 8d2f86d..7d4ae08 100644
--- a/modules/ext.guidedTour.lib.internal.js
+++ b/modules/ext.guidedTour.lib.internal.js
@@ -68,6 +68,7 @@
                        return dfd.promise();
                },
 
+               // This should match GuidedTourLauncher::getNewState
                /**
                 * Returns object used for an initial user state, optionally 
populating it with one
                 *  tour's data.
@@ -216,7 +217,8 @@
                },
 
                /**
-                * Parses user state (as used in the cookie), which is passed 
in as JSON
+                * Parses user state (as used in the cookie and 
wgGuidedTourLaunchState), which
+                * is passed in as JSON
                 *
                 * @param {string} userStateJson User state, as JSON
                 *
diff --git a/modules/ext.guidedTour.lib/ext.guidedTour.lib.main.js 
b/modules/ext.guidedTour.lib/ext.guidedTour.lib.main.js
index 5b953cd..6b56db4 100644
--- a/modules/ext.guidedTour.lib/ext.guidedTour.lib.main.js
+++ b/modules/ext.guidedTour.lib/ext.guidedTour.lib.main.js
@@ -41,7 +41,7 @@
         *  invalid, returns a skeleton state object from
         *  mw.guidedTour.internal#getInitialUserStateObject
         */
-       function getUserState() {
+       function getCookieState() {
                var cookieValue, parsed;
                cookieValue = mw.cookie.get( cookieName );
                parsed = internal.parseUserState( cookieValue );
@@ -50,6 +50,31 @@
                } else {
                        return internal.getInitialUserStateObject();
                }
+       }
+
+       /**
+        * Returns the current combined user state (cookie state and 
server-launched state)
+        * Basically, this acts like the cookie state, except the server can 
specify
+        * tours that take precedence via a $wg.
+        *
+        * @private
+        *
+        * @return {Object} combined state object.  If there is none, or the 
format was
+        * invalid, returns a skeleton state object from
+        * mw.guidedTour.internal#getInitialUserStateObject
+        */
+       function getUserState() {
+               var
+                       cookieState = getCookieState(),
+                       serverState = mw.config.get( 'wgGuidedTourLaunchState' 
),
+                       state = cookieState;
+
+               if ( serverState !== null ) {
+                       state = $.extend( true, state, serverState );
+               }
+
+               return state;
+
        }
 
        /**
@@ -62,11 +87,76 @@
         * @return {void}
         */
        function removeTourFromUserStateByName( tourName ) {
-               var parsedCookie = getUserState();
+               var parsedCookie = getCookieState();
                delete parsedCookie.tours[tourName];
                mw.cookie.set( cookieName, JSON.stringify( parsedCookie ), 
cookieParams );
        }
 
+       /**
+        * Launch tour from given user state
+        *
+        * @private
+        *
+        * @param {Object} state State that specifies the tour progress
+        *
+        * @return {boolean} Whether a tour was launched
+        */
+       function launchTourFromState( state ) {
+               var tourName, tourNames,
+                       candidateTours = [];
+
+               for ( tourName in state.tours ) {
+                       candidateTours.push( {
+                               name: tourName,
+                               step: state.tours[tourName].step
+                       } );
+               }
+
+               tourNames = $.map( candidateTours, function ( el ) {
+                       return el.name;
+               } );
+
+               internal.loadMultipleTours( tourNames )
+                       .always( function () {
+                               var tourName, max, currentStart;
+
+                               // This value is before 1970, but is a simple 
way
+                               // to ensure the comparison below always works.
+                               max = {
+                                       startTime: -1
+                               };
+
+                               // Not all the tours in the cookie necessarily
+                               // loaded successfully, but the defined tours 
did.
+                               // So we make sure it is defined and in the user
+                               // state.
+                               for ( tourName in internal.definedTours ) {
+                                       if ( state.tours[tourName] !== 
undefined &&
+                                            gt.shouldShowTour( {
+                                               tourName: tourName,
+                                               userState: state,
+                                               pageName: mw.config.get( 
'wgPageName' ),
+                                               articleId: mw.config.get( 
'wgArticleId' ),
+                                               condition: 
internal.definedTours[tourName].showConditionally
+                                       } ) ) {
+                                               currentStart = 
state.tours[tourName].startTime || 0;
+                                               if ( currentStart > 
max.startTime ) {
+                                                       max = {
+                                                               name: tourName,
+                                                               step: 
state.tours[tourName].step,
+                                                               startTime: 
currentStart
+                                                       };
+                                               }
+                                       }
+                               }
+
+                               if ( max.name !== undefined ) {
+                                       // Launch the most recently started tour
+                                       // that meets the conditions.
+                                       gt.launchTour( max.name, gt.makeTourId( 
max ) );
+                               }
+                       } );
+       }
 
        // TODO (mattflaschen, 2013-07-10): Known issue: This runs too early on 
a direct
        // visit to a veaction=edit page.  This probably affects other 
JS-generated
@@ -358,67 +448,14 @@
                },
 
                /**
-                * Attempts to launch a tour from the user state (cookie)
+                * Attempts to launch a tour from combined user state (cookie + 
tours launched
+                * directly by server)
                 *
                 * @return {boolean} Whether a tour was launched
                 */
                launchTourFromUserState: function () {
-                       var tourName, tourNames,
-                               userState, candidateTours = [];
-
-
-                       userState = getUserState();
-
-                       for ( tourName in userState.tours ) {
-                               candidateTours.push( {
-                                       name: tourName,
-                                       step: userState.tours[tourName].step
-                               } );
-                       }
-
-                       tourNames = $.map( candidateTours, function ( el ) {
-                               return el.name;
-                       } );
-                       internal.loadMultipleTours( tourNames )
-                               .always( function () {
-                                       var tourName, max, currentStart;
-
-                                       // This value is before 1970, but is a 
simple way
-                                       // to ensure the comparison below 
always works.
-                                       max = {
-                                               startTime: -1
-                                       };
-
-                                       // Not all the tours in the cookie 
necessarily
-                                       // loaded successfully, but the defined 
tours did.
-                                       // So we make sure it is defined and in 
the user
-                                       // state.
-                                       for ( tourName in internal.definedTours 
) {
-                                               if ( userState.tours[tourName] 
!== undefined &&
-                                                    gt.shouldShowTour( {
-                                                       tourName: tourName,
-                                                       userState: userState,
-                                                       pageName: 
mw.config.get( 'wgPageName' ),
-                                                       articleId: 
mw.config.get( 'wgArticleId' ),
-                                                       condition: 
internal.definedTours[tourName].showConditionally
-                                               } ) ) {
-                                                       currentStart = 
userState.tours[tourName].startTime || 0;
-                                                       if ( currentStart > 
max.startTime ) {
-                                                               max = {
-                                                                       name: 
tourName,
-                                                                       step: 
userState.tours[tourName].step,
-                                                                       
startTime: currentStart
-                                                               };
-                                                       }
-                                               }
-                                       }
-
-                                       if ( max.name !== undefined ) {
-                                               // Launch the most recently 
started tour
-                                               // that meets the conditions.
-                                               gt.launchTour( max.name, 
gt.makeTourId( max ) );
-                                       }
-                               } );
+                       var state = getUserState();
+                       return launchTourFromState( state );
                },
 
                /**
@@ -438,7 +475,6 @@
                        // Tour is either in the query string or cookie (prefer 
query string)
 
                        if ( this.launchTourFromQueryString() ) {
-
                                return;
                        }
 
@@ -707,7 +743,7 @@
 
                        userState = getUserState();
                        if ( ( step === 0 ) && userState.tours[tourName] !== 
undefined ) {
-                               // start from cookie position
+                               // start from user state position
                                showTour( tourName, gt.makeTourId( {
                                        name: tourName,
                                        step: userState.tours[tourName].step
@@ -741,7 +777,7 @@
                },
 
                /**
-                * Updates a single tour in the user state.  The tour must 
already be loaded.
+                * Updates a single tour in the user cookie state.  The tour 
must already be loaded.
                 *
                 * @private
                 *
@@ -754,34 +790,34 @@
                 * @return {void}
                 */
                updateUserStateForTour: function ( args ) {
-                       var userState = getUserState(), tourName, tourSpec, 
articleId, pageName,
-                       cookieValue;
+                       var cookieState = getCookieState(), tourName, tourSpec, 
articleId, pageName,
+                               cookieValue;
 
                        tourName = args.tourInfo.name;
                        // It should be defined, except when wasShown is false.
                        tourSpec = internal.definedTours[tourName] || {};
 
                        // Ensure there's a sub-object for this tour
-                       if ( userState.tours[tourName] === undefined ) {
-                               userState.tours[tourName] = {};
+                       if ( cookieState.tours[tourName] === undefined ) {
+                               cookieState.tours[tourName] = {};
 
-                               userState.tours[tourName].startTime = new 
Date().getTime();
+                               cookieState.tours[tourName].startTime = new 
Date().getTime();
                        }
 
                        if ( args.wasShown && tourSpec.showConditionally === 
'stickToFirstPage' &&
-                            userState.tours[tourName].firstArticleId === 
undefined &&
-                            userState.tours[tourName].firstSpecialPageName === 
undefined ) {
+                            cookieState.tours[tourName].firstArticleId === 
undefined &&
+                            cookieState.tours[tourName].firstSpecialPageName 
=== undefined ) {
                                     articleId = mw.config.get( 'wgArticleId' );
                                     if ( articleId !== 0 ) {
-                                            
userState.tours[tourName].firstArticleId = articleId;
+                                            
cookieState.tours[tourName].firstArticleId = articleId;
                                     } else {
                                             pageName = mw.config.get( 
'wgPageName' );
-                                            
userState.tours[tourName].firstSpecialPageName = pageName;
+                                            
cookieState.tours[tourName].firstSpecialPageName = pageName;
                                     }
                             }
 
-                       userState.tours[tourName].step = String( 
args.tourInfo.step );
-                       cookieValue = JSON.stringify( userState );
+                       cookieState.tours[tourName].step = String( 
args.tourInfo.step );
+                       cookieValue = JSON.stringify( cookieState );
                        mw.cookie.set( cookieName, cookieValue, cookieParams );
                },
 
@@ -821,20 +857,20 @@
                 * @throws {mw.guidedTour.TourDefinitionError} On invalid 
conditions
                 */
                shouldShowTour: function ( args ) {
-                       var subCookie = args.userState.tours[args.tourName];
+                       var subState = args.userState.tours[args.tourName];
                        if ( args.condition !== undefined ) {
                                // TODO (mattflaschen, 2013-07-09): Allow 
having multiple
                                // conditions ANDed together in an array.
                                switch ( args.condition ) {
                                        case 'stickToFirstPage':
-                                               if ( subCookie === undefined ) {
+                                               if ( subState === undefined ) {
                                                        // Not yet shown
                                                        return true;
                                                }
-                                               if ( subCookie.firstArticleId 
!== undefined ) {
-                                                       return 
subCookie.firstArticleId === args.articleId;
-                                               } else if ( 
subCookie.firstSpecialPageName !== undefined ) {
-                                                       return 
subCookie.firstSpecialPageName === args.pageName;
+                                               if ( subState.firstArticleId 
!== undefined ) {
+                                                       return 
subState.firstArticleId === args.articleId;
+                                               } else if ( 
subState.firstSpecialPageName !== undefined ) {
+                                                       return 
subState.firstSpecialPageName === args.pageName;
                                                }
                                                break;
                                        case 'wikitext':
diff --git a/tests/phpunit/GuidedTourLauncherTest.php 
b/tests/phpunit/GuidedTourLauncherTest.php
index 511528b..d38c774 100644
--- a/tests/phpunit/GuidedTourLauncherTest.php
+++ b/tests/phpunit/GuidedTourLauncherTest.php
@@ -1,9 +1,108 @@
 <?php
 
+use Wikimedia\TestingAccessWrapper;
+
 class GuidedTourLauncherTest extends MediaWikiTestCase {
+       protected $wrappedLauncher;
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->wrappedLauncher = TestingAccessWrapper::newFromClass( 
'GuidedTourLauncher' );
+       }
+
+       /**
+        * @dataProvider getNewStateProvider
+        */
+       public function testGetNewState( $oldStateValue, $tourName, $step, 
$expectedNewStateValue ) {
+               $newStateValue = $this->wrappedLauncher->getNewState( 
$oldStateValue, $tourName, $step );
+               $this->assertSame(
+                       $expectedNewStateValue,
+                       $newStateValue
+               );
+       }
+
+       public function getNewStateProvider() {
+               $simpleExpectedState = [
+                       'version' => 1,
+                       'tours' => [
+                               'example' => [
+                                       'step' => 'bar',
+                               ],
+                       ],
+               ];
+
+               return [
+                       [
+                               [
+                                       'version' => 1,
+                                       'tours' => [
+                                               'example' => [
+                                                       'step' => 'foo',
+                                                       'firstArticleId' => 123,
+                                               ],
+                                       ],
+                               ],
+                               'example',
+                               'bar',
+                               [
+                                       'version' => 1,
+                                       'tours' => [
+                                               'example' => [
+                                                       'step' => 'bar',
+                                                       'firstArticleId' => 123,
+                                               ],
+                                       ]
+                               ]
+                       ],
+
+                       [
+                               null,
+                               'example',
+                               'bar',
+                               $simpleExpectedState,
+                       ],
+
+                       [
+                               [],
+                               'example',
+                               'bar',
+                               $simpleExpectedState
+                       ],
+
+                       [
+                               [
+                                       'version' => 1,
+                                       'tours' => [
+                                               'someOtherTour' => [
+                                                       'step' => 'baz',
+                                                       'firstSpecialPageName' 
=> 'Special:Watchlist',
+                                               ],
+                                       ]
+                               ],
+                               'example',
+                               'bar',
+                               [
+                                       'version' => 1,
+                                       'tours' => [
+                                               'someOtherTour' => [
+                                                       'step' => 'baz',
+                                                       'firstSpecialPageName' 
=> 'Special:Watchlist',
+                                               ],
+                                               'example' => [
+                                                       'step' => 'bar',
+                                               ],
+                                       ]
+                               ],
+                       ],
+               ];
+       }
+
+       // This should mostly be covered by testGetNewState; these are a couple 
tests to
+       // handle edge cases.
        /**
         * @dataProvider getNewCookieProvider
-        */
+         */
        public function testGetNewCookie( $oldCookieValue, $tourName, $step, 
$expectedNewCookieValue ) {
                $newCookieValue = GuidedTourLauncher::getNewCookie( 
$oldCookieValue, $tourName, $step );
                $this->assertSame(
@@ -47,50 +146,10 @@
                        ],
 
                        [
-                               null,
-                               'example',
-                               'bar',
-                               $simpleExpectedCookieString,
-                       ],
-
-                       [
                                '',
                                'example',
                                'bar',
                                $simpleExpectedCookieString
-                       ],
-
-                       [
-                               '{}',
-                               'example',
-                               'bar',
-                               $simpleExpectedCookieString
-                       ],
-
-                       [
-                               FormatJson::encode( [
-                                       'version' => 1,
-                                       'tours' => [
-                                               'someOtherTour' => [
-                                                       'step' => 'baz',
-                                                       'firstSpecialPageName' 
=> 'Special:Watchlist',
-                                               ],
-                                       ]
-                               ] ),
-                               'example',
-                               'bar',
-                               FormatJson::encode( [
-                                       'version' => 1,
-                                       'tours' => [
-                                               'someOtherTour' => [
-                                                       'step' => 'baz',
-                                                       'firstSpecialPageName' 
=> 'Special:Watchlist',
-                                               ],
-                                               'example' => [
-                                                       'step' => 'bar',
-                                               ],
-                                       ]
-                               ] ),
                        ],
                ];
        }

-- 
To view, visit https://gerrit.wikimedia.org/r/378827
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I313edff5efb51724c75bd95351dab402154f28c6
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/GuidedTour
Gerrit-Branch: master
Gerrit-Owner: Mattflaschen <mflasc...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to