protocol-relative URLs for javascript. Could also be extended to relative urls of...
authorNeil Kandalgaonkar <neilk@users.mediawiki.org>
Mon, 10 Oct 2011 23:15:16 +0000 (23:15 +0000)
committerNeil Kandalgaonkar <neilk@users.mediawiki.org>
Mon, 10 Oct 2011 23:15:16 +0000 (23:15 +0000)
resources/mediawiki/mediawiki.Uri.js
tests/jasmine/spec/mediawiki.Uri.spec.js

index 7ff8dda..192837e 100644 (file)
@@ -56,7 +56,7 @@
  *
  */
 
-( function( $ ) {
+( function( $, mw ) {
 
        /**
         * Function that's useful when constructing the URI string -- we frequently encounter the pattern of
                'fragment'   // top
        ];
 
-       /**
-        * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse.
-        * @constructor
-        * @param {!Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone). Object must have non-blank 'protocol', 'host', and 'path' properties.
-        * @param {Boolean} strict mode (when parsing a string)
-        */
-       mw.Uri = function( uri, strictMode ) {
-               strictMode = !!strictMode;
-               if ( uri !== undefined && uri !== null || uri !== '' ) {
-                       if ( typeof uri === 'string' ) {
-                               this._parse( uri, strictMode );
-                       } else if ( typeof uri === 'object' ) {
-                               var _this = this;
-                               $.each( properties, function( i, property ) {
-                                       _this[property] = uri[property];
-                               } );
-                               if ( this.query === undefined ) {
-                                       this.query = {};
-                               }
-                       }
-               }
-               if ( !( this.protocol && this.host && this.path ) ) {
-                       throw new Error( 'Bad constructor arguments' );
-               }
-       };
-
-       /**
-        * 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, but we also replace space with a +
-        * @param {String} string
-        * @return {String} encoded for URI
-        */
-       mw.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, '+' );
-       };
 
        /**
-        * Standard decodeURIComponent, with '+' to space
-        * @param {String} string encoded for URI
-        * @return {String} decoded string
+        * We use a factory to inject a document location, for relative URLs, including protocol-relative URLs.
+        * so the library is still testable & purely functional.
         */
-       mw.Uri.decode = function( s ) {
-               return decodeURIComponent( s ).replace( /\+/g, ' ' );
-       };
-
-       mw.Uri.prototype = {
+       mw.UriRelative = function( documentLocation ) {
 
                /**
-                * Parse a string and set our properties accordingly.
-                * @param {String} URI
-                * @param {Boolean} strictness
-                * @return {Boolean} success
+                * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse.
+                * @constructor
+                * @param {!Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone). Object must have non-blank 'protocol', 'host', and 'path' properties.
+                * @param {Boolean} strict mode (when parsing a string)
                 */
-               _parse: function( str, strictMode ) {
-                       var matches = parser[ strictMode ? 'strict' : 'loose' ].exec( str );
-                       var uri = this;
-                       $.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.
-                       var q = {};
-                       // using replace to iterate over a string
-                       if ( uri.query ) {
-                               uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) {
-                                       if ( $1 ) {
-                                               var k = mw.Uri.decode( $1 );
-                                               var v = ( $2 === '' || $2 === undefined ) ? null : mw.Uri.decode( $3 );
-                                               if ( typeof q[ k ] === 'string' ) {
-                                                       q[ k ] = [ q[ k ] ];
-                                               }
-                                               if ( typeof q[ k ] === 'object' ) {
-                                                       q[ k ].push( v );
-                                               } else {
-                                                       q[ k ] = v;
-                                               }
+               function Uri( uri, strictMode ) {
+                       strictMode = !!strictMode;
+                       if ( uri !== undefined && uri !== null || uri !== '' ) {
+                               if ( typeof uri === 'string' ) {
+                                       this._parse( uri, strictMode );
+                               } else if ( typeof uri === 'object' ) {
+                                       var _this = this;
+                                       $.each( properties, function( i, property ) {
+                                               _this[property] = uri[property];
+                                       } );
+                                       if ( this.query === undefined ) {
+                                               this.query = {};
                                        }
-                               } );
+                               }
                        }
-                       this.query = q;
-               },
 
-               /**
-                * Returns user and password portion of a URI.
-                * @return {String}
-                */
-               getUserInfo: function() {
-                       return cat( '', this.user, cat( ':', this.password, '' ) );
-               },
+                       // protocol-relative URLs
+                       if ( !this.protocol ) {
+                               this.protocol = defaultProtocol;
+                       }
 
-               /**
-                * Gets host and port portion of a URI.
-                * @return {String}
-                */
-               getHostPort: function() {
-                       return this.host + cat( ':', this.port, '' );
-               },
+                       if ( !( this.protocol && this.host && this.path ) ) {
+                               throw new Error( 'Bad constructor arguments' );
+                       }
+               }
 
                /**
-                * Returns the userInfo and host and port portion of the URI.
-                * In most real-world URLs, this is simply the hostname, but it is more general.
-                * @return {String}
+                * 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, but we also replace space with a +
+                * @param {String} string
+                * @return {String} encoded for URI
                 */
-               getAuthority: function() {
-                       return cat( '', this.getUserInfo(), '@' ) + this.getHostPort();
-               },
+               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, '+' );
+               };
 
                /**
-                * Returns the query arguments of the URL, encoded into a string
-                * Does not preserve the order of arguments passed into the URI. Does handle escaping.
-                * @return {String}
+                * Standard decodeURIComponent, with '+' to space
+                * @param {String} string encoded for URI
+                * @return {String} decoded string
                 */
-               getQueryString: function() {
-                       var args = [];
-                       $.each( this.query, function( key, val ) {
-                               var k = mw.Uri.encode( key );
-                               var vals = val === null ? [ null ] : $.makeArray( val );
-                               $.each( vals, function( i, v ) {
-                                       args.push( k + ( v === null ? '' : '=' + mw.Uri.encode( v ) ) );
+               Uri.decode = function( s ) {
+                       return decodeURIComponent( s ).replace( /\+/g, ' ' );
+               };
+
+               Uri.prototype = {
+
+                       /**
+                        * Parse a string and set our properties accordingly.
+                        * @param {String} URI
+                        * @param {Boolean} strictness
+                        * @return {Boolean} success
+                        */
+                       _parse: function( str, strictMode ) {
+                               var matches = parser[ strictMode ? 'strict' : 'loose' ].exec( str );
+                               var uri = this;
+                               $.each( properties, function( i, property ) {
+                                       uri[ property ] = matches[ i+1 ];
                                } );
-                       } );
-                       return args.join( '&' );
-               },
 
-               /**
-                * Returns everything after the authority section of the URI
-                * @return {String}
-                */
-               getRelativePath: function() {
-                       return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' );
-               },
+                               // 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.
+                               var q = {};
+                               // using replace to iterate over a string
+                               if ( uri.query ) {
+                                       uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) {
+                                               if ( $1 ) {
+                                                       var k = Uri.decode( $1 );
+                                                       var v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 );
+                                                       if ( typeof q[ k ] === 'string' ) {
+                                                               q[ k ] = [ q[ k ] ];
+                                                       }
+                                                       if ( typeof q[ k ] === 'object' ) {
+                                                               q[ k ].push( v );
+                                                       } else {
+                                                               q[ k ] = v;
+                                                       }
+                                               }
+                                       } );
+                               }
+                               this.query = q;
+                       },
 
-               /**
-                * Gets 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();
-               },
+                       /**
+                        * Returns user and password portion of a URI.
+                        * @return {String}
+                        */
+                       getUserInfo: function() {
+                               return cat( '', this.user, cat( ':', this.password, '' ) );
+                       },
 
-               /**
-                * Clone this URI
-                * @return {Object} new URI object with same properties
-                */
-               clone: function() {
-                       return new mw.Uri( this );
-               },
+                       /**
+                        * Gets host and port portion of a URI.
+                        * @return {String}
+                        */
+                       getHostPort: function() {
+                               return this.host + cat( ':', this.port, '' );
+                       },
 
-               /**
-                * Extend the query -- supply query parameters to override or add to ours
-                * @param {Object} query parameters in key-val form to override or add
-                * @return {Object} this URI object
-                */
-               extend: function( parameters ) {
-                       $.extend( this.query, parameters );
-                       return this;
-               }
+                       /**
+                        * Returns the userInfo and host and port portion of the URI.
+                        * In most real-world URLs, this is simply the hostname, but it is more general.
+                        * @return {String}
+                        */
+                       getAuthority: function() {
+                               return cat( '', this.getUserInfo(), '@' ) + this.getHostPort();
+                       },
+
+                       /**
+                        * Returns the query arguments of the URL, encoded into a string
+                        * Does not preserve the order of arguments passed into the URI. Does handle escaping.
+                        * @return {String}
+                        */
+                       getQueryString: function() {
+                               var args = [];
+                               $.each( this.query, function( key, val ) {
+                                       var k = Uri.encode( key );
+                                       var vals = val === null ? [ null ] : $.makeArray( val );
+                                       $.each( vals, function( i, v ) {
+                                               args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) );
+                                       } );
+                               } );
+                               return args.join( '&' );
+                       },
+
+                       /**
+                        * Returns everything after the authority section of the URI
+                        * @return {String}
+                        */
+                       getRelativePath: function() {
+                               return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' );
+                       },
+
+                       /**
+                        * Gets 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 -- supply query parameters to override or add to ours
+                        * @param {Object} query parameters in key-val form to override or add
+                        * @return {Object} this URI object
+                        */
+                       extend: function( parameters ) {
+                               $.extend( this.query, parameters );
+                               return this;
+                       }
+               };
+
+               var defaultProtocol = ( new Uri( documentLocation ) ).protocol;
+
+               return Uri;     
        };
 
