Implement new browser compatibility checks

We now have three stages:
1. Browser feature tests. Dies silently if any fail.
2. Browser blacklist. Dies silently if match found.
3. Browser whitelist. Shows warning if no match found.

Previously we were treating the remotely generated
edit notices as if they were in an object when
in fact they were in an array - the code has been
fixed to reflect that fact.

As locally generated notices will typically require
parsed messages, we've also moved the notice rendering
to after onReady is fired.

Updated jquery.client to latest master from MediaWiki core
(needed for proper detection of Iceweasel, Android and Safari)

Bug: 38128
Change-Id: Idc5f4a23a2709264d869a91d00873c4e187bc470
This commit is contained in:
Ed Sanders 2013-05-26 16:23:03 +01:00 committed by Timo Tijhof
parent 3eccf9f682
commit aaa5ad254b
5 changed files with 136 additions and 84 deletions

View file

@ -101,6 +101,7 @@ $messages['en'] = array(
'visualeditor-aliennode-tooltip' => 'Sorry, this element cannot be edited using the VisualEditor',
'visualeditor-descriptionpagelink' => 'Project:VisualEditor',
'visualeditor-alphawarning' => 'You are using an alpha version of the [[{{MediaWiki:Visualeditor-descriptionpagelink}}|VisualEditor]]. It may be slow and make erroneous changes—please check each edit that you make.',
'visualeditor-browserwarning' => 'You are using a browser which is not officially supported by VisualEditor.',
'visualeditor-report-notice' => 'I understand that by clicking "Report problem" I will transmit my changes and my feedback, which will be stored for analysis. I agree to provide feedback in accordance with the [[{{MediaWiki:Visualeditor-report-link}}|Terms of Use]].',
'visualeditor-report-link' => 'foundation:Terms of Use',
'visualeditor-feedback-link' => 'Project:VisualEditor/Feedback',

View file

@ -44,6 +44,7 @@ class VisualEditorMessagesModule extends ResourceLoaderModule {
$msgArgs = array(
'minoredit' => array( 'minoredit' ),
'watchthis' => array( 'watchthis' ),
'visualeditor-browserwarning' => array( 'visualeditor-browserwarning' ),
'visualeditor-report-notice' => array( 'visualeditor-report-notice' ),
'missingsummary' => array( 'missingsummary' ),
);

View file

@ -6,7 +6,7 @@
/* Private Members */
/**
* @var profileCache {Object} Keyed by userAgent string,
* @var {Object} profileCache Keyed by userAgent string,
* value is the parsed $.client.profile object for that user agent.
*/
var profileCache = {};
@ -18,9 +18,9 @@
/**
* Get an object containing information about the client.
*
* @param nav {Object} An object with atleast a 'userAgent' and 'platform' key.
* @param {Object} nav An object with atleast a 'userAgent' and 'platform' key.
* Defaults to the global Navigator object.
* @return {Object} The resulting client object will be in the following format:
* @returns {Object} The resulting client object will be in the following format:
* {
* 'name': 'firefox',
* 'layout': 'gecko',
@ -50,11 +50,11 @@
// Generic version digit
x = 'x',
// Strings found in user agent strings that need to be conformed
wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3'],
wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3', 'Iceweasel'],
// Translations for conforming user agent strings
userAgentTranslations = [
// Tons of browsers lie about being something they are not
[/(Firefox|MSIE|KHTML,\slike\sGecko|Konqueror)/, ''],
[/(Firefox|MSIE|KHTML,?\slike\sGecko|Konqueror)/, ''],
// Chrome lives in the shadow of Safari still
['Chrome Safari', 'Chrome'],
// KHTML is the layout engine not the browser - LIES!
@ -70,14 +70,14 @@
// version detectection
versionPrefixes = [
'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
'lynx', 'msie', 'safari', 'ps3'
'lynx', 'msie', 'safari', 'ps3', 'android'
],
// Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)',
// Names of known browsers
names = [
'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'konqueror', 'lynx', 'msie', 'opera',
'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq'
'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq', 'android'
],
// Tanslations for conforming browser names
nameTranslations = [],
@ -173,46 +173,61 @@
},
/**
* Checks the current browser against a support map object to determine if the browser has been black-listed or
* not. If the browser was not configured specifically it is assumed to work. It is assumed that the body
* element is classified as either "ltr" or "rtl". If neither is set, "ltr" is assumed.
* Checks the current browser against a support map object.
*
* A browser map is in the following format:
* {
* 'ltr': {
* // Multiple rules with configurable operators
* 'msie': [['>=', 7], ['!=', 9]],
* // Blocked entirely
* // Match no versions
* 'iphone': false,
* // Match any version
* 'android': null
* }
*
* It can optionally be split into ltr/rtl sections:
* {
* 'ltr': {
* 'android': null,
* 'iphone': false
* },
* 'rtl': {
* // Test against a string
* 'msie': [['!==', '8.1.2.3']],
* // RTL rules do not fall through to LTR rules, you must explicity set each of them
* 'android': false,
* // rules are not inherited from ltr
* 'iphone': false
* }
* }
*
* @param map {Object} Browser support map
* @param profile {Object} (optional) a client-profile object.
* @param {Object} map Browser support map
* @param {Object} [profile] A client-profile object
* @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched, otherwise
* returns true if the browser is not found.
*
* @return Boolean true if browser known or assumed to be supported, false if blacklisted
* @returns {boolean} The current browser is in the support map
*/
test: function ( map, profile ) {
test: function ( map, profile, exactMatchOnly ) {
/*jshint evil: true */
var conditions, dir, i, op, val;
profile = $.isPlainObject( profile ) ? profile : $.client.profile();
if ( map.ltr && map.rtl ) {
dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
// Check over each browser condition to determine if we are running in a compatible client
if ( typeof map[dir] !== 'object' || map[dir][profile.name] === undefined ) {
// Unknown, so we assume it's working
return true;
map = map[dir];
}
conditions = map[dir][profile.name];
// Check over each browser condition to determine if we are running in a compatible client
if ( typeof map !== 'object' || map[profile.name] === undefined ) {
// Not found, return true if exactMatchOnly not set, false otherwise
return !exactMatchOnly;
}
conditions = map[profile.name];
if ( conditions === false ) {
// Match no versions
return false;
}
if ( conditions === null ) {
// Match all versions
return true;
}
for ( i = 0; i < conditions.length; i++ ) {
op = conditions[i][0];
val = conditions[i][1];

View file

@ -16,7 +16,15 @@
* @constructor
*/
ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() {
var currentUri = new mw.Uri( window.location.toString() );
var browserWhitelisted,
browserBlacklisted,
currentUri = new mw.Uri( window.location.toString() ),
supportsStrictMode = ( function () {
'use strict';
return this === undefined;
}() ),
// TODO: MW test runner fires before document.body exists, so we just skip it here.
supportsContentEditable = document.body && 'contentEditable' in document.body;
// Parent constructor
ve.init.mw.Target.call(
@ -61,10 +69,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() {
mw.config.get( 'wgAction' ) === 'view' &&
currentUri.query.diff === undefined
);
this.canBeActivated = (
$.client.test( ve.init.mw.ViewPageTarget.compatibility ) ||
'vewhitelist' in currentUri.query
);
this.originalDocumentTitle = document.title;
this.editSummaryByteLimit = 255;
// Tab layout.
@ -72,6 +76,15 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() {
// * replace: Re-creates #ca-edit for VisualEditor and adds #ca-editsource.
this.tabLayout = 'replace';
browserWhitelisted = (
$.client.test( ve.init.mw.ViewPageTarget.compatibility, null, true ) ||
'vewhitelist' in currentUri.query
);
browserBlacklisted = (
$.client.test( ve.init.mw.ViewPageTarget.compatibility, null, true ) ||
'veblacklist' in currentUri.query
);
// Events
this.connect( this, {
'load': 'onLoad',
@ -87,7 +100,11 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() {
} );
// Initialization
if ( this.canBeActivated ) {
if ( supportsStrictMode && supportsContentEditable && !this.browserBlacklisted ) {
if ( !this.browserWhitelisted || true ) {
// show warning
this.localNoticeMessages.push( 'visualeditor-browserwarning' );
}
if ( currentUri.query.venotify ) {
// The following messages can be used here:
// visualeditor-notification-saved
@ -128,27 +145,22 @@ ve.inheritClass( ve.init.mw.ViewPageTarget, ve.init.mw.Target );
* @property
*/
ve.init.mw.ViewPageTarget.compatibility = {
// Left-to-right languages
ltr: {
msie: [['>=', 9]],
firefox: [['>=', 11]],
iceweasel: [['>=', 10]],
safari: [['>=', 5]],
chrome: [['>=', 19]],
opera: false,
netscape: false,
blackberry: false
// The key is the browser name returned by jQuery.client
// The value is either null (match all versions) or a list of tuples
// containing an inequality (<,>,<=,>=) and a version number
whitelist: {
'msie': [['>=', 9]],
'firefox': [['>=', 11]],
'iceweasel': [['>=', 10]],
'safari': [['>=', 5]],
'chrome': [['>=', 19]]
},
// Right-to-left languages
rtl: {
msie: [['>=', 9]],
firefox: [['>=', 11]],
iceweasel: [['>=', 10]],
safari: [['>=', 5]],
chrome: [['>=', 19]],
opera: false,
netscape: false,
blackberry: false
blacklist: {
'msie': [['<', 9]],
'android': [['<', 3]],
// Blacklist all versions:
'opera': null,
'blackberry': null
}
};

View file

@ -48,6 +48,8 @@ ve.init.mw.Target = function VeInitMwTarget( $container, pageName, revision ) {
this.startTimeStamp = null;
this.doc = null;
this.editNotices = null;
this.remoteNotices = [];
this.localNoticeMessages = [];
this.isMobileDevice = (
'ontouchstart' in window ||
( window.DocumentTouch && document instanceof window.DocumentTouch )
@ -132,8 +134,7 @@ ve.inheritClass( ve.init.mw.Target, ve.init.Target );
* @emits loadError
*/
ve.init.mw.Target.onLoad = function ( response ) {
var key, tmp, el,
data = response ? response.visualeditor : null;
var data = response ? response.visualeditor : null;
if ( !data && !response.error ) {
ve.init.mw.Target.onLoadError.call(
@ -159,12 +160,30 @@ ve.init.mw.Target.onLoad = function ( response ) {
this.originalHtml = data.content;
this.doc = ve.createDocumentFromHTML( this.originalHtml );
/* Don't show notices with no visible html (bug 43013). */
this.remoteNotices = data.notices;
this.baseTimeStamp = data.basetimestamp;
this.startTimeStamp = data.starttimestamp;
// Everything worked, the page was loaded, continue as soon as the module is ready
mw.loader.using( this.modules, ve.bind( ve.init.mw.Target.onReady, this ) );
}
};
/**
* Handle the edit notices being ready for rendering.
*
* @static
* @method
*/
ve.init.mw.Target.onNoticesReady = function() {
var i, len, noticeHtmls, tmp, el;
// Since we're going to parse them, we might as well save these nodes
// so we don't have to parse them again later.
this.editNotices = {};
/* Don't show notices without visible html (bug 43013). */
// This is a temporary container for parsed notices in the <body>.
// We need the elements to be in the DOM in order for stylesheets to apply
// and jquery.visibleText to determine whether a node is visible.
@ -172,29 +191,30 @@ ve.init.mw.Target.onLoad = function ( response ) {
// The following is essentially display none, but we can't use that
// since then all descendants will be considered invisible too.
tmp.style.cssText = 'position: static; top: 0; width: 0; height: 0; border: 0; visibility: hidden';
tmp.style.cssText = 'position: static; top: 0; width: 0; height: 0; border: 0; visibility: hidden;';
document.body.appendChild( tmp );
for ( key in data.notices ) {
// Merge locally and remotely generated notices
noticeHtmls = this.remoteNotices;
for ( i = 0, len = this.localNoticeMessages.length; i < len; i++ ) {
noticeHtmls.push(
'<p>' + ve.init.platform.getParsedMessage( this.localNoticeMessages[i] ) + '</p>'
);
}
for ( i = 0, len = noticeHtmls.length; i < len; i++ ) {
el = $( '<div>' )
.addClass( 've-init-mw-viewPageTarget-toolbar-editNotices-notice' )
.attr( 'rel', key )
.html( data.notices[key] )
.html( noticeHtmls[i] )
.get( 0 );
tmp.appendChild( el );
if ( $.getVisibleText( el ).trim() !== '' ) {
this.editNotices[key] = el;
this.editNotices[i] = el;
}
tmp.removeChild( el );
}
document.body.removeChild( tmp );
this.baseTimeStamp = data.basetimestamp;
this.startTimeStamp = data.starttimestamp;
// Everything worked, the page was loaded, continue as soon as the module is ready
mw.loader.using( this.modules, ve.bind( ve.init.mw.Target.onReady, this ) );
}
};
/**
@ -238,6 +258,9 @@ ve.init.mw.Target.onToken = function ( response ) {
* @emits load
*/
ve.init.mw.Target.onReady = function () {
// We need to wait until onReady as local notices may require special messages
ve.init.mw.Target.onNoticesReady.call( this );
this.loading = false;
this.emit( 'load', this.doc );
};