package com.adobe.net
{
import flash.utils.ByteArray;
/**
* This class implements functions and utilities for working with URI's
* (Universal Resource Identifiers). For technical description of the
* URI syntax, please see RFC 3986 at http://www.ietf.org/rfc/rfc3986.txt
* or do a web search for "rfc 3986".
*
* <p>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.</p>
*
* <p>URI's fall into one of three categories:
* <ul>
* <li><scheme>:<scheme-specific-part>#<fragment> (non-hierarchical)</li>
* <li><scheme>:<authority><path>?<query>#<fragment> (hierarchical)</li>
* <li><path>?<query>#<fragment> (relative hierarchical)</li>
* </ul></p>
*
* <p>The query and fragment parts are optional.</p>
*
* <p>This class supports both non-hierarchical and hierarchical URI's</p>
*
* <p>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.</p>
*
* @langversion ActionScript 3.0
* @playerversion Flash 9.0
*/
public class URI
{
public static const URImustEscape:String = " %";
public static const URIbaselineEscape:String = URImustEscape + ":?#/@";
public static const URIpathEscape:String = URImustEscape + "?#";
public static const URIqueryEscape:String = URImustEscape + "#";
public static const URIqueryPartEscape:String = URImustEscape + "#&=";
public static const URInonHierEscape:String = URImustEscape + "?#/";
public static const UNKNOWN_SCHEME:String = "unknown";
protected static const URIbaselineExcludedBitmap:URIEncodingBitmap =
new URIEncodingBitmap(URIbaselineEscape);
protected static const URIschemeExcludedBitmap:URIEncodingBitmap =
URIbaselineExcludedBitmap;
protected static const URIuserpassExcludedBitmap:URIEncodingBitmap =
URIbaselineExcludedBitmap;
protected static const URIauthorityExcludedBitmap:URIEncodingBitmap =
URIbaselineExcludedBitmap;
protected static const URIportExludedBitmap:URIEncodingBitmap =
URIbaselineExcludedBitmap;
protected static const URIpathExcludedBitmap:URIEncodingBitmap =
new URIEncodingBitmap(URIpathEscape);
protected static const URIqueryExcludedBitmap:URIEncodingBitmap =
new URIEncodingBitmap(URIqueryEscape);
protected static const URIqueryPartExcludedBitmap:URIEncodingBitmap =
new URIEncodingBitmap(URIqueryPartEscape);
protected static const URIfragmentExcludedBitmap:URIEncodingBitmap =
URIqueryExcludedBitmap;
protected static const URInonHierexcludedBitmap:URIEncodingBitmap =
new URIEncodingBitmap(URInonHierEscape);
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 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)
{
_nonHierarchical = "";
if (_scheme == "" || _scheme == UNKNOWN_SCHEME)
_relative = true;
else
_relative = false;
if (_authority.length == 0 && _path.length == 0)
_valid = false;
else
_valid = true;
}
else
{
_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
{
if (isAbsolute())
{
if (_scheme.length <= 1 || _scheme == UNKNOWN_SCHEME)
{
return false;
}
else if (verifyAlpha(_scheme) == false)
return false; }
if (hierState)
{
if (_path.search('\\') != -1)
return false; else if (isRelative() == false && _scheme == UNKNOWN_SCHEME)
return false; }
else
{
if (_nonHierarchical.search('\\') != -1)
return false; }
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:
* <scheme>:<authority><path>?<query>#<fragment>
*
* For simplicity, we parse the URI in the following order:
*
* 1. Fragment (anchors)
* 2. Query (CGI stuff)
* 3. Scheme ("http")
* 4. Authority (host name)
* 5. Username/Password (if any)
* 6. Port (server port if any)
* 7. Path (/homepages/mypage.html)
*
* The reason for this order is to minimize any parsing ambiguities.
* Fragments and queries can contain almost anything (they are parts
* that can contain custom data with their own syntax). Parsing
* them out first removes a large chance of parsing errors. This
* method expects well formed URI's, but performing the parse in
* this order makes us a little more tolerant of user error.
*
* REGEXP
* Why doesn't this use regular expressions to parse the URI? We
* have found that in a real world scenario, URI's are not always
* well formed. Sometimes characters that should have been escaped
* are not, and those situations would break a regexp pattern. This
* function attempts to be smart about what it is parsing based on
* location of characters relative to eachother. This function has
* been proven through real-world use to parse the vast majority
* of URI's correctly.
*
* NOTE
* It is assumed that the string in URI form is escaped. This function
* does not escape anything. If you constructed the URI string by
* hand, and used this to parse in the URI and still need it escaped,
* call forceEscape() on your URI object.
*
* Parsing Assumptions
* This routine assumes that the URI being passed is well formed.
* Passing things like local paths, malformed URI's, and the such
* will result in parsing errors. This function can handle
* - absolute hierarchical (e.g. "http://something.com/index.html),
* - relative hierarchical (e.g. "../images/flower.gif"), or
* - non-hierarchical URIs (e.g. "mailto:jsmith@fungoo.com").
*
* Anything else will probably result in a parsing error, or a bogus
* URI object.
*
* Note that non-hierarchical URIs *MUST* have a scheme, otherwise
* they will be mistaken for relative URI's.
*
* If you are not sure what is being passed to you (like manually
* entered text from UI), you can construct a blank URI object and
* call unknownToURI() passing in the unknown string.
*
* @return true if successful, false if there was some kind of
* parsing error
*/
protected function parseURI(uri:String) : Boolean
{
var baseURI:String = uri;
var index:int, index2:int;
initialize();
index = baseURI.indexOf("#");
if (index != -1)
{
if (baseURI.length > (index + 1)) _fragment = baseURI.substr(index + 1, baseURI.length - (index + 1));
baseURI = baseURI.substr(0, index);
}
index = baseURI.indexOf("?");
if (index != -1)
{
if (baseURI.length > (index + 1))
_query = baseURI.substr(index + 1, baseURI.length - (index + 1));
baseURI = baseURI.substr(0, index);
}
index = baseURI.search(':');
index2 = baseURI.search('/');
var containsColon:Boolean = (index != -1);
var containsSlash:Boolean = (index2 != -1);
var colonBeforeSlash:Boolean = (!containsSlash || index < index2);
if (containsColon && colonBeforeSlash)
{
_scheme = baseURI.substr(0, index);
_scheme = _scheme.toLowerCase();
baseURI = baseURI.substr(index + 1);
if (baseURI.substr(0, 2) == "//")
{
_nonHierarchical = "";
baseURI = baseURI.substr(2, baseURI.length - 2);
}
else
{
_nonHierarchical = baseURI;
if ((_valid = validateURI()) == false)
initialize();
return isValid();
}
}
else
{
_scheme = "";
_relative = true;
_nonHierarchical = "";
}
if (isRelative())
{
_authority = "";
_port = "";
_path = baseURI;
}
else
{
if (baseURI.substr(0, 2) == "//")
{
while(baseURI.charAt(0) == "/")
baseURI = baseURI.substr(1, baseURI.length - 1);
}
index = baseURI.search('/');
if (index == -1)
{
_authority = baseURI;
_path = "";
}
else
{
_authority = baseURI.substr(0, index);
_path = baseURI.substr(index, baseURI.length - index);
}
index = _authority.search('@');
if (index != -1)
{
_username = _authority.substr(0, index);
_authority = _authority.substr(index + 1);
index = _username.search(':');
if (index != -1)
{
_password = _username.substring(index + 1, _username.length);
_username = _username.substr(0, index);
}
else
_password = "";
}
else
{
_username = "";
_password = "";
}
index = _authority.search(':');
if (index != -1)
{
_port = _authority.substring(index + 1, _authority.length); _authority = _authority.substr(0, index);
}
else
{
_port = "";
}
_authority = _authority.toLowerCase();
}
if ((_valid = validateURI()) == false)
initialize();
return isValid();
}
/********************************************************************
* Copy function.
*/
public function copyURI(uri:URI) : void
{
this._scheme = uri._scheme;
this._authority = uri._authority;
this._username = uri._username;
this._password = uri._password;
this._port = uri._port;
this._path = uri._path;
this._query = uri._query;
this._fragment = uri._fragment;
this._nonHierarchical = uri._nonHierarchical;
this._valid = uri._valid;
this._relative = uri._relative;
}
/**
* @private
* Checks if the given string only contains a-z or A-Z.
*/
protected function verifyAlpha(str:String) : Boolean
{
var pattern:RegExp = /[^a-z]/;
var index:int;
str = str.toLowerCase();
index = str.search(pattern);
if (index == -1)
return true;
else
return false;
}
/**
* Is this a valid URI?
*
* @return true if this object represents a valid URI, false
* otherwise.
*/
public function isValid() : Boolean
{
return this._valid;
}
/**
* Is this URI an absolute URI? An absolute URI is a complete, fully
* qualified reference to a resource. e.g. http://site.com/index.htm
* Non-hierarchical URI's are always absolute.
*/
public function isAbsolute() : Boolean
{
return !this._relative;
}
/**
* Is this URI a relative URI? Relative URI's do not have a scheme
* and only contain a relative path with optional anchor and query
* parts. e.g. "../reports/index.htm". Non-hierarchical URI's
* will never be relative.
*/
public function isRelative() : Boolean
{
return this._relative;
}
/**
* Does this URI point to a resource that is a directory/folder?
* The URI specification dictates that any path that ends in a slash
* is a directory. This is needed to be able to perform correct path
* logic when combining relative URI's with absolute URI's to
* obtain the correct absolute URI to a resource.
*
* @see URI.chdir
*
* @return true if this URI represents a directory resource, false
* if this URI represents a file resource.
*/
public function isDirectory() : Boolean
{
if (_path.length == 0)
return false;
return (_path.charAt(path.length - 1) == '/');
}
/**
* Is this URI a hierarchical URI? URI's can be
*/
public function isHierarchical() : Boolean
{
return hierState;
}
/**
* The scheme of the URI.
*/
public function get scheme() : String
{
return URI.unescapeChars(_scheme);
}
public function set scheme(schemeStr:String) : void
{
var normalized:String = schemeStr.toLowerCase();
_scheme = URI.fastEscapeChars(normalized, URI.URIschemeExcludedBitmap);
}
/**
* The authority (host) of the URI. Only valid for
* hierarchical URI's. If the URI is relative, this will
* be an empty string. When setting this value, the string
* given is assumed to be unescaped. When retrieving this
* value, the resulting string is unescaped.
*/
public function get authority() : String
{
return URI.unescapeChars(_authority);
}
public function set authority(authorityStr:String) : void
{
authorityStr = authorityStr.toLowerCase();
_authority = URI.fastEscapeChars(authorityStr,
URI.URIauthorityExcludedBitmap);
this.hierState = true;
}
/**
* The username of the URI. Only valid for hierarchical
* URI's. If the URI is relative, this will be an empty
* string.
*
* <p>The URI specification allows for authentication
* credentials to be embedded in the URI as such:</p>
*
* <p>http://user:passwd@host/path/to/file.htm</p>
*
* <p>When setting this value, the string
* given is assumed to be unescaped. When retrieving this
* value, the resulting string is unescaped.</p>
*/
public function get username() : String
{
return URI.unescapeChars(_username);
}
public function set username(usernameStr:String) : void
{
_username = URI.fastEscapeChars(usernameStr, URI.URIuserpassExcludedBitmap);
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);
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:
*
* <p>http://site.com:8080/index.htm</p>
*/
public function get port() : String
{
return URI.unescapeChars(_port);
}
public function set port(portStr:String) : void
{
_port = URI.escapeChars(portStr);
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.
*
* <p>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.</p>
*
* <p>An absolute path may look like:</p>
* <listing>/full/path/to/my/file.htm</listing>
*
* <p>A relative path may look like:</p>
* <listing>
* path/to/my/file.htm
* ../images/logo.gif
* ../../reports/index.htm
* </listing>
*
* <p>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:</p>
*
* <listing>http:/site.com/path/to/file.htm</listing>
*
* <p>This absolute URI has an absolute path of "/path/to/file.htm".</p>
*
* <p>Relative URI's can have either absolute paths or relative paths.
* All of the following relative URI's are valid:</p>
*
* <listing>
* /absolute/path/to/file.htm
* path/to/file.htm
* ../path/to/file.htm
* </listing>
*/
public function get path() : String
{
return URI.unescapeChars(_path);
}
public function set path(pathStr:String) : void
{
this._path = URI.fastEscapeChars(pathStr, URI.URIpathExcludedBitmap);
if (this._scheme == UNKNOWN_SCHEME)
{
this._scheme = "";
}
hierState = true;
}
/**
* The query (CGI) portion of the URI. This part is valid for
* both hierarchical and non-hierarchical URI's.
*
* <p>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.
*
* <p>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:</p>
*
* <listing>http://site.com/script.php?param1=value1¶m2=value2</listing>
*
* <p>This class has specific support for this query syntax</p>
*
* <p>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.</p>
*
* 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);
}
/**
* 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);
}
/**
* 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.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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* @param unescaped character string to be escaped.
*
* @return escaped character string
*
* @see encodeURIComponent
* @see fastEscapeChars
*/
static public function escapeChars(unescaped:String) : String
{
return fastEscapeChars(unescaped, URI.URIbaselineExcludedBitmap);
}
/**
* Unescape any URI escaped characters in the given character
* string.
*
* <p>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.</p>
*
* @param escaped the escaped string to be unescaped.
*
* @return unescaped string.
*/
static public function unescapeChars(escaped:String ) : String
{
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.
*
* <p>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.</p>
*
* @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
{
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;
}
}
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.
*
* <p>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.</p>
*
* @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();
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:
*
* <listing>
* var query:Object = uri.getQueryByMap();
* var value:String = query["param"]; // get a value
* query["param2"] = "foo"; // set a new value
* </listing>
*
* @return Object that contains the name/value pairs of the query.
*
* @see #setQueryByMap
* @see #getQueryValue
* @see #setQueryValue
*/
public function getQueryByMap() : Object
{
var queryStr:String;
var pair:String;
var pairs:Array;
var item:Array;
var name:String, value:String;
var index:int;
var map:Object = new Object();
queryStr = this._query;
pairs = queryStr.split('&');
for each (pair in pairs)
{
if (pair.length == 0)
continue;
item = pair.split('=');
if (item.length > 0)
name = item[0];
else
continue;
if (item.length > 1)
value = item[1];
else
value = "";
name = queryPartUnescape(name);
value = queryPartUnescape(value);
map[name] = value;
}
return map;
}
/**
* Set the query part of this URI using the given object as the
* content source. Any member of the object that has a value of
* null will not be in the resulting query.
*
* @param map object that contains the name/value pairs as
* members of that object.
*
* @see #getQueryByMap
* @see #getQueryValue
* @see #setQueryValue
*/
public function setQueryByMap(map:Object) : void
{
var item:String;
var name:String, value:String;
var queryStr:String = "";
var tmpPair:String;
var foo:String;
for (item in map)
{
name = item;
value = map[item];
if (value == null)
value = "";
name = queryPartEscape(name);
value = queryPartEscape(value);
tmpPair = name;
if (value.length > 0)
{
tmpPair += "=";
tmpPair += value;
}
if (queryStr.length != 0)
queryStr += '&';
queryStr += tmpPair;
}
_query = queryStr;
}
/**
* Similar to Escape(), except this also escapes characters that
* would conflict with the name/value pair query syntax. This is
* intended to be called on each individual "name" and "value"
* in the query making sure that nothing in the name or value
* strings contain characters that would conflict with the query
* syntax (e.g. '=' and '&').
*
* @param unescaped unescaped string that is to be escaped.
*
* @return escaped string.
*
* @see #queryUnescape
*/
static public function queryPartEscape(unescaped:String) : String
{
var escaped:String = unescaped;
escaped = URI.fastEscapeChars(unescaped, URI.URIqueryPartExcludedBitmap);
return escaped;
}
/**
* Unescape the individual name/value string pairs.
*
* @param escaped escaped string to be unescaped
*
* @return unescaped string
*
* @see #queryEscape
*/
static public function queryPartUnescape(escaped:String) : String
{
var unescaped:String = escaped;
unescaped = unescapeChars(unescaped);
return unescaped;
}
/**
* Output this URI as a string. The resulting string is properly
* escaped and well formed for machine processing.
*/
public function toString() : String
{
if (this == null)
return "";
else
return toStringInternal(false);
}
/**
* Output the URI as a string that is easily readable by a human.
* This outputs the URI with all escape sequences unescaped to
* their character representation. This makes the URI easier for
* a human to read, but the URI could be completely invalid
* because some unescaped characters may now cause ambiguous parsing.
* This function should only be used if you want to display a URI to
* a user. This function should never be used outside that specific
* case.
*
* @return the URI in string format with all escape sequences
* unescaped.
*
* @see #toString
*/
public function toDisplayString() : String
{
return toStringInternal(true);
}
/**
* @private
*
* The guts of toString()
*/
protected function toStringInternal(forDisplay:Boolean) : String
{
var uri:String = "";
var part:String = "";
if (isHierarchical() == false)
{
uri += (forDisplay ? this.scheme : _scheme);
uri += ":";
uri += (forDisplay ? this.nonHierarchical : _nonHierarchical);
}
else
{
if (isRelative() == false)
{
if (_scheme.length != 0)
{
part = (forDisplay ? this.scheme : _scheme);
uri += part + ":";
}
if (_authority.length != 0 || isOfType("file"))
{
uri += "//";
if (_username.length != 0)
{
part = (forDisplay ? this.username : _username);
uri += part;
if (_password.length != 0)
{
part = (forDisplay ? this.password : _password);
uri += ":" + part;
}
uri += "@";
}
part = (forDisplay ? this.authority : _authority);
uri += part;
if (port.length != 0)
uri += ":" + port;
}
}
part = (forDisplay ? this.path : _path);
uri += part;
}
if (_query.length != 0)
{
part = (forDisplay ? this.query : _query);
uri += "?" + part;
}
if (fragment.length != 0)
{
part = (forDisplay ? this.fragment : _fragment);
uri += "#" + part;
}
return uri;
}
/**
* Forcefully ensure that this URI is properly escaped.
*
* <p>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.</p>
*/
public function forceEscape() : void
{
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)
{
extension = extension.substr(index + 1);
}
else
{
}
thisExtension = getExtension(true);
if (thisExtension == "")
return false;
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:
*
* <listing>http://something.com/path/to/my/page.html?form=yes&name=bob#anchor</listing>
*
* <p>This will return ".html".</p>
*
* @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 (index == -1 || index == 0)
return String("");
extension = filename.substr(index);
if (minusDot && extension.charAt(0) == ".")
extension = extension.substr(1);
return extension;
}
/**
* Quick function to retrieve the file name off the end of a URI.
*
* <p>For example, if the URI is:</p>
* <listing>http://something.com/some/path/to/my/file.html</listing>
* <p>this function will return "file.html".</p>
*
* @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;
index = pathStr.lastIndexOf("/");
if (index != -1)
filename = pathStr.substr(index + 1);
else
filename = pathStr;
if (minusExtension)
{
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"); else
{
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)
{
return _resolver.resolve(copy);
}
else
{
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
{
var thisURI:URI = URI.resolve(this);
var thatURI:URI = URI.resolve(uri);
if (thisURI.isRelative() || thatURI.isRelative())
{
return URI.NOT_RELATED;
}
else if (thisURI.isHierarchical() == false || thatURI.isHierarchical() == false)
{
if (((thisURI.isHierarchical() == false) && (thatURI.isHierarchical() == true)) ||
((thisURI.isHierarchical() == true) && (thatURI.isHierarchical() == false)))
{
return URI.NOT_RELATED;
}
else
{
if (thisURI.scheme != thatURI.scheme)
return URI.NOT_RELATED;
if (thisURI.nonHierarchical != thatURI.nonHierarchical)
return URI.NOT_RELATED;
return URI.EQUAL;
}
}
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;
if (thisPort == "")
thisPort = thisURI.getDefaultPort();
if (thatPort == "")
thatPort = thatURI.getDefaultPort();
if (thisPort != thatPort)
return URI.NOT_RELATED;
if (compareStr(thisURI.path, thatURI.path, caseSensitive))
return URI.EQUAL;
var thisPath:String = thisURI.path;
var thatPath:String = thatURI.path;
if ( (thisPath == "/" || thatPath == "/") &&
(thisPath == "" || thatPath == "") )
{
return URI.EQUAL;
}
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)
{
return URI.NOT_RELATED;
}
else
{
thatParts.pop();
}
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)
{
return URI.NOT_RELATED;
}
else
{
thisParts.pop();
}
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
{
return URI.NOT_RELATED;
}
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)
{
return null;
}
var relation:int = thisURI.getRelation(thatURI);
if (relation == URI.NOT_RELATED)
{
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;
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.
*
* <p>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:<br/>
* 1. http://something.com/a/b/c/ (directory)<br/>
* 2. http://something.com/a/b/c (not directory)<br/>
* you will get:<br/>
* 1. http://something.com/a/b/c/d<br/>
* 2. http://something.com/a/b/d<br/></p>
*
* <p>See RFC 2396, Appendix C for more info.</p>
*
* @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 == "")
{
return true;
}
else if (ref.substr(0, 2) == "//")
{
var final:String = this.scheme + ":" + ref;
return constructURI(final);
}
else if (ref.charAt(0) == "?")
{
ref = "./" + ref;
}
uriReference = new URI(ref);
if (uriReference.isAbsolute() ||
uriReference.isHierarchical() == false)
{
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(); }
if (thisParts.length > 0 && thisParts[thisParts.length - 1] == "")
{
thisIsDir = true;
thisParts.pop(); }
if (thatParts.length > 0 && thatParts[0] == "")
{
thatIsAbs = true;
thatParts.shift(); }
if (thatParts.length > 0 && thatParts[thatParts.length - 1] == "")
{
thatIsDir = true;
thatParts.pop(); }
if (thatIsAbs)
{
this.path = uriReference.path;
this.queryRaw = uriReference.queryRaw;
this.fragment = uriReference.fragment;
return true;
}
else if (thatParts.length == 0 && uriReference.query == "")
{
this.fragment = uriReference.fragment;
return true;
}
else if (thisIsDir == false && thisParts.length > 0)
{
thisParts.pop();
}
this.queryRaw = uriReference.queryRaw;
this.fragment = uriReference.fragment;
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; lastIsDotOperation = true;
}
else if (curDir == "..")
{
if (i >= 1)
{
if (thisParts[i - 1] == "..")
{
}
else
{
thisParts.splice(i - 1, 2);
i = i - 2; }
}
else
{
if (isRelative())
{
}
else
{
thisParts.splice(i, 1);
i = i - 1; }
}
lastIsDotOperation = true;
}
}
var finalPath:String = "";
thatIsDir = thatIsDir || lastIsDotOperation;
finalPath = joinPath(thisParts, thisIsAbs, thatIsDir);
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 (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())
{
return false;
}
var base:URI = new URI();
base.copyURI(base_uri);
if (base.chdir(toString()) == false)
return false;
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:
*
* <listing>
* var uri1:URI = new URI("http://something.com/path/to/some/file.html");
* var uri2:URI = new URI("http://something.com/path/to/another/file.html");
*
* uri1.MakeRelativePath(uri2);</listing>
*
* <p>uri1 will have a final value of "../some/file.html"</p>
*
* <p>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:</p>
*
* <p>../../../../../../some/path/to/my/file.html</p>
*
* @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())
{
return true;
}
if (base.isRelative())
{
return false;
}
if ( (isOfType(base_uri.scheme) == false) ||
(this.authority != base_uri.authority) )
{
return false;
}
isDir = isDirectory();
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();
while(thatParts.length > 0)
{
if (thisParts.length == 0)
{
break;
}
thisPart = thisParts[0];
thatPart = thatParts[0];
if (compareStr(thisPart, thatPart, caseSensitive))
{
thisParts.shift();
thatParts.shift();
}
else
break;
}
var dotdot:String = "..";
for (i = 0; i < thatParts.length; i++)
{
finalParts.push(dotdot);
}
finalParts = finalParts.concat(thisParts);
finalPath = joinPath(finalParts, false , isDir);
if (finalPath.length == 0)
{
finalPath = "./";
}
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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* @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;
}
unknown = unknown.replace(/\\/g, "/");
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;
}
var uri:URI = new URI(unknown);
if (uri.isHierarchical() == false)
{
if (uri.scheme == UNKNOWN_SCHEME)
{
this.initialize();
return false;
}
copyURI(uri);
forceEscape();
return true;
}
else if ((uri.scheme != UNKNOWN_SCHEME) &&
(uri.scheme.length > 0))
{
if ( (uri.authority.length > 0) ||
(uri.scheme == "file") )
{
copyURI(uri);
forceEscape(); return true;
}
else if (uri.authority.length == 0 && uri.path.length == 0)
{
setParts(uri.scheme, "", "", "", "", "");
return false;
}
}
else
{
var path:String = uri.path;
if (path == ".." || path == "." ||
(path.length >= 3 && path.substr(0, 3) == "../") ||
(path.length >= 2 && path.substr(0, 2) == "./") )
{
copyURI(uri);
forceEscape();
return true;
}
}
uri = new URI(defaultScheme + "://" + unknown);
if (uri.scheme.length > 0 && uri.authority.length > 0)
{
copyURI(uri);
forceEscape(); return true;
}
this.initialize();
return false;
}
} }