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 <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits