From 24ff6662195222479b4d83d41fa89edc8a3c05d1 Mon Sep 17 00:00:00 2001
From: Sem The most important aspect of URI's to understand is that URI's
- * and URL's are not strings. URI's are complex data structures that
- * encapsulate many pieces of information. The string version of a
- * URI is the serialized representation of that data structure. This
- * string serialization is used to provide a human readable
- * representation and a means to transport the data over the network
- * where it can then be parsed back into its' component parts. URI's fall into one of three categories:
- *
- *
The query and fragment parts are optional.
- * - *This class supports both non-hierarchical and hierarchical URI's
- * - *This class is intended to be used "as-is" for the vast majority - * of common URI's. However, if your application requires a custom - * URI syntax (e.g. custom query syntax or special handling of - * non-hierarchical URI's), this class can be fully subclassed. If you - * intended to subclass URI, please see the source code for complete - * documation on protected members and protected fuctions.
- * - * @langversion ActionScript 3.0 - * @playerversion Flash 9.0 - */ - public class URI - { - // Here we define which characters must be escaped for each - // URI part. The characters that must be escaped for each - // part differ depending on what would cause ambiguous parsing. - // RFC 3986 sec. 2.4 states that characters should only be - // encoded when they would conflict with subcomponent delimiters. - // We don't want to over-do the escaping. We only want to escape - // the minimum needed to prevent parsing problems. - - // space and % must be escaped in all cases. '%' is the delimiter - // for escaped characters. - public static const URImustEscape:String = " %"; - - // Baseline of what characters must be escaped - public static const URIbaselineEscape:String = URImustEscape + ":?#/@"; - - // Characters that must be escaped in the part part. - public static const URIpathEscape:String = URImustEscape + "?#"; - - // Characters that must be escaped in the query part, if setting - // the query as a whole string. If the query is set by - // name/value, URIqueryPartEscape is used instead. - public static const URIqueryEscape:String = URImustEscape + "#"; - - // This is what each name/value pair must escape "&=" as well - // so they don't conflict with the "param=value¶m2=value2" - // syntax. - public static const URIqueryPartEscape:String = URImustEscape + "#&="; - - // Non-hierarchical URI's can have query and fragment parts, but - // we also want to prevent '/' otherwise it might end up looking - // like a hierarchical URI to the parser. - public static const URInonHierEscape:String = URImustEscape + "?#/"; - - // Baseline uninitialized setting for the URI scheme. - public static const UNKNOWN_SCHEME:String = "unknown"; - - // The following bitmaps are used for performance enhanced - // character escaping. - - // Baseline characters that need to be escaped. Many parts use - // this. - protected static const URIbaselineExcludedBitmap:URIEncodingBitmap = - new URIEncodingBitmap(URIbaselineEscape); - - // Scheme escaping bitmap - protected static const URIschemeExcludedBitmap:URIEncodingBitmap = - URIbaselineExcludedBitmap; - - // User/pass escaping bitmap - protected static const URIuserpassExcludedBitmap:URIEncodingBitmap = - URIbaselineExcludedBitmap; - - // Authority escaping bitmap - protected static const URIauthorityExcludedBitmap:URIEncodingBitmap = - URIbaselineExcludedBitmap; - - // Port escaping bitmap - protected static const URIportExludedBitmap:URIEncodingBitmap = - URIbaselineExcludedBitmap; - - // Path escaping bitmap - protected static const URIpathExcludedBitmap:URIEncodingBitmap = - new URIEncodingBitmap(URIpathEscape); - - // Query (whole) escaping bitmap - protected static const URIqueryExcludedBitmap:URIEncodingBitmap = - new URIEncodingBitmap(URIqueryEscape); - - // Query (individual parts) escaping bitmap - protected static const URIqueryPartExcludedBitmap:URIEncodingBitmap = - new URIEncodingBitmap(URIqueryPartEscape); - - // Fragments are the last part in the URI. They only need to - // escape space, '#', and '%'. Turns out that is what query - // uses too. - protected static const URIfragmentExcludedBitmap:URIEncodingBitmap = - URIqueryExcludedBitmap; - - // Characters that need to be escaped in the non-hierarchical part - protected static const URInonHierexcludedBitmap:URIEncodingBitmap = - new URIEncodingBitmap(URInonHierEscape); - - // Values used by getRelation() - public static const NOT_RELATED:int = 0; - public static const CHILD:int = 1; - public static const EQUAL:int = 2; - public static const PARENT:int = 3; - - //------------------------------------------------------------------- - // protected class members - //------------------------------------------------------------------- - protected var _valid:Boolean = false; - protected var _relative:Boolean = false; - protected var _scheme:String = ""; - protected var _authority:String = ""; - protected var _username:String = ""; - protected var _password:String = ""; - protected var _port:String = ""; - protected var _path:String = ""; - protected var _query:String = ""; - protected var _fragment:String = ""; - protected var _nonHierarchical:String = ""; - protected static var _resolver:IURIResolver = null; - - - /** - * URI Constructor. If no string is given, this will initialize - * this URI object to a blank URI. - */ - public function URI(uri:String = null) : void - { - if (uri == null) - initialize(); - else - constructURI(uri); - } - - - /** - * @private - * Method that loads the URI from the given string. - */ - protected function constructURI(uri:String) : Boolean - { - if (!parseURI(uri)) - _valid = false; - - return isValid(); - } - - - /** - * @private Private initializiation. - */ - protected function initialize() : void - { - _valid = false; - _relative = false; - - _scheme = UNKNOWN_SCHEME; - _authority = ""; - _username = ""; - _password = ""; - _port = ""; - _path = ""; - _query = ""; - _fragment = ""; - - _nonHierarchical = ""; - } - - /** - * @private Accessor to explicitly set/get the hierarchical - * state of the URI. - */ - protected function set hierState(state:Boolean) : void - { - if (state) - { - // Clear the non-hierarchical data - _nonHierarchical = ""; - - // Also set the state vars while we are at it - if (_scheme == "" || _scheme == UNKNOWN_SCHEME) - _relative = true; - else - _relative = false; - - if (_authority.length == 0 && _path.length == 0) - _valid = false; - else - _valid = true; - } - else - { - // Clear the hierarchical data - _authority = ""; - _username = ""; - _password = ""; - _port = ""; - _path = ""; - - _relative = false; - - if (_scheme == "" || _scheme == UNKNOWN_SCHEME) - _valid = false; - else - _valid = true; - } - } - protected function get hierState() : Boolean - { - return (_nonHierarchical.length == 0); - } - - - /** - * @private Functions that performs some basic consistency validation. - */ - protected function validateURI() : Boolean - { - // Check the scheme - if (isAbsolute()) - { - if (_scheme.length <= 1 || _scheme == UNKNOWN_SCHEME) - { - // we probably parsed a C:\ type path or no scheme - return false; - } - else if (verifyAlpha(_scheme) == false) - return false; // Scheme contains bad characters - } - - if (hierState) - { - if (_path.search('\\') != -1) - return false; // local path - else if (isRelative() == false && _scheme == UNKNOWN_SCHEME) - return false; // It's an absolute URI, but it has a bad scheme - } - else - { - if (_nonHierarchical.search('\\') != -1) - return false; // some kind of local path - } - - // Looks like it's ok. - return true; - } - - - /** - * @private - * - * Given a URI in string format, parse that sucker into its basic - * components and assign them to this object. A URI is of the form: - *The URI specification allows for authentication - * credentials to be embedded in the URI as such:
- * - *http://user:passwd@host/path/to/file.htm
- * - *When setting this value, the string - * given is assumed to be unescaped. When retrieving this - * value, the resulting string is unescaped.
- */ - public function get username() : String - { - return URI.unescapeChars(_username); - } - public function set username(usernameStr:String) : void - { - _username = URI.fastEscapeChars(usernameStr, URI.URIuserpassExcludedBitmap); - - // Only hierarchical URI's can have a username. - this.hierState = true; - } - - - /** - * The password of the URI. Similar to username. - * @see URI.username - */ - public function get password() : String - { - return URI.unescapeChars(_password); - } - public function set password(passwordStr:String) : void - { - _password = URI.fastEscapeChars(passwordStr, - URI.URIuserpassExcludedBitmap); - - // Only hierarchical URI's can have a password. - this.hierState = true; - } - - - /** - * The host port number. Only valid for hierarchical URI's. If - * the URI is relative, this will be an empty string. URI's can - * contain the port number of the remote host: - * - *http://site.com:8080/index.htm
- */ - public function get port() : String - { - return URI.unescapeChars(_port); - } - public function set port(portStr:String) : void - { - _port = URI.escapeChars(portStr); - - // Only hierarchical URI's can have a port. - this.hierState = true; - } - - - /** - * The path portion of the URI. Only valid for hierarchical - * URI's. When setting this value, the string - * given is assumed to be unescaped. When retrieving this - * value, the resulting string is unescaped. - * - *The path portion can be in one of two formats. 1) an absolute - * path, or 2) a relative path. An absolute path starts with a - * slash ('/'), a relative path does not.
- * - *An absolute path may look like:
- *A relative path may look like:
- *Paths can be absolute or relative. Note that this not the same as - * an absolute or relative URI. An absolute URI can only have absolute - * paths. For example:
- * - *This absolute URI has an absolute path of "/path/to/file.htm".
- * - *Relative URI's can have either absolute paths or relative paths. - * All of the following relative URI's are valid:
- * - *This accessor should only be used if a custom query syntax - * is used. This URI class supports the common "param=value" - * style query syntax via the get/setQueryValue() and - * get/setQueryByMap() functions. Those functions should be used - * instead if the common syntax is being used. - * - *
The URI RFC does not specify any particular - * syntax for the query part of a URI. It is intended to allow - * any format that can be agreed upon by the two communicating hosts. - * However, most systems have standardized on the typical CGI - * format:
- * - *This class has specific support for this query syntax
- * - *This common query format is an array of name/value - * pairs with its own syntax that is different from the overall URI - * syntax. The query has its own escaping logic. For a query part - * to be properly escaped and unescaped, it must be split into its - * component parts. This accessor escapes/unescapes the entire query - * part without regard for it's component parts. This has the - * possibliity of leaving the query string in an ambiguious state in - * regards to its syntax. If the contents of the query part are - * important, it is recommended that get/setQueryValue() or - * get/setQueryByMap() are used instead.
- * - * If a different query syntax is being used, a subclass of URI - * can be created to handle that specific syntax. - * - * @see URI.getQueryValue, URI.getQueryByMap - */ - public function get query() : String - { - return URI.unescapeChars(_query); - } - public function set query(queryStr:String) : void - { - _query = URI.fastEscapeChars(queryStr, URI.URIqueryExcludedBitmap); - - // both hierarchical and non-hierarchical URI's can - // have a query. Do not set the hierState. - } - - /** - * Accessor to the raw query data. If you are using a custom query - * syntax, this accessor can be used to get and set the query part - * directly with no escaping/unescaping. This should ONLY be used - * if your application logic is handling custom query logic and - * handling the proper escaping of the query part. - */ - public function get queryRaw() : String - { - return _query; - } - public function set queryRaw(queryStr:String) : void - { - _query = queryStr; - } - - - /** - * The fragment (anchor) portion of the URI. This is valid for - * both hierarchical and non-hierarchical URI's. - */ - public function get fragment() : String - { - return URI.unescapeChars(_fragment); - } - public function set fragment(fragmentStr:String) : void - { - _fragment = URI.fastEscapeChars(fragmentStr, URIfragmentExcludedBitmap); - - // both hierarchical and non-hierarchical URI's can - // have a fragment. Do not set the hierState. - } - - - /** - * The non-hierarchical part of the URI. For example, if - * this URI object represents "mailto:somebody@company.com", - * this will contain "somebody@company.com". This is valid only - * for non-hierarchical URI's. - */ - public function get nonHierarchical() : String - { - return URI.unescapeChars(_nonHierarchical); - } - public function set nonHierarchical(nonHier:String) : void - { - _nonHierarchical = URI.fastEscapeChars(nonHier, URInonHierexcludedBitmap); - - // This is a non-hierarchical URI. - this.hierState = false; - } - - - /** - * Quick shorthand accessor to set the parts of this URI. - * The given parts are assumed to be in unescaped form. If - * the URI is non-hierarchical (e.g. mailto:) you will need - * to call SetScheme() and SetNonHierarchical(). - */ - public function setParts(schemeStr:String, authorityStr:String, - portStr:String, pathStr:String, queryStr:String, - fragmentStr:String) : void - { - this.scheme = schemeStr; - this.authority = authorityStr; - this.port = portStr; - this.path = pathStr; - this.query = queryStr; - this.fragment = fragmentStr; - - hierState = true; - } - - - /** - * URI escapes the given character string. This is similar in function - * to the global encodeURIComponent() function in ActionScript, but is - * slightly different in regards to which characters get escaped. This - * escapes the characters specified in the URIbaselineExluded set (see class - * static members). This is needed for this class to work properly. - * - *If a different set of characters need to be used for the escaping, - * you may use fastEscapeChars() and specify a custom URIEncodingBitmap - * that contains the characters your application needs escaped.
- * - *Never pass a full URI to this function. A URI can only be properly - * escaped/unescaped when split into its component parts (see RFC 3986 - * section 2.4). This is due to the fact that the URI component separators - * could be characters that would normally need to be escaped.
- * - * @param unescaped character string to be escaped. - * - * @return escaped character string - * - * @see encodeURIComponent - * @see fastEscapeChars - */ - static public function escapeChars(unescaped:String) : String - { - // This uses the excluded set by default. - return fastEscapeChars(unescaped, URI.URIbaselineExcludedBitmap); - } - - - /** - * Unescape any URI escaped characters in the given character - * string. - * - *Never pass a full URI to this function. A URI can only be properly - * escaped/unescaped when split into its component parts (see RFC 3986 - * section 2.4). This is due to the fact that the URI component separators - * could be characters that would normally need to be escaped.
- * - * @param escaped the escaped string to be unescaped. - * - * @return unescaped string. - */ - static public function unescapeChars(escaped:String /*, onlyHighASCII:Boolean = false*/) : String - { - // We can just use the default AS function. It seems to - // decode everything correctly - var unescaped:String; - unescaped = decodeURIComponent(escaped); - return unescaped; - } - - /** - * Performance focused function that escapes the given character - * string using the given URIEncodingBitmap as the rule for what - * characters need to be escaped. This function is used by this - * class and can be used externally to this class to perform - * escaping on custom character sets. - * - *Never pass a full URI to this function. A URI can only be properly - * escaped/unescaped when split into its component parts (see RFC 3986 - * section 2.4). This is due to the fact that the URI component separators - * could be characters that would normally need to be escaped.
- * - * @param unescaped the unescaped string to be escaped - * @param bitmap the set of characters that need to be escaped - * - * @return the escaped string. - */ - static public function fastEscapeChars(unescaped:String, bitmap:URIEncodingBitmap) : String - { - var escaped:String = ""; - var c:String; - var x:int, i:int; - - for (i = 0; i < unescaped.length; i++) - { - c = unescaped.charAt(i); - - x = bitmap.ShouldEscape(c); - if (x) - { - c = x.toString(16); - if (c.length == 1) - c = "0" + c; - - c = "%" + c; - c = c.toUpperCase(); - } - - escaped += c; - } - - return escaped; - } - - - /** - * Is this URI of a particular scheme type? For example, - * passing "http" to a URI object that represents the URI - * "http://site.com/" would return true. - * - * @param scheme scheme to check for - * - * @return true if this URI object is of the given type, false - * otherwise. - */ - public function isOfType(scheme:String) : Boolean - { - // Schemes are never case sensitive. Ignore case. - scheme = scheme.toLowerCase(); - return (this._scheme == scheme); - } - - - /** - * Get the value for the specified named in the query part. This - * assumes the query part of the URI is in the common - * "name1=value1&name2=value2" syntax. Do not call this function - * if you are using a custom query syntax. - * - * @param name name of the query value to get. - * - * @return the value of the query name, empty string if the - * query name does not exist. - */ - public function getQueryValue(name:String) : String - { - var map:Object; - var item:String; - var value:String; - - map = getQueryByMap(); - - for (item in map) - { - if (item == name) - { - value = map[item]; - return value; - } - } - - // Didn't find the specified key - return new String(""); - } - - - /** - * Set the given value on the given query name. If the given name - * does not exist, it will automatically add this name/value pair - * to the query. If null is passed as the value, it will remove - * the given item from the query. - * - *This automatically escapes any characters that may conflict with - * the query syntax so that they are "safe" within the query. The - * strings passed are assumed to be literal unescaped name and value.
- * - * @param name name of the query value to set - * @param value value of the query item to set. If null, this will - * force the removal of this item from the query. - */ - public function setQueryValue(name:String, value:String) : void - { - var map:Object; - - map = getQueryByMap(); - - // If the key doesn't exist yet, this will create a new pair in - // the map. If it does exist, this will overwrite the previous - // value, which is what we want. - map[name] = value; - - setQueryByMap(map); - } - - - /** - * Get the query of the URI in an Object class that allows for easy - * access to the query data via Object accessors. For example: - * - *Sometimes URI's are constructed by hand using strings outside - * this class. In those cases, it is unlikely the URI has been - * properly escaped. This function forcefully escapes this URI - * by unescaping each part and then re-escaping it. If the URI - * did not have any escaping, the first unescape will do nothing - * and then the re-escape will properly escape everything. If - * the URI was already escaped, the unescape and re-escape will - * essentally be a no-op. This provides a safe way to make sure - * a URI is in the proper escaped form.
- */ - public function forceEscape() : void - { - // The accessors for each of the members will unescape - // and then re-escape as we get and assign them. - - // Handle the parts that are common for both hierarchical - // and non-hierarchical URI's - this.scheme = this.scheme; - this.setQueryByMap(this.getQueryByMap()); - this.fragment = this.fragment; - - if (isHierarchical()) - { - this.authority = this.authority; - this.path = this.path; - this.port = this.port; - this.username = this.username; - this.password = this.password; - } - else - { - this.nonHierarchical = this.nonHierarchical; - } - } - - - /** - * Does this URI point to a resource of the given file type? - * Given a file extension (or just a file name, this will strip the - * extension), check to see if this URI points to a file of that - * type. - * - * @param extension string that contains a file extension with or - * without a dot ("html" and ".html" are both valid), or a file - * name with an extension (e.g. "index.html"). - * - * @return true if this URI points to a resource with the same file - * file extension as the extension provided, false otherwise. - */ - public function isOfFileType(extension:String) : Boolean - { - var thisExtension:String; - var index:int; - - index = extension.lastIndexOf("."); - if (index != -1) - { - // Strip the extension - extension = extension.substr(index + 1); - } - else - { - // The caller passed something without a dot in it. We - // will assume that it is just a plain extension (e.g. "html"). - // What they passed is exactly what we want - } - - thisExtension = getExtension(true); - - if (thisExtension == "") - return false; - - // Compare the extensions ignoring case - if (compareStr(thisExtension, extension, false) == 0) - return true; - else - return false; - } - - - /** - * Get the ".xyz" file extension from the filename in the URI. - * For example, if we have the following URI: - * - *This will return ".html".
- * - * @param minusDot If true, this will strip the dot from the extension. - * If true, the above example would have returned "html". - * - * @return the file extension - */ - public function getExtension(minusDot:Boolean = false) : String - { - var filename:String = getFilename(); - var extension:String; - var index:int; - - if (filename == "") - return String(""); - - index = filename.lastIndexOf("."); - - // If it doesn't have an extension, or if it is a "hidden" file, - // it doesn't have an extension. Hidden files on unix start with - // a dot (e.g. ".login"). - if (index == -1 || index == 0) - return String(""); - - extension = filename.substr(index); - - // If the caller does not want the dot, remove it. - if (minusDot && extension.charAt(0) == ".") - extension = extension.substr(1); - - return extension; - } - - /** - * Quick function to retrieve the file name off the end of a URI. - * - *For example, if the URI is:
- *this function will return "file.html".
- * - * @param minusExtension true if the file extension should be stripped - * - * @return the file name. If this URI is a directory, the return - * value will be empty string. - */ - public function getFilename(minusExtension:Boolean = false) : String - { - if (isDirectory()) - return String(""); - - var pathStr:String = this.path; - var filename:String; - var index:int; - - // Find the last path separator. - index = pathStr.lastIndexOf("/"); - - if (index != -1) - filename = pathStr.substr(index + 1); - else - filename = pathStr; - - if (minusExtension) - { - // The caller has requested that the extension be removed - index = filename.lastIndexOf("."); - - if (index != -1) - filename = filename.substr(0, index); - } - - return filename; - } - - - /** - * @private - * Helper function to compare strings. - * - * @return true if the two strings are identical, false otherwise. - */ - static protected function compareStr(str1:String, str2:String, - sensitive:Boolean = true) : Boolean - { - if (sensitive == false) - { - str1 = str1.toLowerCase(); - str2 = str2.toLowerCase(); - } - - return (str1 == str2) - } - - /** - * Based on the type of this URI (http, ftp, etc.) get - * the default port used for that protocol. This is - * just intended to be a helper function for the most - * common cases. - */ - public function getDefaultPort() : String - { - if (_scheme == "http") - return String("80"); - else if (_scheme == "ftp") - return String("21"); - else if (_scheme == "file") - return String(""); - else if (_scheme == "sftp") - return String("22"); // ssh standard port - else - { - // Don't know the port for this URI type - return String(""); - } - } - - /** - * @private - * - * This resolves the given URI if the application has a - * resolver interface defined. This function does not - * modify the passed in URI and returns a new URI. - */ - static protected function resolve(uri:URI) : URI - { - var copy:URI = new URI(); - copy.copyURI(uri); - - if (_resolver != null) - { - // A resolver class has been registered. Call it. - return _resolver.resolve(copy); - } - else - { - // No resolver. Nothing to do, but we don't - // want to reuse the one passed in. - return copy; - } - } - - /** - * Accessor to set and get the resolver object used by all URI - * objects to dynamically resolve URI's before comparison. - */ - static public function set resolver(resolver:IURIResolver) : void - { - _resolver = resolver; - } - static public function get resolver() : IURIResolver - { - return _resolver; - } - - /** - * Given another URI, return this URI object's relation to the one given. - * URI's can have 1 of 4 possible relationships. They can be unrelated, - * equal, parent, or a child of the given URI. - * - * @param uri URI to compare this URI object to. - * @param caseSensitive true if the URI comparison should be done - * taking case into account, false if the comparison should be - * performed case insensitive. - * - * @return URI.NOT_RELATED, URI.CHILD, URI.PARENT, or URI.EQUAL - */ - public function getRelation(uri:URI, caseSensitive:Boolean = true) : int - { - // Give the app a chance to resolve these URI's before we compare them. - var thisURI:URI = URI.resolve(this); - var thatURI:URI = URI.resolve(uri); - - if (thisURI.isRelative() || thatURI.isRelative()) - { - // You cannot compare relative URI's due to their lack of context. - // You could have two relative URI's that look like: - // ../../images/ - // ../../images/marketing/logo.gif - // These may appear related, but you have no overall context - // from which to make the comparison. The first URI could be - // from one site and the other URI could be from another site. - return URI.NOT_RELATED; - } - else if (thisURI.isHierarchical() == false || thatURI.isHierarchical() == false) - { - // One or both of the URI's are non-hierarchical. - if (((thisURI.isHierarchical() == false) && (thatURI.isHierarchical() == true)) || - ((thisURI.isHierarchical() == true) && (thatURI.isHierarchical() == false))) - { - // XOR. One is hierarchical and the other is - // non-hierarchical. They cannot be compared. - return URI.NOT_RELATED; - } - else - { - // They are both non-hierarchical - if (thisURI.scheme != thatURI.scheme) - return URI.NOT_RELATED; - - if (thisURI.nonHierarchical != thatURI.nonHierarchical) - return URI.NOT_RELATED; - - // The two non-hierarcical URI's are equal. - return URI.EQUAL; - } - } - - // Ok, this URI and the one we are being compared to are both - // absolute hierarchical URI's. - - if (thisURI.scheme != thatURI.scheme) - return URI.NOT_RELATED; - - if (thisURI.authority != thatURI.authority) - return URI.NOT_RELATED; - - var thisPort:String = thisURI.port; - var thatPort:String = thatURI.port; - - // Different ports are considered completely different servers. - if (thisPort == "") - thisPort = thisURI.getDefaultPort(); - if (thatPort == "") - thatPort = thatURI.getDefaultPort(); - - // Check to see if the port is the default port. - if (thisPort != thatPort) - return URI.NOT_RELATED; - - if (compareStr(thisURI.path, thatURI.path, caseSensitive)) - return URI.EQUAL; - - // Special case check. If we are here, the scheme, authority, - // and port match, and it is not a relative path, but the - // paths did not match. There is a special case where we - // could have: - // http://something.com/ - // http://something.com - // Technically, these are equal. So lets, check for this case. - var thisPath:String = thisURI.path; - var thatPath:String = thatURI.path; - - if ( (thisPath == "/" || thatPath == "/") && - (thisPath == "" || thatPath == "") ) - { - // We hit the special case. These two are equal. - return URI.EQUAL; - } - - // Ok, the paths do not match, but one path may be a parent/child - // of the other. For example, we may have: - // http://something.com/path/to/homepage/ - // http://something.com/path/to/homepage/images/logo.gif - // In this case, the first is a parent of the second (or the second - // is a child of the first, depending on which you compare to the - // other). To make this comparison, we must split the path into - // its component parts (split the string on the '/' path delimiter). - // We then compare the - var thisParts:Array, thatParts:Array; - var thisPart:String, thatPart:String; - var i:int; - - thisParts = thisPath.split("/"); - thatParts = thatPath.split("/"); - - if (thisParts.length > thatParts.length) - { - thatPart = thatParts[thatParts.length - 1]; - if (thatPart.length > 0) - { - // if the last part is not empty, the passed URI is - // not a directory. There is no way the passed URI - // can be a parent. - return URI.NOT_RELATED; - } - else - { - // Remove the empty trailing part - thatParts.pop(); - } - - // This may be a child of the one passed in - for (i = 0; i < thatParts.length; i++) - { - thisPart = thisParts[i]; - thatPart = thatParts[i]; - - if (compareStr(thisPart, thatPart, caseSensitive) == false) - return URI.NOT_RELATED; - } - - return URI.CHILD; - } - else if (thisParts.length < thatParts.length) - { - thisPart = thisParts[thisParts.length - 1]; - if (thisPart.length > 0) - { - // if the last part is not empty, this URI is not a - // directory. There is no way this object can be - // a parent. - return URI.NOT_RELATED; - } - else - { - // Remove the empty trailing part - thisParts.pop(); - } - - // This may be the parent of the one passed in - for (i = 0; i < thisParts.length; i++) - { - thisPart = thisParts[i]; - thatPart = thatParts[i]; - - if (compareStr(thisPart, thatPart, caseSensitive) == false) - return URI.NOT_RELATED; - } - - return URI.PARENT; - } - else - { - // Both URI's have the same number of path components, but - // it failed the equivelence check above. This means that - // the two URI's are not related. - return URI.NOT_RELATED; - } - - // If we got here, the scheme and authority are the same, - // but the paths pointed to two different locations that - // were in different parts of the file system tree - return URI.NOT_RELATED; - } - - /** - * Given another URI, return the common parent between this one - * and the provided URI. - * - * @param uri the other URI from which to find a common parent - * @para caseSensitive true if this operation should be done - * with case sensitive comparisons. - * - * @return the parent URI if successful, null otherwise. - */ - public function getCommonParent(uri:URI, caseSensitive:Boolean = true) : URI - { - var thisURI:URI = URI.resolve(this); - var thatURI:URI = URI.resolve(uri); - - if(!thisURI.isAbsolute() || !thatURI.isAbsolute() || - thisURI.isHierarchical() == false || - thatURI.isHierarchical() == false) - { - // Both URI's must be absolute hierarchical for this to - // make sense. - return null; - } - - var relation:int = thisURI.getRelation(thatURI); - if (relation == URI.NOT_RELATED) - { - // The given URI is not related to this one. No - // common parent. - return null; - } - - thisURI.chdir("."); - thatURI.chdir("."); - - var strBefore:String, strAfter:String; - do - { - relation = thisURI.getRelation(thatURI, caseSensitive); - if(relation == URI.EQUAL || relation == URI.PARENT) - break; - - // If strBefore and strAfter end up being the same, - // we know we are at the root of the path because - // chdir("..") is doing nothing. - strBefore = thisURI.toString(); - thisURI.chdir(".."); - strAfter = thisURI.toString(); - } - while(strBefore != strAfter); - - return thisURI; - } - - - /** - * This function is used to move around in a URI in a way similar - * to the 'cd' or 'chdir' commands on Unix. These operations are - * completely string based, using the context of the URI to - * determine the position within the path. The heuristics used - * to determine the action are based off Appendix C in RFC 2396. - * - *URI paths that end in '/' are considered paths that point to
- * directories, while paths that do not end in '/' are files. For
- * example, if you execute chdir("d") on the following URI's:
- * 1. http://something.com/a/b/c/ (directory)
- * 2. http://something.com/a/b/c (not directory)
- * you will get:
- * 1. http://something.com/a/b/c/d
- * 2. http://something.com/a/b/d
See RFC 2396, Appendix C for more info.
- * - * @param reference the URI or path to "cd" to. - * @param escape true if the passed reference string should be URI - * escaped before using it. - * - * @return true if the chdir was successful, false otherwise. - */ - public function chdir(reference:String, escape:Boolean = false) : Boolean - { - var uriReference:URI; - var ref:String = reference; - - if (escape) - ref = URI.escapeChars(reference); - - if (ref == "") - { - // NOOP - return true; - } - else if (ref.substr(0, 2) == "//") - { - // Special case. This is an absolute URI but without the scheme. - // Take the scheme from this URI and tack it on. This is - // intended to make working with chdir() a little more - // tolerant. - var final:String = this.scheme + ":" + ref; - - return constructURI(final); - } - else if (ref.charAt(0) == "?") - { - // A relative URI that is just a query part is essentially - // a "./?query". We tack on the "./" here to make the rest - // of our logic work. - ref = "./" + ref; - } - - // Parse the reference passed in as a URI. This way we - // get any query and fragments parsed out as well. - uriReference = new URI(ref); - - if (uriReference.isAbsolute() || - uriReference.isHierarchical() == false) - { - // If the URI given is a full URI, it replaces this one. - copyURI(uriReference); - return true; - } - - - var thisPath:String, thatPath:String; - var thisParts:Array, thatParts:Array; - var thisIsDir:Boolean = false, thatIsDir:Boolean = false; - var thisIsAbs:Boolean = false, thatIsAbs:Boolean = false; - var lastIsDotOperation:Boolean = false; - var curDir:String; - var i:int; - - thisPath = this.path; - thatPath = uriReference.path; - - if (thisPath.length > 0) - thisParts = thisPath.split("/"); - else - thisParts = new Array(); - - if (thatPath.length > 0) - thatParts = thatPath.split("/"); - else - thatParts = new Array(); - - if (thisParts.length > 0 && thisParts[0] == "") - { - thisIsAbs = true; - thisParts.shift(); // pop the first one off the array - } - if (thisParts.length > 0 && thisParts[thisParts.length - 1] == "") - { - thisIsDir = true; - thisParts.pop(); // pop the last one off the array - } - - if (thatParts.length > 0 && thatParts[0] == "") - { - thatIsAbs = true; - thatParts.shift(); // pop the first one off the array - } - if (thatParts.length > 0 && thatParts[thatParts.length - 1] == "") - { - thatIsDir = true; - thatParts.pop(); // pop the last one off the array - } - - if (thatIsAbs) - { - // The reference is an absolute path (starts with a slash). - // It replaces this path wholesale. - this.path = uriReference.path; - - // And it inherits the query and fragment - this.queryRaw = uriReference.queryRaw; - this.fragment = uriReference.fragment; - - return true; - } - else if (thatParts.length == 0 && uriReference.query == "") - { - // The reference must have only been a fragment. Fragments just - // get appended to whatever the current path is. We don't want - // to overwrite any query that may already exist, so this case - // only takes on the new fragment. - this.fragment = uriReference.fragment; - return true; - } - else if (thisIsDir == false && thisParts.length > 0) - { - // This path ends in a file. It goes away no matter what. - thisParts.pop(); - } - - // By default, this assumes the query and fragment of the reference - this.queryRaw = uriReference.queryRaw; - this.fragment = uriReference.fragment; - - // Append the parts of the path from the passed in reference - // to this object's path. - thisParts = thisParts.concat(thatParts); - - for(i = 0; i < thisParts.length; i++) - { - curDir = thisParts[i]; - lastIsDotOperation = false; - - if (curDir == ".") - { - thisParts.splice(i, 1); - i = i - 1; // account for removing this item - lastIsDotOperation = true; - } - else if (curDir == "..") - { - if (i >= 1) - { - if (thisParts[i - 1] == "..") - { - // If the previous is a "..", we must have skipped - // it due to this URI being relative. We can't - // collapse leading ".."s in a relative URI, so - // do nothing. - } - else - { - thisParts.splice(i - 1, 2); - i = i - 2; // move back to account for the 2 we removed - } - } - else - { - // This is the first thing in the path. - - if (isRelative()) - { - // We can't collapse leading ".."s in a relative - // path. Do noting. - } - else - { - // This is an abnormal case. We have dot-dotted up - // past the base of our "file system". This is a - // case where we had a /path/like/this.htm and were - // given a path to chdir to like this: - // ../../../../../../mydir - // Obviously, it has too many ".." and will take us - // up beyond the top of the URI. However, according - // RFC 2396 Appendix C.2, we should try to handle - // these abnormal cases appropriately. In this case, - // we will do what UNIX command lines do if you are - // at the root (/) of the filesystem and execute: - // # cd ../../../../../bin - // Which will put you in /bin. Essentially, the extra - // ".."'s will just get eaten. - - thisParts.splice(i, 1); - i = i - 1; // account for the ".." we just removed - } - } - - lastIsDotOperation = true; - } - } - - var finalPath:String = ""; - - // If the last thing in the path was a "." or "..", then this thing is a - // directory. If the last thing isn't a dot-op, then we don't want to - // blow away any information about the directory (hence the "|=" binary - // assignment). - thatIsDir = thatIsDir || lastIsDotOperation; - - // Reconstruct the path with the abs/dir info we have - finalPath = joinPath(thisParts, thisIsAbs, thatIsDir); - - // Set the path (automatically escaping it) - this.path = finalPath; - - return true; - } - - /** - * @private - * Join an array of path parts back into a URI style path string. - * This is used by the various path logic functions to recombine - * a path. This is different than the standard Array.join() - * function because we need to take into account the starting and - * ending path delimiters if this is an absolute path or a - * directory. - * - * @param parts the Array that contains strings of each path part. - * @param isAbs true if the given path is absolute - * @param isDir true if the given path is a directory - * - * @return the combined path string. - */ - protected function joinPath(parts:Array, isAbs:Boolean, isDir:Boolean) : String - { - var pathStr:String = ""; - var i:int; - - for (i = 0; i < parts.length; i++) - { - if (pathStr.length > 0) - pathStr += "/"; - - pathStr += parts[i]; - } - - // If this path is a directory, tack on the directory delimiter, - // but only if the path contains something. Adding this to an - // empty path would make it "/", which is an absolute path that - // starts at the root. - if (isDir && pathStr.length > 0) - pathStr += "/"; - - if (isAbs) - pathStr = "/" + pathStr; - - return pathStr; - } - - /** - * Given an absolute URI, make this relative URI absolute using - * the given URI as a base. This URI instance must be relative - * and the base_uri must be absolute. - * - * @param base_uri URI to use as the base from which to make - * this relative URI into an absolute URI. - * - * @return true if successful, false otherwise. - */ - public function makeAbsoluteURI(base_uri:URI) : Boolean - { - if (isAbsolute() || base_uri.isRelative()) - { - // This URI needs to be relative, and the base needs to be - // absolute otherwise we won't know what to do! - return false; - } - - // Make a copy of the base URI. We don't want to modify - // the passed URI. - var base:URI = new URI(); - base.copyURI(base_uri); - - // ChDir on the base URI. This will preserve any query - // and fragment we have. - if (base.chdir(toString()) == false) - return false; - - // It worked, so copy the base into this one - copyURI(base); - - return true; - } - - - /** - * Given a URI to use as a base from which this object should be - * relative to, convert this object into a relative URI. For example, - * if you have: - * - *uri1 will have a final value of "../some/file.html"
- * - *Note! This function is brute force. If you have two URI's - * that are completely unrelated, this will still attempt to make - * the relative URI. In that case, you will most likely get a - * relative path that looks something like:
- * - *../../../../../../some/path/to/my/file.html
- * - * @param base_uri the URI from which to make this URI relative - * - * @return true if successful, false if the base_uri and this URI - * are not related, of if error. - */ - public function makeRelativeURI(base_uri:URI, caseSensitive:Boolean = true) : Boolean - { - var base:URI = new URI(); - base.copyURI(base_uri); - - var thisParts:Array, thatParts:Array; - var finalParts:Array = new Array(); - var thisPart:String, thatPart:String, finalPath:String; - var pathStr:String = this.path; - var queryStr:String = this.queryRaw; - var fragmentStr:String = this.fragment; - var i:int; - var diff:Boolean = false; - var isDir:Boolean = false; - - if (isRelative()) - { - // We're already relative. - return true; - } - - if (base.isRelative()) - { - // The base is relative. A relative base doesn't make sense. - return false; - } - - - if ( (isOfType(base_uri.scheme) == false) || - (this.authority != base_uri.authority) ) - { - // The schemes and/or authorities are different. We can't - // make a relative path to something that is completely - // unrelated. - return false; - } - - // Record the state of this URI - isDir = isDirectory(); - - // We are based of the directory of the given URI. We need to - // make sure the URI is pointing to a directory. Changing - // directory to "." will remove any file name if the base is - // not a directory. - base.chdir("."); - - thisParts = pathStr.split("/"); - thatParts = base.path.split("/"); - - if (thisParts.length > 0 && thisParts[0] == "") - thisParts.shift(); - - if (thisParts.length > 0 && thisParts[thisParts.length - 1] == "") - { - isDir = true; - thisParts.pop(); - } - - if (thatParts.length > 0 && thatParts[0] == "") - thatParts.shift(); - if (thatParts.length > 0 && thatParts[thatParts.length - 1] == "") - thatParts.pop(); - - - // Now that we have the paths split into an array of directories, - // we can compare the two paths. We start from the left of side - // of the path and start comparing. When we either run out of - // directories (one path is longer than the other), or we find - // a directory that is different, we stop. The remaining parts - // of each path is then used to determine the relative path. For - // example, lets say we have: - // path we want to make relative: /a/b/c/d/e.txt - // path to use as base for relative: /a/b/f/ - // - // This loop will start at the left, and remove directories - // until we get a mismatch or run off the end of one of them. - // In this example, the result will be: - // c/d/e.txt - // f - // - // For every part left over in the base path, we prepend a ".." - // to the relative to get the final path: - // ../c/d/e.txt - while(thatParts.length > 0) - { - if (thisParts.length == 0) - { - // we matched all there is to match, we are done. - // This is the case where "this" object is a parent - // path of the given URI. eg: - // this.path = /a/b/ (thisParts) - // base.path = /a/b/c/d/e/ (thatParts) - break; - } - - thisPart = thisParts[0]; - thatPart = thatParts[0]; - - if (compareStr(thisPart, thatPart, caseSensitive)) - { - thisParts.shift(); - thatParts.shift(); - } - else - break; - } - - // If there are any path info left from the base URI, that means - // **this** object is above the given URI in the file tree. For - // each part left over in the given URI, we need to move up one - // directory to get where we are. - var dotdot:String = ".."; - for (i = 0; i < thatParts.length; i++) - { - finalParts.push(dotdot); - } - - // Append the parts of this URI to any dot-dot's we have - finalParts = finalParts.concat(thisParts); - - // Join the parts back into a path - finalPath = joinPath(finalParts, false /* not absolute */, isDir); - - if (finalPath.length == 0) - { - // The two URI's are exactly the same. The proper relative - // path is: - finalPath = "./"; - } - - // Set the parts of the URI, preserving the original query and - // fragment parts. - setParts("", "", "", finalPath, queryStr, fragmentStr); - - return true; - } - - /** - * Given a string, convert it to a URI. The string could be a - * full URI that is improperly escaped, a malformed URI (e.g. - * missing a protocol like "www.something.com"), a relative URI, - * or any variation there of. - * - *The intention of this function is to take anything that a - * user might manually enter as a URI/URL and try to determine what - * they mean. This function differs from the URI constructor in - * that it makes some assumptions to make it easy to import user - * entered URI data.
- * - *This function is intended to be a helper function. - * It is not all-knowning and will probably make mistakes - * when attempting to parse a string of unknown origin. If - * your applicaiton is receiving input from the user, your - * application should already have a good idea what the user - * should be entering, and your application should be - * pre-processing the user's input to make sure it is well formed - * before passing it to this function.
- * - *It is assumed that the string given to this function is - * something the user may have manually entered. Given this, - * the URI string is probably unescaped or improperly escaped. - * This function will attempt to properly escape the URI by - * using forceEscape(). The result is that a toString() call - * on a URI that was created from unknownToURI() may not match - * the input string due to the difference in escaping.
- * - * @param unknown a potental URI string that should be parsed - * and loaded into this object. - * @param defaultScheme if it is determined that the passed string - * looks like a URI, but it is missing the scheme part, this - * string will be used as the missing scheme. - * - * @return true if the given string was successfully parsed into - * a valid URI object, false otherwise. - */ - public function unknownToURI(unknown:String, defaultScheme:String = "http") : Boolean - { - var temp:String; - - if (unknown.length == 0) - { - this.initialize(); - return false; - } - - // Some users love the backslash key. Fix it. - unknown = unknown.replace(/\\/g, "/"); - - // Check for any obviously missing scheme. - if (unknown.length >= 2) - { - temp = unknown.substr(0, 2); - if (temp == "//") - unknown = defaultScheme + ":" + unknown; - } - - if (unknown.length >= 3) - { - temp = unknown.substr(0, 3); - if (temp == "://") - unknown = defaultScheme + unknown; - } - - // Try parsing it as a normal URI - var uri:URI = new URI(unknown); - - if (uri.isHierarchical() == false) - { - if (uri.scheme == UNKNOWN_SCHEME) - { - this.initialize(); - return false; - } - - // It's a non-hierarchical URI - copyURI(uri); - forceEscape(); - return true; - } - else if ((uri.scheme != UNKNOWN_SCHEME) && - (uri.scheme.length > 0)) - { - if ( (uri.authority.length > 0) || - (uri.scheme == "file") ) - { - // file://... URI - copyURI(uri); - forceEscape(); // ensure proper escaping - return true; - } - else if (uri.authority.length == 0 && uri.path.length == 0) - { - // It's is an incomplete URI (eg "http://") - - setParts(uri.scheme, "", "", "", "", ""); - return false; - } - } - else - { - // Possible relative URI. We can only detect relative URI's - // that start with "." or "..". If it starts with something - // else, the parsing is ambiguous. - var path:String = uri.path; - - if (path == ".." || path == "." || - (path.length >= 3 && path.substr(0, 3) == "../") || - (path.length >= 2 && path.substr(0, 2) == "./") ) - { - // This is a relative URI. - copyURI(uri); - forceEscape(); - return true; - } - } - - // Ok, it looks like we are just a normal URI missing the scheme. Tack - // on the scheme. - uri = new URI(defaultScheme + "://" + unknown); - - // Check to see if we are good now - if (uri.scheme.length > 0 && uri.authority.length > 0) - { - // It was just missing the scheme. - copyURI(uri); - forceEscape(); // Make sure we are properly encoded. - return true; - } - - // don't know what this is - this.initialize(); - return false; - } - - } // end URI class -} // end package \ No newline at end of file -- cgit v1.2.3