diff options
Diffstat (limited to 'resources/mediawiki/mediawiki.Title.js')
-rw-r--r-- | resources/mediawiki/mediawiki.Title.js | 574 |
1 files changed, 402 insertions, 172 deletions
diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index b86a14ba..5038c515 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -1,192 +1,368 @@ /*! * @author Neil Kandalgaonkar, 2010 - * @author Timo Tijhof, 2011 + * @author Timo Tijhof, 2011-2013 * @since 1.18 - * - * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink */ ( function ( mw, $ ) { - /* Local space */ - /** * @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] Namespace id. If given, title will be taken as-is. + * 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 ) { - this.ns = 0; // integer namespace id - this.name = null; // name in canonical 'database' form - this.ext = null; // extension - - if ( arguments.length === 2 ) { - setNameAndExtension( this, title ); - this.ns = fixNsId( namespace ); - } else if ( arguments.length === 1 ) { - setAll( this, title ); + 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; } -var - /* Public methods (defined later) */ - fn, + /* Private members */ + + var /** - * Strip some illegal chars: control chars, colon, less than, greater than, - * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity - * intact, like unicode bidi chars, but it's a good start.. - * @ignore - * @param {string} s - * @return {string} + * @private + * @static + * @property NS_MAIN */ - clean = function ( s ) { - if ( s !== undefined ) { - return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' ); - } - }, + NS_MAIN = 0, /** - * Convert db-key to readable text. - * @ignore - * @param {string} s - * @return {string} + * @private + * @static + * @property NS_TALK */ - text = function ( s ) { - if ( s !== null && s !== undefined ) { - return s.replace( /_/g, ' ' ); - } else { - return ''; - } - }, + NS_TALK = 1, /** - * Sanitize name. - * @ignore + * @private + * @static + * @property NS_SPECIAL */ - fixName = function ( s ) { - return clean( $.trim( s ) ); - }, + NS_SPECIAL = -1, /** - * Sanitize extension. - * @ignore + * 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 */ - fixExt = function ( s ) { - return clean( s ); + 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]+;' + ), + /** - * Sanitize namespace id. - * @ignore - * @param id {Number} Namespace id. - * @return {Number|Boolean} The id as-is or boolean false if invalid. + * 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} */ - fixNsId = function ( id ) { - // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] ) - var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()]; + parse = function ( title, defaultNamespace ) { + var namespace, m, id, i, fragment, ext; - // Check only undefined (may be false-y, such as '' (main namespace) ). - if ( ns === undefined ) { + namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace; + + title = title + // Normalise whitespace to underscores and remove duplicates + .replace( /[ _\s]+/g, '_' ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + + if ( title === '' ) { return false; + } + + // Process initial colon + if ( 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]; + + // 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 { - return Number( id ); + fragment = title + // Get segment starting after the hash + .substr( 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 + .substr( 0, i ) + // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux") + .replace( rUnderscoreTrim, '' ); } - }, - /** - * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias). - * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. - * @ignore - * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). - * @return {Number|Boolean} Namespace id or boolean false if unrecognized. - */ - getNsIdByName = function ( ns ) { - // 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 nullwiki.example.org) - // Also, toLowerCase throws exception on null/undefined, because - // it is a String.prototype method. - if ( typeof ns !== 'string' ) { + + // Reject illegal characters + if ( title.match( rInvalid ) ) { return false; } - ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize - var id = mw.config.get( 'wgNamespaceIds' )[ns]; - if ( id === undefined ) { - mw.log( 'mw.Title: Unrecognized namespace: ' + ns ); + + // 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.substr( -2 ) === '/.' || + title.substr( -3 ) === '/..' + ) + ) { + return false; + } + + // Disallow magic tilde sequence + if ( title.indexOf( '~~~' ) !== -1 ) { + return false; + } + + // Disallow titles exceeding the 255 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 ) > 255 ) { + 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; } - return fixNsId( id ); + + // 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.substr( i + 1 ); + title = title.substr( 0, i ); + } + + return { + namespace: namespace, + title: title, + ext: ext, + fragment: fragment + }; }, /** - * Helper to extract namespace, name and extension from a string. + * Convert db-key to readable text. * - * @ignore - * @param {mw.Title} title - * @param {string} raw - * @return {mw.Title} + * @private + * @static + * @method text + * @param {string} s + * @return {string} */ - setAll = function ( title, s ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), - nsMatch = getNsIdByName( matches[1] ); - - // Namespace must be valid, and title must be a non-empty string. - if ( nsMatch && typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ns = nsMatch; - title.name = fixName( matches[2] ); - if ( typeof matches[3] === 'string' && matches[3] !== '' ) { - title.ext = fixExt( matches[3] ); - } + text = function ( s ) { + if ( s !== null && s !== undefined ) { + return s.replace( /_/g, ' ' ); } else { - // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace. - title.ns = 0; - setNameAndExtension( title, s ); + return ''; } - return title; }, + // 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 */ + /** - * Helper to extract name and extension from a string. + * Constructor for Title objects with a null return instead of an exception for invalid titles. * - * @ignore - * @param {mw.Title} title - * @param {string} raw - * @return {mw.Title} + * @static + * @method + * @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 */ - setNameAndExtension = function ( title, raw ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ ); - - // Title must be a non-empty string. - if ( typeof matches[1] === 'string' && matches[1] !== '' ) { - title.name = fixName( matches[1] ); - if ( typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ext = fixExt( matches[2] ); - } - } else { - throw new Error( 'mw.Title: Could not parse title "' + raw + '"' ); + Title.newFromText = function ( title, namespace ) { + var t, parsed = parse( title, namespace ); + if ( !parsed ) { + return null; } - return title; + + t = createObject( Title.prototype ); + t.namespace = parsed.namespace; + t.title = parsed.title; + t.ext = parsed.ext; + t.fragment = parsed.fragment; + + return t; }; + /** + * 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\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Thumbnails in non-hashed upload directories + /\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Full size images + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/, + + // Full-size images in non-hashed upload directories + /\/([^\s\/]+)$/ + ], - /* Static space */ + 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 {Mixed} title prefixed db-key name (string) or instance of Title - * @return {Mixed} Boolean true/false if the information is available. Otherwise null. + * @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 type = $.type( title ), obj = Title.exist.pages, match; + var match, + type = $.type( title ), + obj = Title.exist.pages; + if ( type === 'string' ) { match = obj[title]; } else if ( type === 'object' && title instanceof Title ) { @@ -194,23 +370,23 @@ var } 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; }; - /** - * @static - * @property - */ Title.exist = { /** + * Boolean true value indicates page does exist. + * * @static * @property {Object} exist.pages Keyed by PrefixedDb title. - * Boolean true value indicates page does exist. */ pages: {}, + /** * Example to declare existing titles: * Title.exist.set(['User:John_Doe', ...]); @@ -219,8 +395,8 @@ var * * @static * @property exist.set - * @param {string|Array} titles Title(s) in strict prefixedDb title form. - * @param {boolean} [state] State of the given titles. Defaults to true. + * @param {string|Array} titles Title(s) in strict prefixedDb title form + * @param {boolean} [state=true] State of the given titles * @return {boolean} */ set: function ( titles, state ) { @@ -234,42 +410,60 @@ var } }; - /* Public methods */ + /* Public members */ - fn = { + Title.prototype = { constructor: Title, /** - * Get the namespace number. + * Get the namespace number + * + * Example: 6 for "File:Example_image.svg". + * * @return {number} */ - getNamespaceId: function (){ - return this.ns; + getNamespaceId: function () { + return this.namespace; }, /** - * Get the namespace prefix (in the content-language). - * In NS_MAIN this is '', otherwise namespace name plus ':' + * 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 mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); + getNamespacePrefix: function () { + return this.namespace === NS_MAIN ? + '' : + ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' ); }, /** - * The name, like "Foo_bar" + * 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.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { - return this.name; + if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.title; } else { - return $.ucFirst( this.name ); + return $.ucFirst( this.title ); } }, /** - * The name, like "Foo bar" + * 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 () { @@ -277,24 +471,30 @@ var }, /** - * Get full name in prefixed DB form, like File:Foo_bar.jpg, - * most useful for API calls, anything that must identify the "title". - * @return {string} + * Get the extension of the page name (if any) + * + * @return {string|null} Name extension or null if there is none */ - getPrefixedDb: function () { - return this.getNamespacePrefix() + this.getMain(); + getExtension: function () { + return this.ext; }, /** - * Get full name in text form, like "File:Foo bar.jpg". + * Shortcut for appendable string to form the main page name. + * + * Returns a string like ".json", or "" if no extension. + * * @return {string} */ - getPrefixedText: function () { - return text( this.getPrefixedDb() ); + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; }, /** - * The main title (without namespace), like "Foo_bar.jpg" + * Get the main page name (transformed by #text) + * + * Example: "Example_image.svg" for "File:Example_image.svg". + * * @return {string} */ getMain: function () { @@ -302,7 +502,10 @@ var }, /** - * The "text" form, like "Foo bar.jpg" + * Get the main page name (transformed by #text) + * + * Example: "Example image.svg" for "File:Example_image.svg". + * * @return {string} */ getMainText: function () { @@ -310,46 +513,73 @@ var }, /** - * Get the extension (returns null if there was none) - * @return {string|null} + * Get the full page name + * + * Eaxample: "File:Example_image.svg". + * Most useful for API calls, anything that must identify the "title". + * + * @return {string} */ - getExtension: function () { - return this.ext; + getPrefixedDb: function () { + return this.getNamespacePrefix() + this.getMain(); }, /** - * Convenience method: return string like ".jpg", or "" if no extension + * Get the full page name (transformed by #text) + * + * Example: "File:Example image.svg" for "File:Example_image.svg". + * * @return {string} */ - getDotExtension: function () { - return this.ext === null ? '' : '.' + this.ext; + getPrefixedText: function () { + return text( this.getPrefixedDb() ); + }, + + /** + * 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; }, /** - * Return the URL to this title - * @see mw.util#wikiGetlink + * Get the URL to this title + * + * @see mw.util#getUrl * @return {string} */ getUrl: function () { - return mw.util.wikiGetlink( this.toString() ); + return mw.util.getUrl( this.toString() ); }, /** * Whether this title exists on the wiki. + * * @see #static-method-exists - * @return {boolean|null} If the information is available. Otherwise null. + * @return {boolean|null} Boolean if the information is available, otherwise null */ exists: function () { return Title.exists( this ); } }; - // Alias - fn.toString = fn.getPrefixedDb; - fn.toText = fn.getPrefixedText; + /** + * @alias #getPrefixedDb + * @method + */ + Title.prototype.toString = Title.prototype.getPrefixedDb; + - // Assign - Title.prototype = fn; + /** + * @alias #getPrefixedText + * @method + */ + Title.prototype.toText = Title.prototype.getPrefixedText; // Expose mw.Title = Title; |