From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- .../src/mediawiki/images/arrow-collapsed-ltr.png | Bin 0 -> 133 bytes .../src/mediawiki/images/arrow-collapsed-ltr.svg | 1 + .../src/mediawiki/images/arrow-collapsed-rtl.png | Bin 0 -> 136 bytes .../src/mediawiki/images/arrow-collapsed-rtl.svg | 1 + resources/src/mediawiki/images/arrow-expanded.png | Bin 0 -> 134 bytes resources/src/mediawiki/images/arrow-expanded.svg | 1 + .../src/mediawiki/images/arrow-sort-ascending.png | Bin 0 -> 244 bytes .../src/mediawiki/images/arrow-sort-ascending.svg | 1 + .../src/mediawiki/images/arrow-sort-descending.png | Bin 0 -> 245 bytes .../src/mediawiki/images/arrow-sort-descending.svg | 1 + .../pager-arrow-disabled-fastforward-ltr.png | Bin 0 -> 323 bytes .../pager-arrow-disabled-fastforward-rtl.png | Bin 0 -> 318 bytes .../images/pager-arrow-disabled-forward-ltr.png | Bin 0 -> 307 bytes .../images/pager-arrow-disabled-forward-rtl.png | Bin 0 -> 301 bytes .../images/pager-arrow-fastforward-ltr.png | Bin 0 -> 342 bytes .../images/pager-arrow-fastforward-rtl.png | Bin 0 -> 352 bytes .../mediawiki/images/pager-arrow-forward-ltr.png | Bin 0 -> 337 bytes .../mediawiki/images/pager-arrow-forward-rtl.png | Bin 0 -> 330 bytes resources/src/mediawiki/mediawiki.Title.js | 939 ++++++++ resources/src/mediawiki/mediawiki.Uri.js | 403 ++++ resources/src/mediawiki/mediawiki.content.json.css | 53 + resources/src/mediawiki/mediawiki.cookie.js | 126 + resources/src/mediawiki/mediawiki.debug.init.js | 3 + resources/src/mediawiki/mediawiki.debug.js | 391 ++++ resources/src/mediawiki/mediawiki.debug.less | 189 ++ .../src/mediawiki/mediawiki.debug.profile.css | 45 + resources/src/mediawiki/mediawiki.debug.profile.js | 556 +++++ resources/src/mediawiki/mediawiki.feedback.css | 9 + resources/src/mediawiki/mediawiki.feedback.js | 320 +++ .../src/mediawiki/mediawiki.feedback.spinner.gif | Bin 0 -> 1108 bytes resources/src/mediawiki/mediawiki.hidpi.js | 5 + resources/src/mediawiki/mediawiki.hlist.css | 78 + resources/src/mediawiki/mediawiki.hlist.js | 31 + resources/src/mediawiki/mediawiki.htmlform.js | 408 ++++ resources/src/mediawiki/mediawiki.icon.less | 19 + resources/src/mediawiki/mediawiki.inspect.js | 284 +++ resources/src/mediawiki/mediawiki.jqueryMsg.js | 1251 ++++++++++ resources/src/mediawiki/mediawiki.jqueryMsg.peg | 85 + resources/src/mediawiki/mediawiki.js | 2399 ++++++++++++++++++++ resources/src/mediawiki/mediawiki.log.js | 84 + resources/src/mediawiki/mediawiki.notification.css | 27 + .../mediawiki.notification.hideForPrint.css | 3 + resources/src/mediawiki/mediawiki.notification.js | 523 +++++ resources/src/mediawiki/mediawiki.notify.js | 27 + .../src/mediawiki/mediawiki.pager.tablePager.less | 84 + .../src/mediawiki/mediawiki.searchSuggest.css | 24 + resources/src/mediawiki/mediawiki.searchSuggest.js | 199 ++ resources/src/mediawiki/mediawiki.toc.js | 60 + resources/src/mediawiki/mediawiki.user.js | 258 +++ resources/src/mediawiki/mediawiki.util.js | 531 +++++ 50 files changed, 9419 insertions(+) create mode 100644 resources/src/mediawiki/images/arrow-collapsed-ltr.png create mode 100644 resources/src/mediawiki/images/arrow-collapsed-ltr.svg create mode 100644 resources/src/mediawiki/images/arrow-collapsed-rtl.png create mode 100644 resources/src/mediawiki/images/arrow-collapsed-rtl.svg create mode 100644 resources/src/mediawiki/images/arrow-expanded.png create mode 100644 resources/src/mediawiki/images/arrow-expanded.svg create mode 100644 resources/src/mediawiki/images/arrow-sort-ascending.png create mode 100644 resources/src/mediawiki/images/arrow-sort-ascending.svg create mode 100644 resources/src/mediawiki/images/arrow-sort-descending.png create mode 100644 resources/src/mediawiki/images/arrow-sort-descending.svg create mode 100644 resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.png create mode 100644 resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.png create mode 100644 resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.png create mode 100644 resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.png create mode 100644 resources/src/mediawiki/images/pager-arrow-fastforward-ltr.png create mode 100644 resources/src/mediawiki/images/pager-arrow-fastforward-rtl.png create mode 100644 resources/src/mediawiki/images/pager-arrow-forward-ltr.png create mode 100644 resources/src/mediawiki/images/pager-arrow-forward-rtl.png create mode 100644 resources/src/mediawiki/mediawiki.Title.js create mode 100644 resources/src/mediawiki/mediawiki.Uri.js create mode 100644 resources/src/mediawiki/mediawiki.content.json.css create mode 100644 resources/src/mediawiki/mediawiki.cookie.js create mode 100644 resources/src/mediawiki/mediawiki.debug.init.js create mode 100644 resources/src/mediawiki/mediawiki.debug.js create mode 100644 resources/src/mediawiki/mediawiki.debug.less create mode 100644 resources/src/mediawiki/mediawiki.debug.profile.css create mode 100644 resources/src/mediawiki/mediawiki.debug.profile.js create mode 100644 resources/src/mediawiki/mediawiki.feedback.css create mode 100644 resources/src/mediawiki/mediawiki.feedback.js create mode 100644 resources/src/mediawiki/mediawiki.feedback.spinner.gif create mode 100644 resources/src/mediawiki/mediawiki.hidpi.js create mode 100644 resources/src/mediawiki/mediawiki.hlist.css create mode 100644 resources/src/mediawiki/mediawiki.hlist.js create mode 100644 resources/src/mediawiki/mediawiki.htmlform.js create mode 100644 resources/src/mediawiki/mediawiki.icon.less create mode 100644 resources/src/mediawiki/mediawiki.inspect.js create mode 100644 resources/src/mediawiki/mediawiki.jqueryMsg.js create mode 100644 resources/src/mediawiki/mediawiki.jqueryMsg.peg create mode 100644 resources/src/mediawiki/mediawiki.js create mode 100644 resources/src/mediawiki/mediawiki.log.js create mode 100644 resources/src/mediawiki/mediawiki.notification.css create mode 100644 resources/src/mediawiki/mediawiki.notification.hideForPrint.css create mode 100644 resources/src/mediawiki/mediawiki.notification.js create mode 100644 resources/src/mediawiki/mediawiki.notify.js create mode 100644 resources/src/mediawiki/mediawiki.pager.tablePager.less create mode 100644 resources/src/mediawiki/mediawiki.searchSuggest.css create mode 100644 resources/src/mediawiki/mediawiki.searchSuggest.js create mode 100644 resources/src/mediawiki/mediawiki.toc.js create mode 100644 resources/src/mediawiki/mediawiki.user.js create mode 100644 resources/src/mediawiki/mediawiki.util.js (limited to 'resources/src/mediawiki') diff --git a/resources/src/mediawiki/images/arrow-collapsed-ltr.png b/resources/src/mediawiki/images/arrow-collapsed-ltr.png new file mode 100644 index 00000000..b17e578b Binary files /dev/null and b/resources/src/mediawiki/images/arrow-collapsed-ltr.png differ diff --git a/resources/src/mediawiki/images/arrow-collapsed-ltr.svg b/resources/src/mediawiki/images/arrow-collapsed-ltr.svg new file mode 100644 index 00000000..6233fd5e --- /dev/null +++ b/resources/src/mediawiki/images/arrow-collapsed-ltr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/src/mediawiki/images/arrow-collapsed-rtl.png b/resources/src/mediawiki/images/arrow-collapsed-rtl.png new file mode 100644 index 00000000..a834548e Binary files /dev/null and b/resources/src/mediawiki/images/arrow-collapsed-rtl.png differ diff --git a/resources/src/mediawiki/images/arrow-collapsed-rtl.svg b/resources/src/mediawiki/images/arrow-collapsed-rtl.svg new file mode 100644 index 00000000..44d5587a --- /dev/null +++ b/resources/src/mediawiki/images/arrow-collapsed-rtl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/src/mediawiki/images/arrow-expanded.png b/resources/src/mediawiki/images/arrow-expanded.png new file mode 100644 index 00000000..2bec798e Binary files /dev/null and b/resources/src/mediawiki/images/arrow-expanded.png differ diff --git a/resources/src/mediawiki/images/arrow-expanded.svg b/resources/src/mediawiki/images/arrow-expanded.svg new file mode 100644 index 00000000..a0d217d2 --- /dev/null +++ b/resources/src/mediawiki/images/arrow-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/src/mediawiki/images/arrow-sort-ascending.png b/resources/src/mediawiki/images/arrow-sort-ascending.png new file mode 100644 index 00000000..f2d339de Binary files /dev/null and b/resources/src/mediawiki/images/arrow-sort-ascending.png differ diff --git a/resources/src/mediawiki/images/arrow-sort-ascending.svg b/resources/src/mediawiki/images/arrow-sort-ascending.svg new file mode 100644 index 00000000..1e7a0943 --- /dev/null +++ b/resources/src/mediawiki/images/arrow-sort-ascending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/src/mediawiki/images/arrow-sort-descending.png b/resources/src/mediawiki/images/arrow-sort-descending.png new file mode 100644 index 00000000..8afbca96 Binary files /dev/null and b/resources/src/mediawiki/images/arrow-sort-descending.png differ diff --git a/resources/src/mediawiki/images/arrow-sort-descending.svg b/resources/src/mediawiki/images/arrow-sort-descending.svg new file mode 100644 index 00000000..cf11adb4 --- /dev/null +++ b/resources/src/mediawiki/images/arrow-sort-descending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.png b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.png new file mode 100644 index 00000000..2a64fd03 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.png b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.png new file mode 100644 index 00000000..78a493e6 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.png b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.png new file mode 100644 index 00000000..aa4fbf8c Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.png b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.png new file mode 100644 index 00000000..83df0684 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.png b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.png new file mode 100644 index 00000000..caf50331 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-fastforward-ltr.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.png b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.png new file mode 100644 index 00000000..52b32a5a Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-fastforward-rtl.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-forward-ltr.png b/resources/src/mediawiki/images/pager-arrow-forward-ltr.png new file mode 100644 index 00000000..3f8fee38 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-forward-ltr.png differ diff --git a/resources/src/mediawiki/images/pager-arrow-forward-rtl.png b/resources/src/mediawiki/images/pager-arrow-forward-rtl.png new file mode 100644 index 00000000..f363bf66 Binary files /dev/null and b/resources/src/mediawiki/images/pager-arrow-forward-rtl.png differ diff --git a/resources/src/mediawiki/mediawiki.Title.js b/resources/src/mediawiki/mediawiki.Title.js new file mode 100644 index 00000000..7ced42fe --- /dev/null +++ b/resources/src/mediawiki/mediawiki.Title.js @@ -0,0 +1,939 @@ +/*! + * @author Neil Kandalgaonkar, 2010 + * @author Timo Tijhof, 2011-2013 + * @since 1.18 + */ +( function ( mw, $ ) { + + /** + * @class mw.Title + * + * Parse titles into an object struture. Note that when using the constructor + * directly, passing invalid titles will result in an exception. Use #newFromText to use the + * logic directly and get null for invalid titles which is easier to work with. + * + * @constructor + * @param {string} title Title of the page. If no second argument given, + * this will be searched for a namespace + * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title + * @throws {Error} When the title is invalid + */ + function Title( title, namespace ) { + var parsed = parse( title, namespace ); + if ( !parsed ) { + throw new Error( 'Unable to parse title' ); + } + + this.namespace = parsed.namespace; + this.title = parsed.title; + this.ext = parsed.ext; + this.fragment = parsed.fragment; + + return this; + } + + /* Private members */ + + var + + /** + * @private + * @static + * @property NS_MAIN + */ + NS_MAIN = 0, + + /** + * @private + * @static + * @property NS_TALK + */ + NS_TALK = 1, + + /** + * @private + * @static + * @property NS_SPECIAL + */ + NS_SPECIAL = -1, + + /** + * @private + * @static + * @property NS_MEDIA + */ + NS_MEDIA = -2, + + /** + * @private + * @static + * @property NS_FILE + */ + NS_FILE = 6, + + /** + * @private + * @static + * @property FILENAME_MAX_BYTES + */ + FILENAME_MAX_BYTES = 240, + + /** + * @private + * @static + * @property TITLE_MAX_BYTES + */ + TITLE_MAX_BYTES = 255, + + /** + * Get the namespace id from a namespace name (either from the localized, canonical or alias + * name). + * + * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or + * even 'Bild'. + * + * @private + * @static + * @method getNsIdByName + * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored) + * @return {number|boolean} Namespace id or boolean false + */ + getNsIdByName = function ( ns ) { + var id; + + // Don't cast non-strings to strings, because null or undefined should not result in + // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki) + // Also, toLowerCase throws exception on null/undefined, because it is a String method. + if ( typeof ns !== 'string' ) { + return false; + } + ns = ns.toLowerCase(); + id = mw.config.get( 'wgNamespaceIds' )[ns]; + if ( id === undefined ) { + return false; + } + return id; + }, + + rUnderscoreTrim = /^_+|_+$/g, + + rSplit = /^(.+?)_*:_*(.*)$/, + + // See Title.php#getTitleInvalidRegex + rInvalid = new RegExp( + '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' + + // URL percent encoding sequences interfere with the ability + // to round-trip titles -- you can't link to them consistently. + '|%[0-9A-Fa-f]{2}' + + // XML/HTML character references produce similar issues. + '|&[A-Za-z0-9\u0080-\uFFFF]+;' + + '|&#[0-9]+;' + + '|&#x[0-9A-Fa-f]+;' + ), + + // From MediaWikiTitleCodec.php#L225 @26fcab1f18c568a41 + // "Clean up whitespace" in function MediaWikiTitleCodec::splitTitleString() + rWhitespace = /[ _\u0009\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\s]+/g, + + /** + * Slightly modified from Flinfo. Credit goes to Lupo and Flominator. + * @private + * @static + * @property sanitationRules + */ + sanitationRules = [ + // "signature" + { + pattern: /~{3}/g, + replace: '', + generalRule: true + }, + // Space, underscore, tab, NBSP and other unusual spaces + { + pattern: rWhitespace, + replace: ' ', + generalRule: true + }, + // unicode bidi override characters: Implicit, Embeds, Overrides + { + pattern: /[\u200E\u200F\u202A-\u202E]/g, + replace: '', + generalRule: true + }, + // control characters + { + pattern: /[\x00-\x1f\x7f]/g, + replace: '', + generalRule: true + }, + // URL encoding (possibly) + { + pattern: /%([0-9A-Fa-f]{2})/g, + replace: '% $1', + generalRule: true + }, + // HTML-character-entities + { + pattern: /&(([A-Za-z0-9\x80-\xff]+|#[0-9]+|#x[0-9A-Fa-f]+);)/g, + replace: '& $1', + generalRule: true + }, + // slash, colon (not supported by file systems like NTFS/Windows, Mac OS 9 [:], ext4 [/]) + { + pattern: /[:\/#]/g, + replace: '-', + fileRule: true + }, + // brackets, greater than + { + pattern: /[\]\}>]/g, + replace: ')', + generalRule: true + }, + // brackets, lower than + { + pattern: /[\[\{<]/g, + replace: '(', + generalRule: true + }, + // everything that wasn't covered yet + { + pattern: new RegExp( rInvalid.source, 'g' ), + replace: '-', + generalRule: true + }, + // directory structures + { + pattern: /^(\.|\.\.|\.\/.*|\.\.\/.*|.*\/\.\/.*|.*\/\.\.\/.*|.*\/\.|.*\/\.\.)$/g, + replace: '', + generalRule: true + } + ], + + /** + * Internal helper for #constructor and #newFromtext. + * + * Based on Title.php#secureAndSplit + * + * @private + * @static + * @method parse + * @param {string} title + * @param {number} [defaultNamespace=NS_MAIN] + * @return {Object|boolean} + */ + parse = function ( title, defaultNamespace ) { + var namespace, m, id, i, fragment, ext; + + namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace; + + title = title + // Normalise whitespace to underscores and remove duplicates + .replace( /[ _\s]+/g, '_' ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + + // Process initial colon + if ( title !== '' && title.charAt( 0 ) === ':' ) { + // Initial colon means main namespace instead of specified default + namespace = NS_MAIN; + title = title + // Strip colon + .slice( 1 ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + } + + if ( title === '' ) { + return false; + } + + // Process namespace prefix (if any) + m = title.match( rSplit ); + if ( m ) { + id = getNsIdByName( m[1] ); + if ( id !== false ) { + // Ordinary namespace + namespace = id; + title = m[2]; + + // For Talk:X pages, make sure X has no "namespace" prefix + if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) { + // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x) + if ( getNsIdByName( m[1] ) !== false ) { + return false; + } + } + } + } + + // Process fragment + i = title.indexOf( '#' ); + if ( i === -1 ) { + fragment = null; + } else { + fragment = title + // Get segment starting after the hash + .slice( i + 1 ) + // Convert to text + // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo") + .replace( /_/g, ' ' ); + + title = title + // Strip hash + .slice( 0, i ) + // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux") + .replace( rUnderscoreTrim, '' ); + } + + // Reject illegal characters + if ( title.match( rInvalid ) ) { + return false; + } + + // Disallow titles that browsers or servers might resolve as directory navigation + if ( + title.indexOf( '.' ) !== -1 && ( + title === '.' || title === '..' || + title.indexOf( './' ) === 0 || + title.indexOf( '../' ) === 0 || + title.indexOf( '/./' ) !== -1 || + title.indexOf( '/../' ) !== -1 || + title.slice( -2 ) === '/.' || + title.slice( -3 ) === '/..' + ) + ) { + return false; + } + + // Disallow magic tilde sequence + if ( title.indexOf( '~~~' ) !== -1 ) { + return false; + } + + // Disallow titles exceeding the TITLE_MAX_BYTES byte size limit (size of underlying database field) + // Except for special pages, e.g. [[Special:Block/Long name]] + // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should + // be less than 512 bytes. + if ( namespace !== NS_SPECIAL && $.byteLength( title ) > TITLE_MAX_BYTES ) { + return false; + } + + // Can't make a link to a namespace alone. + if ( title === '' && namespace !== NS_MAIN ) { + return false; + } + + // Any remaining initial :s are illegal. + if ( title.charAt( 0 ) === ':' ) { + return false; + } + + // For backwards-compatibility with old mw.Title, we separate the extension from the + // rest of the title. + i = title.lastIndexOf( '.' ); + if ( i === -1 || title.length <= i + 1 ) { + // Extensions are the non-empty segment after the last dot + ext = null; + } else { + ext = title.slice( i + 1 ); + title = title.slice( 0, i ); + } + + return { + namespace: namespace, + title: title, + ext: ext, + fragment: fragment + }; + }, + + /** + * Convert db-key to readable text. + * + * @private + * @static + * @method text + * @param {string} s + * @return {string} + */ + text = function ( s ) { + if ( s !== null && s !== undefined ) { + return s.replace( /_/g, ' ' ); + } else { + return ''; + } + }, + + /** + * Sanitizes a string based on a rule set and a filter + * + * @private + * @static + * @method sanitize + * @param {string} s + * @param {Array} filter + * @return {string} + */ + sanitize = function ( s, filter ) { + var i, ruleLength, rule, m, filterLength, + rules = sanitationRules; + + for ( i = 0, ruleLength = rules.length; i < ruleLength; ++i ) { + rule = rules[i]; + for ( m = 0, filterLength = filter.length; m < filterLength; ++m ) { + if ( rule[filter[m]] ) { + s = s.replace( rule.pattern, rule.replace ); + } + } + } + return s; + }, + + /** + * Cuts a string to a specific byte length, assuming UTF-8 + * or less, if the last character is a multi-byte one + * + * @private + * @static + * @method trimToByteLength + * @param {string} s + * @param {number} length + * @return {string} + */ + trimToByteLength = function ( s, length ) { + var byteLength, chopOffChars, chopOffBytes; + + // bytelength is always greater or equal to the length in characters + s = s.substr( 0, length ); + while ( ( byteLength = $.byteLength( s ) ) > length ) { + // Calculate how many characters can be safely removed + // First, we need to know how many bytes the string exceeds the threshold + chopOffBytes = byteLength - length; + // A character in UTF-8 is at most 4 bytes + // One character must be removed in any case because the + // string is too long + chopOffChars = Math.max( 1, Math.floor( chopOffBytes / 4 ) ); + s = s.substr( 0, s.length - chopOffChars ); + } + return s; + }, + + /** + * Cuts a file name to a specific byte length + * + * @private + * @static + * @method trimFileNameToByteLength + * @param {string} name without extension + * @param {string} extension file extension + * @return {string} The full name, including extension + */ + trimFileNameToByteLength = function ( name, extension ) { + // There is a special byte limit for file names and ... remember the dot + return trimToByteLength( name, FILENAME_MAX_BYTES - extension.length - 1 ) + '.' + extension; + }, + + // Polyfill for ES5 Object.create + createObject = Object.create || ( function () { + return function ( o ) { + function Title() {} + if ( o !== Object( o ) ) { + throw new Error( 'Cannot inherit from a non-object' ); + } + Title.prototype = o; + return new Title(); + }; + }() ); + + /* Static members */ + + /** + * Constructor for Title objects with a null return instead of an exception for invalid titles. + * + * @static + * @param {string} title + * @param {number} [namespace=NS_MAIN] Default namespace + * @return {mw.Title|null} A valid Title object or null if the title is invalid + */ + Title.newFromText = function ( title, namespace ) { + var t, parsed = parse( title, namespace ); + if ( !parsed ) { + return null; + } + + t = createObject( Title.prototype ); + t.namespace = parsed.namespace; + t.title = parsed.title; + t.ext = parsed.ext; + t.fragment = parsed.fragment; + + return t; + }; + + /** + * Constructor for Title objects from user input altering that input to + * produce a title that MediaWiki will accept as legal + * + * @static + * @param {string} title + * @param {number} [defaultNamespace=NS_MAIN] + * If given, will used as default namespace for the given title. + * @param {Object} [options] additional options + * @param {string} [options.fileExtension=''] + * If the title is about to be created for the Media or File namespace, + * ensures the resulting Title has the correct extension. Useful, for example + * on systems that predict the type by content-sniffing, not by file extension. + * If different from empty string, `forUploading` is assumed. + * @param {boolean} [options.forUploading=true] + * Makes sure that a file is uploadable under the title returned. + * There are pages in the file namespace under which file upload is impossible. + * Automatically assumed if the title is created in the Media namespace. + * @return {mw.Title|null} A valid Title object or null if the input cannot be turned into a valid title + */ + Title.newFromUserInput = function ( title, defaultNamespace, options ) { + var namespace, m, id, ext, parts, normalizeExtension; + + // defaultNamespace is optional; check whether options moves up + if ( arguments.length < 3 && $.type( defaultNamespace ) === 'object' ) { + options = defaultNamespace; + defaultNamespace = undefined; + } + + // merge options into defaults + options = $.extend( { + fileExtension: '', + forUploading: true + }, options ); + + normalizeExtension = function ( extension ) { + // Remove only trailing space (that is removed by MW anyway) + extension = extension.toLowerCase().replace(/\s*$/, ''); + return extension; + }; + + namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace; + + // Normalise whitespace and remove duplicates + title = $.trim( title.replace( rWhitespace, ' ' ) ); + + // Process initial colon + if ( title !== '' && title.charAt( 0 ) === ':' ) { + // Initial colon means main namespace instead of specified default + namespace = NS_MAIN; + title = title + // Strip colon + .substr( 1 ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + } + + // Process namespace prefix (if any) + m = title.match( rSplit ); + if ( m ) { + id = getNsIdByName( m[1] ); + if ( id !== false ) { + // Ordinary namespace + namespace = id; + title = m[2]; + } + } + + if ( namespace === NS_MEDIA + || ( ( options.forUploading || options.fileExtension ) && ( namespace === NS_FILE ) ) + ) { + + title = sanitize( title, [ 'generalRule', 'fileRule' ] ); + + // Operate on the file extension + // Although it is possible having spaces between the name and the ".ext" this isn't nice for + // operating systems hiding file extensions -> strip them later on + parts = title.split( '.' ); + + if ( parts.length > 1 ) { + + // Get the last part, which is supposed to be the file extension + ext = parts.pop(); + + // Does the supplied file name carry the desired file extension? + if ( options.fileExtension + && normalizeExtension( ext ) !== normalizeExtension( options.fileExtension ) + ) { + + // No, push back, whatever there was after the dot + parts.push( ext ); + + // And add the desired file extension later + ext = options.fileExtension; + } + + // Remove whitespace of the name part (that W/O extension) + title = $.trim( parts.join( '.' ) ); + + // Cut, if too long and append file extension + title = trimFileNameToByteLength( title, ext ); + + } else { + + // Missing file extension + title = $.trim( parts.join( '.' ) ); + + if ( options.fileExtension ) { + + // Cut, if too long and append the desired file extension + title = trimFileNameToByteLength( title, options.fileExtension ); + + } else { + + // Name has no file extension and a fallback wasn't provided either + return null; + } + } + } else { + + title = sanitize( title, [ 'generalRule' ] ); + + // Cut titles exceeding the TITLE_MAX_BYTES byte size limit + // (size of underlying database field) + if ( namespace !== NS_SPECIAL ) { + title = trimToByteLength( title, TITLE_MAX_BYTES ); + } + } + + // Any remaining initial :s are illegal. + title = title.replace( /^\:+/, '' ); + + return Title.newFromText( title, namespace ); + }; + + /** + * Sanitizes a file name as supplied by the user, originating in the user's file system + * so it is most likely a valid MediaWiki title and file name after processing. + * Returns null on fatal errors. + * + * @static + * @param {string} uncleanName The unclean file name including file extension but + * without namespace + * @param {string} [fileExtension] the desired file extension + * @return {mw.Title|null} A valid Title object or null if the title is invalid + */ + Title.newFromFileName = function ( uncleanName, fileExtension ) { + + return Title.newFromUserInput( 'File:' + uncleanName, { + fileExtension: fileExtension, + forUploading: true + } ); + }; + + /** + * Get the file title from an image element + * + * var title = mw.Title.newFromImg( $( 'img:first' ) ); + * + * @static + * @param {HTMLElement|jQuery} img The image to use as a base + * @return {mw.Title|null} The file title or null if unsuccessful + */ + Title.newFromImg = function ( img ) { + var matches, i, regex, src, decodedSrc, + + // thumb.php-generated thumbnails + thumbPhpRegex = /thumb\.php/, + regexes = [ + // Thumbnails + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[^\s\/]+-(?:\1|thumbnail)[^\s\/]*$/, + + // Thumbnails in non-hashed upload directories + /\/([^\s\/]+)\/[^\s\/]+-(?:\1|thumbnail)[^\s\/]*$/, + + // Full size images + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/, + + // Full-size images in non-hashed upload directories + /\/([^\s\/]+)$/ + ], + + recount = regexes.length; + + src = img.jquery ? img[0].src : img.src; + + matches = src.match( thumbPhpRegex ); + + if ( matches ) { + return mw.Title.newFromText( 'File:' + mw.util.getParamValue( 'f', src ) ); + } + + decodedSrc = decodeURIComponent( src ); + + for ( i = 0; i < recount; i++ ) { + regex = regexes[i]; + matches = decodedSrc.match( regex ); + + if ( matches && matches[1] ) { + return mw.Title.newFromText( 'File:' + matches[1] ); + } + } + + return null; + }; + + /** + * Whether this title exists on the wiki. + * + * @static + * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title + * @return {boolean|null} Boolean if the information is available, otherwise null + */ + Title.exists = function ( title ) { + var match, + type = $.type( title ), + obj = Title.exist.pages; + + if ( type === 'string' ) { + match = obj[title]; + } else if ( type === 'object' && title instanceof Title ) { + match = obj[title.toString()]; + } else { + throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' ); + } + + if ( typeof match === 'boolean' ) { + return match; + } + + return null; + }; + + /** + * Store page existence + * + * @static + * @property {Object} exist + * @property {Object} exist.pages Keyed by title. Boolean true value indicates page does exist. + * + * @property {Function} exist.set The setter function. + * + * Example to declare existing titles: + * + * Title.exist.set( ['User:John_Doe', ...] ); + * + * Example to declare titles nonexistent: + * + * Title.exist.set( ['File:Foo_bar.jpg', ...], false ); + * + * @property {string|Array} exist.set.titles Title(s) in strict prefixedDb title form + * @property {boolean} [exist.set.state=true] State of the given titles + * @return {boolean} + */ + Title.exist = { + pages: {}, + + set: function ( titles, state ) { + titles = $.isArray( titles ) ? titles : [titles]; + state = state === undefined ? true : !!state; + var pages = this.pages, i, len = titles.length; + for ( i = 0; i < len; i++ ) { + pages[ titles[i] ] = state; + } + return true; + } + }; + + /* Public members */ + + Title.prototype = { + constructor: Title, + + /** + * Get the namespace number + * + * Example: 6 for "File:Example_image.svg". + * + * @return {number} + */ + getNamespaceId: function () { + return this.namespace; + }, + + /** + * Get the namespace prefix (in the content language) + * + * Example: "File:" for "File:Example_image.svg". + * In #NS_MAIN this is '', otherwise namespace name plus ':' + * + * @return {string} + */ + getNamespacePrefix: function () { + return this.namespace === NS_MAIN ? + '' : + ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' ); + }, + + /** + * Get the page name without extension or namespace prefix + * + * Example: "Example_image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMain. + * + * @return {string} + */ + getName: function () { + if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.title; + } else { + return $.ucFirst( this.title ); + } + }, + + /** + * Get the page name (transformed by #text) + * + * Example: "Example image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMainText. + * + * @return {string} + */ + getNameText: function () { + return text( this.getName() ); + }, + + /** + * Get the extension of the page name (if any) + * + * @return {string|null} Name extension or null if there is none + */ + getExtension: function () { + return this.ext; + }, + + /** + * Shortcut for appendable string to form the main page name. + * + * Returns a string like ".json", or "" if no extension. + * + * @return {string} + */ + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; + }, + + /** + * Get the main page name + * + * Example: "Example_image.svg" for "File:Example_image.svg". + * + * @return {string} + */ + getMain: function () { + return this.getName() + this.getDotExtension(); + }, + + /** + * Get the main page name (transformed by #text) + * + * Example: "Example image.svg" for "File:Example_image.svg". + * + * @return {string} + */ + getMainText: function () { + return text( this.getMain() ); + }, + + /** + * Get the full page name + * + * Example: "File:Example_image.svg". + * Most useful for API calls, anything that must identify the "title". + * + * @return {string} + */ + getPrefixedDb: function () { + return this.getNamespacePrefix() + this.getMain(); + }, + + /** + * Get the full page name (transformed by #text) + * + * Example: "File:Example image.svg" for "File:Example_image.svg". + * + * @return {string} + */ + getPrefixedText: function () { + return text( this.getPrefixedDb() ); + }, + + /** + * Get the page name relative to a namespace + * + * Example: + * + * - "Foo:Bar" relative to the Foo namespace becomes "Bar". + * - "Bar" relative to any non-main namespace becomes ":Bar". + * - "Foo:Bar" relative to any namespace other than Foo stays "Foo:Bar". + * + * @param {number} namespace The namespace to be relative to + * @return {string} + */ + getRelativeText: function ( namespace ) { + if ( this.getNamespaceId() === namespace ) { + return this.getMainText(); + } else if ( this.getNamespaceId() === NS_MAIN ) { + return ':' + this.getPrefixedText(); + } else { + return this.getPrefixedText(); + } + }, + + /** + * Get the fragment (if any). + * + * Note that this method (by design) does not include the hash character and + * the value is not url encoded. + * + * @return {string|null} + */ + getFragment: function () { + return this.fragment; + }, + + /** + * Get the URL to this title + * + * @see mw.util#getUrl + * @param {Object} [params] A mapping of query parameter names to values, + * e.g. `{ action: 'edit' }`. + * @return {string} + */ + getUrl: function ( params ) { + return mw.util.getUrl( this.toString(), params ); + }, + + /** + * Whether this title exists on the wiki. + * + * @see #static-method-exists + * @return {boolean|null} Boolean if the information is available, otherwise null + */ + exists: function () { + return Title.exists( this ); + } + }; + + /** + * @alias #getPrefixedDb + * @method + */ + Title.prototype.toString = Title.prototype.getPrefixedDb; + + /** + * @alias #getPrefixedText + * @method + */ + Title.prototype.toText = Title.prototype.getPrefixedText; + + // Expose + mw.Title = Title; + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/mediawiki.Uri.js b/resources/src/mediawiki/mediawiki.Uri.js new file mode 100644 index 00000000..55663128 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.Uri.js @@ -0,0 +1,403 @@ +/** + * Library for simple URI parsing and manipulation. + * + * Intended to be minimal, but featureful; do not expect full RFC 3986 compliance. The use cases we + * have in mind are constructing 'next page' or 'previous page' URLs, detecting whether we need to + * use cross-domain proxies for an API, constructing simple URL-based API calls, etc. Parsing here + * is regex-based, so may not work on all URIs, but is good enough for most. + * + * You can modify the properties directly, then use the #toString method to extract the full URI + * string again. Example: + * + * var uri = new mw.Uri( 'http://example.com/mysite/mypage.php?quux=2' ); + * + * if ( uri.host == 'example.com' ) { + * uri.host = 'foo.example.com'; + * uri.extend( { bar: 1 } ); + * + * $( 'a#id1' ).attr( 'href', uri ); + * // anchor with id 'id1' now links to http://foo.example.com/mysite/mypage.php?bar=1&quux=2 + * + * $( 'a#id2' ).attr( 'href', uri.clone().extend( { bar: 3, pif: 'paf' } ) ); + * // anchor with id 'id2' now links to http://foo.example.com/mysite/mypage.php?bar=3&quux=2&pif=paf + * } + * + * Given a URI like + * `http://usr:pwd@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=&test3=value+%28escaped%29&r=1&r=2#top` + * the returned object will have the following properties: + * + * protocol 'http' + * user 'usr' + * password 'pwd' + * host 'www.example.com' + * port '81' + * path '/dir/dir.2/index.htm' + * query { + * q1: '0', + * test1: null, + * test2: '', + * test3: 'value (escaped)' + * r: ['1', '2'] + * } + * fragment 'top' + * + * (N.b., 'password' is technically not allowed for HTTP URIs, but it is possible with other kinds + * of URIs.) + * + * Parsing based on parseUri 1.2.2 (c) Steven Levithan , MIT License. + * + * + * @class mw.Uri + */ + +( function ( mw, $ ) { + /** + * Function that's useful when constructing the URI string -- we frequently encounter the pattern + * of having to add something to the URI as we go, but only if it's present, and to include a + * character before or after if so. + * + * @private + * @static + * @param {string|undefined} pre To prepend + * @param {string} val To include + * @param {string} post To append + * @param {boolean} raw If true, val will not be encoded + * @return {string} Result + */ + function cat( pre, val, post, raw ) { + if ( val === undefined || val === null || val === '' ) { + return ''; + } + return pre + ( raw ? val : mw.Uri.encode( val ) ) + post; + } + + /** + * Regular expressions to parse many common URIs. + * + * @private + * @static + * @property {Object} parser + */ + var parser = { + strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ + }, + + /** + * The order here matches the order of captured matches in the `parser` property regexes. + * + * @private + * @static + * @property {Array} properties + */ + properties = [ + 'protocol', + 'user', + 'password', + 'host', + 'port', + 'path', + 'query', + 'fragment' + ]; + + /** + * @property {string} protocol For example `http` (always present) + */ + /** + * @property {string|undefined} user For example `usr` + */ + /** + * @property {string|undefined} password For example `pwd` + */ + /** + * @property {string} host For example `www.example.com` (always present) + */ + /** + * @property {string|undefined} port For example `81` + */ + /** + * @property {string} path For example `/dir/dir.2/index.htm` (always present) + */ + /** + * @property {Object} query For example `{ a: '0', b: '', c: 'value' }` (always present) + */ + /** + * @property {string|undefined} fragment For example `top` + */ + + /** + * A factory method to create a variation of mw.Uri with a different default location (for + * relative URLs, including protocol-relative URLs). Used so the library is still testable & + * purely functional. + * + * @method + * @member mw + */ + mw.UriRelative = function ( documentLocation ) { + var defaultUri; + + /** + * @class mw.Uri + * @constructor + * + * Construct a new URI object. Throws error if arguments are illegal/impossible, or + * otherwise don't parse. + * + * @param {Object|string} [uri] URI string, or an Object with appropriate properties (especially + * another URI object to clone). Object must have non-blank `protocol`, `host`, and `path` + * properties. If omitted (or set to `undefined`, `null` or empty string), then an object + * will be created for the default `uri` of this constructor (`document.location` for + * mw.Uri, other values for other instances -- see mw.UriRelative for details). + * @param {Object|boolean} [options] Object with options, or (backwards compatibility) a boolean + * for strictMode + * @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url. + * @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters + * override each other (`true`) or automagically convert them to an array (`false`). + */ + function Uri( uri, options ) { + options = typeof options === 'object' ? options : { strictMode: !!options }; + options = $.extend( { + strictMode: false, + overrideKeys: false + }, options ); + + if ( uri !== undefined && uri !== null && uri !== '' ) { + if ( typeof uri === 'string' ) { + this.parse( uri, options ); + } else if ( typeof uri === 'object' ) { + // Copy data over from existing URI object + for ( var prop in uri ) { + // Only copy direct properties, not inherited ones + if ( uri.hasOwnProperty( prop ) ) { + // Deep copy object properties + if ( $.isArray( uri[prop] ) || $.isPlainObject( uri[prop] ) ) { + this[prop] = $.extend( true, {}, uri[prop] ); + } else { + this[prop] = uri[prop]; + } + } + } + if ( !this.query ) { + this.query = {}; + } + } + } else { + // If we didn't get a URI in the constructor, use the default one. + return defaultUri.clone(); + } + + // protocol-relative URLs + if ( !this.protocol ) { + this.protocol = defaultUri.protocol; + } + // No host given: + if ( !this.host ) { + this.host = defaultUri.host; + // port ? + if ( !this.port ) { + this.port = defaultUri.port; + } + } + if ( this.path && this.path.charAt( 0 ) !== '/' ) { + // A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot + // figure out whether the last path component of defaultUri.path is a directory or a file. + throw new Error( 'Bad constructor arguments' ); + } + if ( !( this.protocol && this.host && this.path ) ) { + throw new Error( 'Bad constructor arguments' ); + } + } + + /** + * Encode a value for inclusion in a url. + * + * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more + * compliant with RFC 3986. Similar to rawurlencode from PHP and our JS library + * mw.util.rawurlencode, except this also replaces spaces with `+`. + * + * @static + * @param {string} s String to encode + * @return {string} Encoded string for URI + */ + Uri.encode = function ( s ) { + return encodeURIComponent( s ) + .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' ) + .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ) + .replace( /%20/g, '+' ); + }; + + /** + * Decode a url encoded value. + * + * Reversed #encode. Standard decodeURIComponent, with addition of replacing + * `+` with a space. + * + * @static + * @param {string} s String to decode + * @return {string} Decoded string + */ + Uri.decode = function ( s ) { + return decodeURIComponent( s.replace( /\+/g, '%20' ) ); + }; + + Uri.prototype = { + + /** + * Parse a string and set our properties accordingly. + * + * @private + * @param {string} str URI, see constructor. + * @param {Object} options See constructor. + */ + parse: function ( str, options ) { + var q, matches, + uri = this; + + // Apply parser regex and set all properties based on the result + matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); + $.each( properties, function ( i, property ) { + uri[ property ] = matches[ i + 1 ]; + } ); + + // uri.query starts out as the query string; we will parse it into key-val pairs then make + // that object the "query" property. + // we overwrite query in uri way to make cloning easier, it can use the same list of properties. + q = {}; + // using replace to iterate over a string + if ( uri.query ) { + uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) { + var k, v; + if ( $1 ) { + k = Uri.decode( $1 ); + v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); + + // If overrideKeys, always (re)set top level value. + // If not overrideKeys but this key wasn't set before, then we set it as well. + if ( options.overrideKeys || q[ k ] === undefined ) { + q[ k ] = v; + + // Use arrays if overrideKeys is false and key was already seen before + } else { + // Once before, still a string, turn into an array + if ( typeof q[ k ] === 'string' ) { + q[ k ] = [ q[ k ] ]; + } + // Add to the array + if ( $.isArray( q[ k ] ) ) { + q[ k ].push( v ); + } + } + } + } ); + } + uri.query = q; + }, + + /** + * Get user and password section of a URI. + * + * @return {string} + */ + getUserInfo: function () { + return cat( '', this.user, cat( ':', this.password, '' ) ); + }, + + /** + * Get host and port section of a URI. + * + * @return {string} + */ + getHostPort: function () { + return this.host + cat( ':', this.port, '' ); + }, + + /** + * Get the userInfo, host and port section of the URI. + * + * In most real-world URLs this is simply the hostname, but the definition of 'authority' section is more general. + * + * @return {string} + */ + getAuthority: function () { + return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); + }, + + /** + * Get the query arguments of the URL, encoded into a string. + * + * Does not preserve the original order of arguments passed in the URI. Does handle escaping. + * + * @return {string} + */ + getQueryString: function () { + var args = []; + $.each( this.query, function ( key, val ) { + var k = Uri.encode( key ), + vals = $.isArray( val ) ? val : [ val ]; + $.each( vals, function ( i, v ) { + if ( v === null ) { + args.push( k ); + } else if ( k === 'title' ) { + args.push( k + '=' + mw.util.wikiUrlencode( v ) ); + } else { + args.push( k + '=' + Uri.encode( v ) ); + } + } ); + } ); + return args.join( '&' ); + }, + + /** + * Get everything after the authority section of the URI. + * + * @return {string} + */ + getRelativePath: function () { + return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); + }, + + /** + * Get the entire URI string. + * + * May not be precisely the same as input due to order of query arguments. + * + * @return {string} The URI string + */ + toString: function () { + return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); + }, + + /** + * Clone this URI + * + * @return {Object} New URI object with same properties + */ + clone: function () { + return new Uri( this ); + }, + + /** + * Extend the query section of the URI with new parameters. + * + * @param {Object} parameters Query parameters to add to ours (or to override ours with) as an + * object + * @return {Object} This URI object + */ + extend: function ( parameters ) { + $.extend( this.query, parameters ); + return this; + } + }; + + defaultUri = new Uri( documentLocation ); + + return Uri; + }; + + // If we are running in a browser, inject the current document location (for relative URLs). + if ( document && document.location && document.location.href ) { + mw.Uri = mw.UriRelative( document.location.href ); + } + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/mediawiki.content.json.css b/resources/src/mediawiki/mediawiki.content.json.css new file mode 100644 index 00000000..d93e291e --- /dev/null +++ b/resources/src/mediawiki/mediawiki.content.json.css @@ -0,0 +1,53 @@ +/*! + * CSS for styling HTML-formatted JSON Schema objects + * + * @file + * @author Munaf Assaf + */ + +.mw-json { + border-collapse: collapse; + border-spacing: 0; + font-style: normal; +} + +.mw-json th, +.mw-json td { + border: 1px solid gray; + font-size: 16px; + padding: 0.5em 1em; +} + +.mw-json td { + background-color: #eee; + font-style: italic; +} + +.mw-json .value { + background-color: #dcfae3; + font-family: monospace, monospace; + white-space: pre-wrap; +} + +.mw-json tr { + margin-bottom: 0.5em; +} + +.mw-json th { + background-color: #fff; + font-weight: normal; +} + +.mw-json caption { + /* For stylistic reasons, suppress the caption of the outermost table */ + display: none; +} + +.mw-json table caption { + color: gray; + display: inline-block; + font-size: 10px; + font-style: italic; + margin-bottom: 0.5em; + text-align: left; +} diff --git a/resources/src/mediawiki/mediawiki.cookie.js b/resources/src/mediawiki/mediawiki.cookie.js new file mode 100644 index 00000000..6f9f0abb --- /dev/null +++ b/resources/src/mediawiki/mediawiki.cookie.js @@ -0,0 +1,126 @@ +( function ( mw, $ ) { + 'use strict'; + + /** + * Provides an API for getting and setting cookies that is + * syntactically and functionally similar to the server-side cookie + * API (`WebRequest#getCookie` and `WebResponse#setcookie`). + * + * @author Sam Smith + * @author Matthew Flaschen + * @author Timo Tijhof + * + * @class mw.cookie + * @singleton + */ + mw.cookie = { + + /** + * Sets or deletes a cookie. + * + * While this is natural in JavaScript, contrary to `WebResponse#setcookie` in PHP, the + * default values for the `options` properties only apply if that property isn't set + * already in your options object (e.g. passing `{ secure: null }` or `{ secure: undefined }` + * overrides the default value for `options.secure`). + * + * @param {string} key + * @param {string|null} value Value of cookie. If `value` is `null` then this method will + * instead remove a cookie by name of `key`. + * @param {Object|Date} [options] Options object, or expiry date + * @param {Date|null} [options.expires=wgCookieExpiration] The expiry date of the cookie. + * + * Default cookie expiration is based on `wgCookieExpiration`. If `wgCookieExpiration` is + * 0, a session cookie is set (expires when the browser is closed). For non-zero values of + * `wgCookieExpiration`, the cookie expires `wgCookieExpiration` seconds from now. + * + * If options.expires is null, then a session cookie is set. + * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key + * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie + * @param {string} [options.path=wgCookiePath] The path attribute of the cookie + * @param {boolean} [options.secure=false] Whether or not to include the secure attribute. + * (Does **not** use the wgCookieSecure configuration variable) + */ + set: function ( key, value, options ) { + var config, defaultOptions, date; + + // wgCookieSecure is not used for now, since 'detect' could not work with + // ResourceLoaderStartUpModule, as module cache is not fragmented by protocol. + config = mw.config.get( [ + 'wgCookiePrefix', + 'wgCookieDomain', + 'wgCookiePath', + 'wgCookieExpiration' + ] ); + + defaultOptions = { + prefix: config.wgCookiePrefix, + domain: config.wgCookieDomain, + path: config.wgCookiePath, + secure: false + }; + + // Options argument can also be a shortcut for the expiry + // Expiry can be a Date or null + if ( $.type( options ) !== 'object' ) { + // Also takes care of options = undefined, in which case we also don't need $.extend() + defaultOptions.expires = options; + options = defaultOptions; + } else { + options = $.extend( defaultOptions, options ); + } + + // $.cookie makes session cookies when expiry is omitted, + // however our default is to expire wgCookieExpiration seconds from now. + // Note: If wgCookieExpiration is 0, that is considered a special value indicating + // all cookies should be session cookies by default. + if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) { + date = new Date(); + date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) ); + options.expires = date; + } else if ( options.expires === null ) { + // $.cookie makes a session cookie when expires is omitted + delete options.expires; + } + + // Process prefix + key = options.prefix + key; + delete options.prefix; + + // Process value + if ( value !== null ) { + value = String( value ); + } + + // Other options are handled by $.cookie + $.cookie( key, value, options ); + }, + + /** + * Gets the value of a cookie. + * + * @param {string} key + * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is + * `undefined` or `null`, then `wgCookiePrefix` is used + * @param {Mixed} [defaultValue=null] + * @return {string} If the cookie exists, then the value of the + * cookie, otherwise `defaultValue` + */ + get: function ( key, prefix, defaultValue ) { + var result; + + if ( prefix === undefined || prefix === null ) { + prefix = mw.config.get( 'wgCookiePrefix' ); + } + + // Was defaultValue omitted? + if ( arguments.length < 3 ) { + defaultValue = null; + } + + result = $.cookie( prefix + key ); + + return result !== null ? result : defaultValue; + } + }; + +} ( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/mediawiki.debug.init.js b/resources/src/mediawiki/mediawiki.debug.init.js new file mode 100644 index 00000000..0f85e80d --- /dev/null +++ b/resources/src/mediawiki/mediawiki.debug.init.js @@ -0,0 +1,3 @@ +jQuery( function () { + mediaWiki.Debug.init(); +} ); diff --git a/resources/src/mediawiki/mediawiki.debug.js b/resources/src/mediawiki/mediawiki.debug.js new file mode 100644 index 00000000..4935984f --- /dev/null +++ b/resources/src/mediawiki/mediawiki.debug.js @@ -0,0 +1,391 @@ +( function ( mw, $ ) { + 'use strict'; + + var debug, + hovzer = $.getFootHovzer(); + + /** + * Debug toolbar. + * + * Enabled server-side through `$wgDebugToolbar`. + * + * @class mw.Debug + * @singleton + * @author John Du Hart + * @since 1.19 + */ + debug = mw.Debug = { + /** + * Toolbar container element + * + * @property {jQuery} + */ + $container: null, + + /** + * Object containing data for the debug toolbar + * + * @property {Object} + */ + data: {}, + + /** + * Initialize the debugging pane + * + * Shouldn't be called before the document is ready + * (since it binds to elements on the page). + * + * @param {Object} [data] Defaults to 'debugInfo' from mw.config + */ + init: function ( data ) { + + this.data = data || mw.config.get( 'debugInfo' ); + this.buildHtml(); + + // Insert the container into the DOM + hovzer.$.append( this.$container ); + hovzer.update(); + + $( '.mw-debug-panelink' ).click( this.switchPane ); + }, + + /** + * Switch between panes + * + * Should be called with an HTMLElement as its thisArg, + * because it's meant to be an event handler. + * + * TODO: Store cookie for last pane open. + * + * @param {jQuery.Event} e + */ + switchPane: function ( e ) { + var currentPaneId = debug.$container.data( 'currentPane' ), + requestedPaneId = $( this ).prop( 'id' ).slice( 9 ), + $currentPane = $( '#mw-debug-pane-' + currentPaneId ), + $requestedPane = $( '#mw-debug-pane-' + requestedPaneId ), + hovDone = false; + + function updateHov() { + if ( !hovDone ) { + hovzer.update(); + hovDone = true; + } + } + + // Skip hash fragment handling. Prevents screen from jumping. + e.preventDefault(); + + $( this ).addClass( 'current ' ); + $( '.mw-debug-panelink' ).not( this ).removeClass( 'current ' ); + + // Hide the current pane + if ( requestedPaneId === currentPaneId ) { + $currentPane.slideUp( updateHov ); + debug.$container.data( 'currentPane', null ); + return; + } + + debug.$container.data( 'currentPane', requestedPaneId ); + + if ( currentPaneId === undefined || currentPaneId === null ) { + $requestedPane.slideDown( updateHov ); + } else { + $currentPane.hide(); + $requestedPane.show(); + updateHov(); + } + }, + + /** + * Construct the HTML for the debugging toolbar + */ + buildHtml: function () { + var $container, $bits, panes, id, gitInfo; + + $container = $( '
' ); + + $bits = $( '
' ); + + /** + * Returns a jQuery element for a debug-bit div + * + * @ignore + * @param {string} id + * @return {jQuery} + */ + function bitDiv( id ) { + return $( '
' ).prop( { + id: 'mw-debug-' + id, + className: 'mw-debug-bit' + } ) + .appendTo( $bits ); + } + + /** + * Returns a jQuery element for a pane link + * + * @ignore + * @param {string} id + * @param {string} text + * @return {jQuery} + */ + function paneLabel( id, text ) { + return $( '' ) + .prop( { + className: 'mw-debug-panelabel', + href: '#mw-debug-pane-' + id + } ) + .text( text ); + } + + /** + * Returns a jQuery element for a debug-bit div with a for a pane link + * + * @ignore + * @param {string} id CSS id snippet. Will be prefixed with 'mw-debug-' + * @param {string} text Text to show + * @param {string} count Optional count to show + * @return {jQuery} + */ + function paneTriggerBitDiv( id, text, count ) { + if ( count ) { + text = text + ' (' + count + ')'; + } + return $( '
' ).prop( { + id: 'mw-debug-' + id, + className: 'mw-debug-bit mw-debug-panelink' + } ) + .append( paneLabel( id, text ) ) + .appendTo( $bits ); + } + + paneTriggerBitDiv( 'console', 'Console', this.data.log.length ); + + paneTriggerBitDiv( 'querylist', 'Queries', this.data.queries.length ); + + paneTriggerBitDiv( 'debuglog', 'Debug log', this.data.debugLog.length ); + + paneTriggerBitDiv( 'request', 'Request' ); + + paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length ); + + paneTriggerBitDiv( 'profile', 'Profile', this.data.profile.length ); + + gitInfo = ''; + if ( this.data.gitRevision !== false ) { + gitInfo = '(' + this.data.gitRevision.slice( 0, 7 ) + ')'; + if ( this.data.gitViewUrl !== false ) { + gitInfo = $( '' ) + .attr( 'href', this.data.gitViewUrl ) + .text( gitInfo ); + } + } + + bitDiv( 'mwversion' ) + .append( $( 'MediaWiki' ) ) + .append( document.createTextNode( ': ' + this.data.mwVersion + ' ' ) ) + .append( gitInfo ); + + if ( this.data.gitBranch !== false ) { + bitDiv( 'gitbranch' ).text( 'Git branch: ' + this.data.gitBranch ); + } + + bitDiv( 'phpversion' ) + .append( $( this.data.phpEngine === 'HHVM' + ? 'HHVM' + : 'PHP' + ) ) + .append( ': ' + this.data.phpVersion ); + + bitDiv( 'time' ) + .text( 'Time: ' + this.data.time.toFixed( 5 ) ); + + bitDiv( 'memory' ) + .text( 'Memory: ' + this.data.memory + ' (Peak: ' + this.data.memoryPeak + ')' ); + + $bits.appendTo( $container ); + + panes = { + console: this.buildConsoleTable(), + querylist: this.buildQueryTable(), + debuglog: this.buildDebugLogTable(), + request: this.buildRequestPane(), + includes: this.buildIncludesPane(), + profile: this.buildProfilePane() + }; + + for ( id in panes ) { + if ( !panes.hasOwnProperty( id ) ) { + continue; + } + + $( '
' ) + .prop( { + className: 'mw-debug-pane', + id: 'mw-debug-pane-' + id + } ) + .append( panes[id] ) + .appendTo( $container ); + } + + this.$container = $container; + }, + + /** + * Build the console panel + */ + buildConsoleTable: function () { + var $table, entryTypeText, i, length, entry; + + $table = $( '' ); + + $( '' ).css( 'width', /* padding = */ 20 + ( 10 * /* fontSize = */ 11 ) ).appendTo( $table ); + $( '' ).appendTo( $table ); + $( '' ).css( 'width', 350 ).appendTo( $table ); + + entryTypeText = function ( entryType ) { + switch ( entryType ) { + case 'log': + return 'Log'; + case 'warn': + return 'Warning'; + case 'deprecated': + return 'Deprecated'; + default: + return 'Unknown'; + } + }; + + for ( i = 0, length = this.data.log.length; i < length; i += 1 ) { + entry = this.data.log[i]; + entry.typeText = entryTypeText( entry.type ); + + $( '' ) + .append( $( '' ) + .append( $( '' ).css( 'width', '4em' ) ) + .append( $( '' ) ) + .append( $( '' ).css( 'width', '8em' ) ) + .append( $( '' ).css( 'width', '18em' ) ) + .appendTo( $table ); + + for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) { + query = this.data.queries[i]; + + $( '' ) + .append( $( '
' ) + .text( entry.typeText ) + .addClass( 'mw-debug-console-' + entry.type ) + ) + .append( $( '' ).html( entry.msg ) ) + .append( $( '' ).text( entry.caller ) ) + .appendTo( $table ); + } + + return $table; + }, + + /** + * Build query list pane + * + * @return {jQuery} + */ + buildQueryTable: function () { + var $table, i, length, query; + + $table = $( '
' ); + + $( '
#SQLTimeCall
' ).text( i + 1 ) ) + .append( $( '' ).text( query.sql ) ) + .append( $( '' ).text( ( query.time * 1000 ).toFixed( 4 ) + 'ms' ) ) + .append( $( '' ).text( query['function'] ) ) + .appendTo( $table ); + } + + return $table; + }, + + /** + * Build legacy debug log pane + * + * @return {jQuery} + */ + buildDebugLogTable: function () { + var $list, i, length, line; + $list = $( '