Merge "Apply coding conventions for JavaScript"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.Title.js
index 95b18a8..5e594ad 100644 (file)
@@ -8,7 +8,7 @@
        /**
         * @class mw.Title
         *
-        * Parse titles into an object struture. Note that when using the constructor
+        * Parse titles into an object structure. 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.
         *
         */
        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).
                '|&#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.
         *
                        return false;
                }
 
-               // Disallow titles exceeding the 255 byte size limit (size of underlying database field)
+               // 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 ) > 255 ) {
+               if ( namespace !== NS_SPECIAL && $.byteLength( title ) > TITLE_MAX_BYTES ) {
                        return false;
                }
 
                }
        },
 
+       /**
+        * 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 ) {
         * Constructor for Title objects with a null return instead of an exception for invalid titles.
         *
         * @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
                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
         *