-} )( jQuery );
+       // inject the current document location, for relative URLs
+       mw.Uri = mw.UriRelative( document.location.href );
+
+       
+
+} )( jQuery, mediaWiki );
index 978ab6d..a21ba67 100644 (file)
@@ -7,7 +7,7 @@
                        function basicTests( strict ) {
                        
                                describe( "should parse a simple HTTP URI correctly", function() { 
-                                       
+
                                        var uriString = 'http://www.ietf.org/rfc/rfc2396.txt';
                                        var uri;
                                        if ( strict ) {
 
                } );
 
+               describe( "should handle protocol-relative URLs", function() { 
+
+                       it ( "should create protocol-relative URLs with same protocol as document", function() {
+                               var uriRel = mw.UriRelative( 'glork://en.wiki.local/foo.php' );
+                               var uri = new uriRel( '//en.wiki.local/w/api.php' );
+                               expect( uri.protocol ).toEqual( 'glork' );
+                       } );
+
+               } );
+
                it( "should throw error on no arguments to constructor", function() {
                        expect( function() { 
                                var uri = new mw.Uri();
                        } ).toThrow( "Bad constructor arguments" );
                } );
 
-               it( "should throw error on URI without protocol as argument to constructor", function() {
+               it( "should throw error on URI without protocol or // in strict mode", function() {
                        expect( function() { 
-                               var uri = new mw.Uri( 'foo.com/bar/baz' );
+                               var uri = new mw.Uri( 'foo.com/bar/baz', true );
                        } ).toThrow( "Bad constructor arguments" );
                } );
 
+               it( "should normalize URI without protocol or // in loose mode", function() {
+                       var uri = new mw.Uri( 'foo.com/bar/baz', false );
+                       expect( uri.toString() ).toEqual( 'http://foo.com/bar/baz' );
+               } );
+
        } );
 
 } )();