addressable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +5 -0
- data/LICENSE +20 -0
- data/README +3 -0
- data/coverage/index.html +261 -0
- data/coverage/lib-addressable-uri_rb.html +1723 -0
- data/doc/classes/Addressable.html +115 -0
- data/doc/classes/Addressable/URI.html +2294 -0
- data/doc/classes/Addressable/URI/CharacterClasses.html +173 -0
- data/doc/classes/Addressable/URI/IDNA.html +193 -0
- data/doc/classes/Addressable/URI/InvalidOptionError.html +117 -0
- data/doc/classes/Addressable/URI/InvalidTemplateValue.html +117 -0
- data/doc/classes/Addressable/URI/InvalidURIError.html +117 -0
- data/doc/created.rid +1 -0
- data/doc/files/CHANGELOG.html +111 -0
- data/doc/files/LICENSE.html +130 -0
- data/doc/files/README.html +110 -0
- data/doc/files/lib/addressable/uri_rb.html +109 -0
- data/doc/files/lib/addressable/version_rb.html +101 -0
- data/doc/fr_class_index.html +33 -0
- data/doc/fr_file_index.html +31 -0
- data/doc/fr_method_index.html +81 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/addressable/uri.rb +1092 -0
- data/lib/addressable/version.rb +32 -0
- data/rakefile +251 -0
- data/spec/addressable/uri_spec.rb +2278 -0
- data/spec/data/rfc3986.txt +3419 -0
- data/specdoc/index.html +1094 -0
- metadata +101 -0
data/doc/index.html
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
|
5
|
+
|
6
|
+
<!--
|
7
|
+
|
8
|
+
Addressable -- URI Implementation
|
9
|
+
|
10
|
+
-->
|
11
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
12
|
+
<head>
|
13
|
+
<title>Addressable -- URI Implementation</title>
|
14
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
15
|
+
</head>
|
16
|
+
<frameset rows="20%, 80%">
|
17
|
+
<frameset cols="25%,35%,45%">
|
18
|
+
<frame src="fr_file_index.html" title="Files" name="Files" />
|
19
|
+
<frame src="fr_class_index.html" name="Classes" />
|
20
|
+
<frame src="fr_method_index.html" name="Methods" />
|
21
|
+
</frameset>
|
22
|
+
<frame src="files/README.html" name="docwin" />
|
23
|
+
</frameset>
|
24
|
+
</html>
|
data/doc/rdoc-style.css
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
|
2
|
+
body {
|
3
|
+
font-family: Verdana,Arial,Helvetica,sans-serif;
|
4
|
+
font-size: 90%;
|
5
|
+
margin: 0;
|
6
|
+
margin-left: 40px;
|
7
|
+
padding: 0;
|
8
|
+
background: white;
|
9
|
+
}
|
10
|
+
|
11
|
+
h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
|
12
|
+
h1 { font-size: 150%; }
|
13
|
+
h2,h3,h4 { margin-top: 1em; }
|
14
|
+
|
15
|
+
a { background: #eef; color: #039; text-decoration: none; }
|
16
|
+
a:hover { background: #039; color: #eef; }
|
17
|
+
|
18
|
+
/* Override the base stylesheet's Anchor inside a table cell */
|
19
|
+
td > a {
|
20
|
+
background: transparent;
|
21
|
+
color: #039;
|
22
|
+
text-decoration: none;
|
23
|
+
}
|
24
|
+
|
25
|
+
/* and inside a section title */
|
26
|
+
.section-title > a {
|
27
|
+
background: transparent;
|
28
|
+
color: #eee;
|
29
|
+
text-decoration: none;
|
30
|
+
}
|
31
|
+
|
32
|
+
/* === Structural elements =================================== */
|
33
|
+
|
34
|
+
div#index {
|
35
|
+
margin: 0;
|
36
|
+
margin-left: -40px;
|
37
|
+
padding: 0;
|
38
|
+
font-size: 90%;
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
div#index a {
|
43
|
+
margin-left: 0.7em;
|
44
|
+
}
|
45
|
+
|
46
|
+
div#index .section-bar {
|
47
|
+
margin-left: 0px;
|
48
|
+
padding-left: 0.7em;
|
49
|
+
background: #ccc;
|
50
|
+
font-size: small;
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
div#classHeader, div#fileHeader {
|
55
|
+
width: auto;
|
56
|
+
color: white;
|
57
|
+
padding: 0.5em 1.5em 0.5em 1.5em;
|
58
|
+
margin: 0;
|
59
|
+
margin-left: -40px;
|
60
|
+
border-bottom: 3px solid #006;
|
61
|
+
}
|
62
|
+
|
63
|
+
div#classHeader a, div#fileHeader a {
|
64
|
+
background: inherit;
|
65
|
+
color: white;
|
66
|
+
}
|
67
|
+
|
68
|
+
div#classHeader td, div#fileHeader td {
|
69
|
+
background: inherit;
|
70
|
+
color: white;
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
div#fileHeader {
|
75
|
+
background: #057;
|
76
|
+
}
|
77
|
+
|
78
|
+
div#classHeader {
|
79
|
+
background: #048;
|
80
|
+
}
|
81
|
+
|
82
|
+
|
83
|
+
.class-name-in-header {
|
84
|
+
font-size: 180%;
|
85
|
+
font-weight: bold;
|
86
|
+
}
|
87
|
+
|
88
|
+
|
89
|
+
div#bodyContent {
|
90
|
+
padding: 0 1.5em 0 1.5em;
|
91
|
+
}
|
92
|
+
|
93
|
+
div#description {
|
94
|
+
padding: 0.5em 1.5em;
|
95
|
+
background: #efefef;
|
96
|
+
border: 1px dotted #999;
|
97
|
+
}
|
98
|
+
|
99
|
+
div#description h1,h2,h3,h4,h5,h6 {
|
100
|
+
color: #125;;
|
101
|
+
background: transparent;
|
102
|
+
}
|
103
|
+
|
104
|
+
div#validator-badges {
|
105
|
+
text-align: center;
|
106
|
+
}
|
107
|
+
div#validator-badges img { border: 0; }
|
108
|
+
|
109
|
+
div#copyright {
|
110
|
+
color: #333;
|
111
|
+
background: #efefef;
|
112
|
+
font: 0.75em sans-serif;
|
113
|
+
margin-top: 5em;
|
114
|
+
margin-bottom: 0;
|
115
|
+
padding: 0.5em 2em;
|
116
|
+
}
|
117
|
+
|
118
|
+
|
119
|
+
/* === Classes =================================== */
|
120
|
+
|
121
|
+
table.header-table {
|
122
|
+
color: white;
|
123
|
+
font-size: small;
|
124
|
+
}
|
125
|
+
|
126
|
+
.type-note {
|
127
|
+
font-size: small;
|
128
|
+
color: #DEDEDE;
|
129
|
+
}
|
130
|
+
|
131
|
+
.xxsection-bar {
|
132
|
+
background: #eee;
|
133
|
+
color: #333;
|
134
|
+
padding: 3px;
|
135
|
+
}
|
136
|
+
|
137
|
+
.section-bar {
|
138
|
+
color: #333;
|
139
|
+
border-bottom: 1px solid #999;
|
140
|
+
margin-left: -20px;
|
141
|
+
}
|
142
|
+
|
143
|
+
|
144
|
+
.section-title {
|
145
|
+
background: #79a;
|
146
|
+
color: #eee;
|
147
|
+
padding: 3px;
|
148
|
+
margin-top: 2em;
|
149
|
+
margin-left: -30px;
|
150
|
+
border: 1px solid #999;
|
151
|
+
}
|
152
|
+
|
153
|
+
.top-aligned-row { vertical-align: top }
|
154
|
+
.bottom-aligned-row { vertical-align: bottom }
|
155
|
+
|
156
|
+
/* --- Context section classes ----------------------- */
|
157
|
+
|
158
|
+
.context-row { }
|
159
|
+
.context-item-name { font-family: monospace; font-weight: bold; color: black; }
|
160
|
+
.context-item-value { font-size: small; color: #448; }
|
161
|
+
.context-item-desc { color: #333; padding-left: 2em; }
|
162
|
+
|
163
|
+
/* --- Method classes -------------------------- */
|
164
|
+
.method-detail {
|
165
|
+
background: #efefef;
|
166
|
+
padding: 0;
|
167
|
+
margin-top: 0.5em;
|
168
|
+
margin-bottom: 1em;
|
169
|
+
border: 1px dotted #ccc;
|
170
|
+
}
|
171
|
+
.method-heading {
|
172
|
+
color: black;
|
173
|
+
background: #ccc;
|
174
|
+
border-bottom: 1px solid #666;
|
175
|
+
padding: 0.2em 0.5em 0 0.5em;
|
176
|
+
}
|
177
|
+
.method-signature { color: black; background: inherit; }
|
178
|
+
.method-name { font-weight: bold; }
|
179
|
+
.method-args { font-style: italic; }
|
180
|
+
.method-description { padding: 0 0.5em 0 0.5em; }
|
181
|
+
|
182
|
+
/* --- Source code sections -------------------- */
|
183
|
+
|
184
|
+
a.source-toggle { font-size: 90%; }
|
185
|
+
div.method-source-code {
|
186
|
+
background: #262626;
|
187
|
+
color: #ffdead;
|
188
|
+
margin: 1em;
|
189
|
+
padding: 0.5em;
|
190
|
+
border: 1px dashed #999;
|
191
|
+
overflow: hidden;
|
192
|
+
}
|
193
|
+
|
194
|
+
div.method-source-code pre { color: #ffdead; overflow: hidden; }
|
195
|
+
|
196
|
+
/* --- Ruby keyword styles --------------------- */
|
197
|
+
|
198
|
+
.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
|
199
|
+
|
200
|
+
.ruby-constant { color: #7fffd4; background: transparent; }
|
201
|
+
.ruby-keyword { color: #00ffff; background: transparent; }
|
202
|
+
.ruby-ivar { color: #eedd82; background: transparent; }
|
203
|
+
.ruby-operator { color: #00ffee; background: transparent; }
|
204
|
+
.ruby-identifier { color: #ffdead; background: transparent; }
|
205
|
+
.ruby-node { color: #ffa07a; background: transparent; }
|
206
|
+
.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
|
207
|
+
.ruby-regexp { color: #ffa07a; background: transparent; }
|
208
|
+
.ruby-value { color: #7fffd4; background: transparent; }
|
@@ -0,0 +1,1092 @@
|
|
1
|
+
#--
|
2
|
+
# Addressable, Copyright (c) 2006-2007 Bob Aman
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module Addressable
|
25
|
+
# This is an implementation of a URI parser based on RFC 3986.
|
26
|
+
class URI
|
27
|
+
# Raised if something other than a uri is supplied.
|
28
|
+
class InvalidURIError < StandardError
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised if an invalid method option is supplied.
|
32
|
+
class InvalidOptionError < StandardError
|
33
|
+
end
|
34
|
+
|
35
|
+
# Raised if an invalid method option is supplied.
|
36
|
+
class InvalidTemplateValue < StandardError
|
37
|
+
end
|
38
|
+
|
39
|
+
module CharacterClasses
|
40
|
+
ALPHA = "a-zA-Z"
|
41
|
+
DIGIT = "0-9"
|
42
|
+
GEN_DELIMS = "\\:\\/\\?\\#\\[\\]\\@"
|
43
|
+
SUB_DELIMS = "\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\="
|
44
|
+
RESERVED = GEN_DELIMS + SUB_DELIMS
|
45
|
+
UNRESERVED = ALPHA + DIGIT + "\\-\\.\\_\\~"
|
46
|
+
PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@"
|
47
|
+
SCHEME = ALPHA + DIGIT + "\\-\\+\\."
|
48
|
+
AUTHORITY = PCHAR
|
49
|
+
PATH = PCHAR + "\\/"
|
50
|
+
QUERY = PCHAR + "\\/\\?"
|
51
|
+
FRAGMENT = PCHAR + "\\/\\?"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a URI object based on the parsed string.
|
55
|
+
def self.parse(uri_string)
|
56
|
+
return nil if uri_string.nil?
|
57
|
+
|
58
|
+
# If a URI object is passed, just return itself.
|
59
|
+
return uri_string if uri_string.kind_of?(self)
|
60
|
+
|
61
|
+
# If a URI object of the Ruby standard library variety is passed,
|
62
|
+
# convert it to a string, then parse the string.
|
63
|
+
if uri_string.class.name =~ /^URI::/
|
64
|
+
uri_string = uri_string.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
uri_regex =
|
68
|
+
/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
|
69
|
+
scan = uri_string.scan(uri_regex)
|
70
|
+
fragments = scan[0]
|
71
|
+
return nil if fragments.nil?
|
72
|
+
scheme = fragments[1]
|
73
|
+
authority = fragments[3]
|
74
|
+
path = fragments[4]
|
75
|
+
query = fragments[6]
|
76
|
+
fragment = fragments[8]
|
77
|
+
userinfo = nil
|
78
|
+
user = nil
|
79
|
+
password = nil
|
80
|
+
host = nil
|
81
|
+
port = nil
|
82
|
+
if authority != nil
|
83
|
+
userinfo = authority.scan(/^([^\[\]]*)@/).flatten[0]
|
84
|
+
if userinfo != nil
|
85
|
+
user = userinfo.strip.scan(/^([^:]*):?/).flatten[0]
|
86
|
+
password = userinfo.strip.scan(/:(.*)$/).flatten[0]
|
87
|
+
end
|
88
|
+
host = authority.gsub(/^([^\[\]]*)@/, "").gsub(/:([^:@\[\]]*?)$/, "")
|
89
|
+
port = authority.scan(/:([^:@\[\]]*?)$/).flatten[0]
|
90
|
+
end
|
91
|
+
if port.nil? || port == ""
|
92
|
+
port = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
# WARNING: Not standards-compliant, but follows the theme
|
96
|
+
# of Postel's law:
|
97
|
+
#
|
98
|
+
# Special exception for dealing with the retarded idea of the
|
99
|
+
# feed pseudo-protocol. Without this exception, the parser will read
|
100
|
+
# the URI as having a blank port number, instead of as having a second
|
101
|
+
# URI embedded within. This exception translates these broken URIs
|
102
|
+
# and instead treats the inner URI as opaque.
|
103
|
+
if scheme == "feed" && host == "http"
|
104
|
+
userinfo = nil
|
105
|
+
user = nil
|
106
|
+
password = nil
|
107
|
+
host = nil
|
108
|
+
port = nil
|
109
|
+
path = authority + path
|
110
|
+
end
|
111
|
+
|
112
|
+
return Addressable::URI.new(
|
113
|
+
scheme, user, password, host, port, path, query, fragment)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Converts a path to a file protocol URI. If the path supplied is
|
117
|
+
# relative, it will be returned as a relative URI. If the path supplied
|
118
|
+
# is actually a URI, it will return the parsed URI.
|
119
|
+
def self.convert_path(path)
|
120
|
+
return nil if path.nil?
|
121
|
+
|
122
|
+
converted_uri = path.strip
|
123
|
+
if converted_uri.length > 0 && converted_uri[0..0] == "/"
|
124
|
+
converted_uri = "file://" + converted_uri
|
125
|
+
end
|
126
|
+
if converted_uri.length > 0 &&
|
127
|
+
converted_uri.scan(/^[a-zA-Z]:[\\\/]/).size > 0
|
128
|
+
converted_uri = "file:///" + converted_uri
|
129
|
+
end
|
130
|
+
converted_uri.gsub!(/^file:\/*/i, "file:///")
|
131
|
+
if converted_uri =~ /^file:/i
|
132
|
+
# Adjust windows-style uris
|
133
|
+
converted_uri.gsub!(/^file:\/\/\/([a-zA-Z])\|/i, 'file:///\1:')
|
134
|
+
converted_uri.gsub!(/\\/, '/')
|
135
|
+
converted_uri = self.parse(converted_uri).normalize
|
136
|
+
if File.exists?(converted_uri.path) &&
|
137
|
+
File.stat(converted_uri.path).directory?
|
138
|
+
converted_uri.path.gsub!(/\/$/, "")
|
139
|
+
converted_uri.path = converted_uri.path + '/'
|
140
|
+
end
|
141
|
+
else
|
142
|
+
converted_uri = self.parse(converted_uri)
|
143
|
+
end
|
144
|
+
|
145
|
+
return converted_uri
|
146
|
+
end
|
147
|
+
|
148
|
+
# Expands a URI template into a full URI.
|
149
|
+
#
|
150
|
+
# An optional processor object may be supplied. The object should
|
151
|
+
# respond to either the :validate or :transform messages or both.
|
152
|
+
# Both the :validate and :transform methods should take two parameters:
|
153
|
+
# :name and :value. The :validate method should return true or false;
|
154
|
+
# true if the value of the variable is valid, false otherwise. The
|
155
|
+
# :transform method should return the transformed variable value as a
|
156
|
+
# string.
|
157
|
+
#
|
158
|
+
# An example:
|
159
|
+
#
|
160
|
+
# class ExampleProcessor
|
161
|
+
# def self.validate(name, value)
|
162
|
+
# return !!(value =~ /^[\w ]+$/) if name == "query"
|
163
|
+
# return true
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# def self.transform(name, value)
|
167
|
+
# return value.gsub(/ /, "+") if name == "query"
|
168
|
+
# return value
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# Addressable::URI.expand_template(
|
173
|
+
# "http://example.com/search/{query}/",
|
174
|
+
# {"query" => "an example search query"},
|
175
|
+
# ExampleProcessor).to_s
|
176
|
+
# => "http://example.com/search/an+example+search+query/"
|
177
|
+
def self.expand_template(pattern, mapping, processor=nil)
|
178
|
+
result = pattern.dup
|
179
|
+
for name, value in mapping
|
180
|
+
transformed_value = value
|
181
|
+
if processor != nil
|
182
|
+
if processor.respond_to?(:validate)
|
183
|
+
if !processor.validate(name, value)
|
184
|
+
raise InvalidTemplateValue,
|
185
|
+
"(#{name}, #{value}) is an invalid template value."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
if processor.respond_to?(:transform)
|
189
|
+
transformed_value = processor.transform(name, value)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Handle percent escaping
|
194
|
+
transformed_value = self.encode_segment(transformed_value,
|
195
|
+
Addressable::URI::CharacterClasses::RESERVED +
|
196
|
+
Addressable::URI::CharacterClasses::UNRESERVED)
|
197
|
+
|
198
|
+
result.gsub!(/\{#{Regexp.escape(name)}\}/, transformed_value)
|
199
|
+
end
|
200
|
+
result.gsub!(/\{[#{CharacterClasses::UNRESERVED}]+\}/, "")
|
201
|
+
return Addressable::URI.parse(result)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Joins several uris together.
|
205
|
+
def self.join(*uris)
|
206
|
+
uri_objects = uris.collect do |uri|
|
207
|
+
uri.kind_of?(self) ? uri : self.parse(uri.to_s)
|
208
|
+
end
|
209
|
+
result = uri_objects.shift.dup
|
210
|
+
for uri in uri_objects
|
211
|
+
result.merge!(uri)
|
212
|
+
end
|
213
|
+
return result
|
214
|
+
end
|
215
|
+
|
216
|
+
# Percent encodes a URI segment. Returns a string. Takes an optional
|
217
|
+
# character class parameter, which should be specified as a string
|
218
|
+
# containing a regular expression character class (not including the
|
219
|
+
# surrounding square brackets). The character class parameter defaults
|
220
|
+
# to the reserved plus unreserved character classes specified in
|
221
|
+
# RFC 3986. Usage of the constants within the CharacterClasses module is
|
222
|
+
# highly recommended when using this method.
|
223
|
+
#
|
224
|
+
# An example:
|
225
|
+
#
|
226
|
+
# Addressable::URI.escape_segment("simple-example", "b-zB-Z0-9")
|
227
|
+
# => "simple%2Dex%61mple"
|
228
|
+
def self.encode_segment(segment, character_class=
|
229
|
+
Addressable::URI::CharacterClasses::RESERVED +
|
230
|
+
Addressable::URI::CharacterClasses::UNRESERVED)
|
231
|
+
return nil if segment.nil?
|
232
|
+
return segment.gsub(
|
233
|
+
/[^#{character_class}]/
|
234
|
+
) do |sequence|
|
235
|
+
("%" + sequence.unpack('C')[0].to_s(16).upcase)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Unencodes any percent encoded characters within a URI segment.
|
240
|
+
# Returns a string.
|
241
|
+
def self.unencode_segment(segment)
|
242
|
+
return nil if segment.nil?
|
243
|
+
return segment.to_s.gsub(/%[0-9a-f]{2}/i) do |sequence|
|
244
|
+
sequence[1..3].to_i(16).chr
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Percent encodes any special characters in the URI. This method does
|
249
|
+
# not take IRIs or IDNs into account.
|
250
|
+
def self.encode(uri)
|
251
|
+
uri_object = uri.kind_of?(self) ? uri : self.parse(uri.to_s)
|
252
|
+
return Addressable::URI.new(
|
253
|
+
self.encode_segment(uri_object.scheme,
|
254
|
+
Addressable::URI::CharacterClasses::SCHEME),
|
255
|
+
self.encode_segment(uri_object.user,
|
256
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
257
|
+
self.encode_segment(uri_object.password,
|
258
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
259
|
+
self.encode_segment(uri_object.host,
|
260
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
261
|
+
self.encode_segment(uri_object.specified_port,
|
262
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
263
|
+
self.encode_segment(uri_object.path,
|
264
|
+
Addressable::URI::CharacterClasses::PATH),
|
265
|
+
self.encode_segment(uri_object.query,
|
266
|
+
Addressable::URI::CharacterClasses::QUERY),
|
267
|
+
self.encode_segment(uri_object.fragment,
|
268
|
+
Addressable::URI::CharacterClasses::FRAGMENT)
|
269
|
+
).to_s
|
270
|
+
end
|
271
|
+
|
272
|
+
class << self
|
273
|
+
alias_method :escape, :encode
|
274
|
+
end
|
275
|
+
|
276
|
+
# Normalizes the encoding of a URI. Characters within a hostname are
|
277
|
+
# not percent encoded to allow for internationalized domain names.
|
278
|
+
def self.normalized_encode(uri)
|
279
|
+
uri_object = uri.kind_of?(self) ? uri : self.parse(uri.to_s)
|
280
|
+
segments = {
|
281
|
+
:scheme => self.unencode_segment(uri_object.scheme),
|
282
|
+
:user => self.unencode_segment(uri_object.user),
|
283
|
+
:password => self.unencode_segment(uri_object.password),
|
284
|
+
:host => self.unencode_segment(uri_object.host),
|
285
|
+
:port => self.unencode_segment(uri_object.specified_port),
|
286
|
+
:path => self.unencode_segment(uri_object.path),
|
287
|
+
:query => self.unencode_segment(uri_object.query),
|
288
|
+
:fragment => self.unencode_segment(uri_object.fragment)
|
289
|
+
}
|
290
|
+
if URI::IDNA.send(:use_libidn?)
|
291
|
+
segments.each do |key, value|
|
292
|
+
if value != nil
|
293
|
+
segments[key] = IDN::Stringprep.nfkc_normalize(value.to_s)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
return Addressable::URI.new(
|
298
|
+
self.encode_segment(segments[:scheme],
|
299
|
+
Addressable::URI::CharacterClasses::SCHEME),
|
300
|
+
self.encode_segment(segments[:user],
|
301
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
302
|
+
self.encode_segment(segments[:password],
|
303
|
+
Addressable::URI::CharacterClasses::AUTHORITY),
|
304
|
+
segments[:host],
|
305
|
+
segments[:port],
|
306
|
+
self.encode_segment(segments[:path],
|
307
|
+
Addressable::URI::CharacterClasses::PATH),
|
308
|
+
self.encode_segment(segments[:query],
|
309
|
+
Addressable::URI::CharacterClasses::QUERY),
|
310
|
+
self.encode_segment(segments[:fragment],
|
311
|
+
Addressable::URI::CharacterClasses::FRAGMENT)
|
312
|
+
).to_s
|
313
|
+
end
|
314
|
+
|
315
|
+
# Extracts uris from an arbitrary body of text.
|
316
|
+
def self.extract(text, options={})
|
317
|
+
defaults = {:base => nil, :parse => false}
|
318
|
+
options = defaults.merge(options)
|
319
|
+
raise InvalidOptionError unless (options.keys - defaults.keys).empty?
|
320
|
+
# This regular expression needs to be less forgiving or else it would
|
321
|
+
# match virtually all text. Which isn't exactly what we're going for.
|
322
|
+
extract_regex = /((([a-z\+]+):)[^ \n\<\>\"\\]+[\w\/])/
|
323
|
+
extracted_uris =
|
324
|
+
text.scan(extract_regex).collect { |match| match[0] }
|
325
|
+
sgml_extract_regex = /<[^>]+href=\"([^\"]+?)\"[^>]*>/
|
326
|
+
sgml_extracted_uris =
|
327
|
+
text.scan(sgml_extract_regex).collect { |match| match[0] }
|
328
|
+
extracted_uris.concat(sgml_extracted_uris - extracted_uris)
|
329
|
+
textile_extract_regex = /\".+?\":([^ ]+\/[^ ]+)[ \,\.\;\:\?\!\<\>\"]/i
|
330
|
+
textile_extracted_uris =
|
331
|
+
text.scan(textile_extract_regex).collect { |match| match[0] }
|
332
|
+
extracted_uris.concat(textile_extracted_uris - extracted_uris)
|
333
|
+
parsed_uris = []
|
334
|
+
base_uri = nil
|
335
|
+
if options[:base] != nil
|
336
|
+
base_uri = options[:base] if options[:base].kind_of?(self)
|
337
|
+
base_uri = self.parse(options[:base].to_s) if base_uri == nil
|
338
|
+
end
|
339
|
+
for uri_string in extracted_uris
|
340
|
+
begin
|
341
|
+
if base_uri == nil
|
342
|
+
parsed_uris << self.parse(uri_string)
|
343
|
+
else
|
344
|
+
parsed_uris << (base_uri + self.parse(uri_string))
|
345
|
+
end
|
346
|
+
rescue Exception
|
347
|
+
nil
|
348
|
+
end
|
349
|
+
end
|
350
|
+
parsed_uris.reject! do |uri|
|
351
|
+
(uri.scheme =~ /T\d+/ ||
|
352
|
+
uri.scheme == "xmlns" ||
|
353
|
+
uri.scheme == "xml" ||
|
354
|
+
uri.scheme == "thr" ||
|
355
|
+
uri.scheme == "this" ||
|
356
|
+
uri.scheme == "float" ||
|
357
|
+
uri.scheme == "user" ||
|
358
|
+
uri.scheme == "username" ||
|
359
|
+
uri.scheme == "out")
|
360
|
+
end
|
361
|
+
if options[:parse]
|
362
|
+
return parsed_uris
|
363
|
+
else
|
364
|
+
return parsed_uris.collect { |uri| uri.to_s }
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Creates a new uri object from component parts. Passing nil for
|
369
|
+
# any of these parameters is acceptable.
|
370
|
+
def initialize(scheme, user, password, host, port, path, query, fragment)
|
371
|
+
@scheme = scheme
|
372
|
+
@scheme = nil if @scheme.to_s.strip == ""
|
373
|
+
@user = user
|
374
|
+
@password = password
|
375
|
+
@host = host
|
376
|
+
@specified_port = port.to_s
|
377
|
+
@port = port
|
378
|
+
@port = @port.to_s if @port.kind_of?(Fixnum)
|
379
|
+
if @port != nil && !(@port =~ /^\d+$/)
|
380
|
+
raise InvalidURIError,
|
381
|
+
"Invalid port number: #{@port.inspect}"
|
382
|
+
end
|
383
|
+
@port = @port.to_i
|
384
|
+
@port = nil if @port == 0
|
385
|
+
@path = path
|
386
|
+
@query = query
|
387
|
+
@fragment = fragment
|
388
|
+
|
389
|
+
validate()
|
390
|
+
end
|
391
|
+
|
392
|
+
# Returns the scheme (protocol) for this URI.
|
393
|
+
def scheme
|
394
|
+
return @scheme
|
395
|
+
end
|
396
|
+
|
397
|
+
# Sets the scheme (protocol for this URI.)
|
398
|
+
def scheme=(new_scheme)
|
399
|
+
@scheme = new_scheme
|
400
|
+
end
|
401
|
+
|
402
|
+
# Returns the user for this URI.
|
403
|
+
def user
|
404
|
+
return @user
|
405
|
+
end
|
406
|
+
|
407
|
+
# Sets the user for this URI.
|
408
|
+
def user=(new_user)
|
409
|
+
@user = new_user
|
410
|
+
|
411
|
+
# You can't have a nil user with a non-nil password
|
412
|
+
if @password != nil
|
413
|
+
@user = "" if @user.nil?
|
414
|
+
end
|
415
|
+
|
416
|
+
# Reset dependant values
|
417
|
+
@userinfo = nil
|
418
|
+
@authority = nil
|
419
|
+
|
420
|
+
# Ensure we haven't created an invalid URI
|
421
|
+
validate()
|
422
|
+
end
|
423
|
+
|
424
|
+
# Returns the password for this URI.
|
425
|
+
def password
|
426
|
+
return @password
|
427
|
+
end
|
428
|
+
|
429
|
+
# Sets the password for this URI.
|
430
|
+
def password=(new_password)
|
431
|
+
@password = new_password
|
432
|
+
|
433
|
+
# You can't have a nil user with a non-nil password
|
434
|
+
if @password != nil
|
435
|
+
@user = "" if @user.nil?
|
436
|
+
end
|
437
|
+
|
438
|
+
# Reset dependant values
|
439
|
+
@userinfo = nil
|
440
|
+
@authority = nil
|
441
|
+
|
442
|
+
# Ensure we haven't created an invalid URI
|
443
|
+
validate()
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns the username and password segment of this URI.
|
447
|
+
def userinfo
|
448
|
+
if !defined?(@userinfo) || @userinfo.nil?
|
449
|
+
current_user = self.user
|
450
|
+
current_password = self.password
|
451
|
+
if current_user == nil && current_password == nil
|
452
|
+
@userinfo = nil
|
453
|
+
elsif current_user != nil && current_password == nil
|
454
|
+
@userinfo = "#{current_user}"
|
455
|
+
elsif current_user != nil && current_password != nil
|
456
|
+
@userinfo = "#{current_user}:#{current_password}"
|
457
|
+
end
|
458
|
+
end
|
459
|
+
return @userinfo
|
460
|
+
end
|
461
|
+
|
462
|
+
# Sets the username and password segment of this URI.
|
463
|
+
def userinfo=(new_userinfo)
|
464
|
+
new_user = new_userinfo.to_s.strip.scan(/^(.*):/).flatten[0]
|
465
|
+
new_password = new_userinfo.to_s.strip.scan(/:(.*)$/).flatten[0]
|
466
|
+
|
467
|
+
# Password assigned first to ensure validity in case of nil
|
468
|
+
self.password = new_password
|
469
|
+
self.user = new_user
|
470
|
+
|
471
|
+
# Reset dependant values
|
472
|
+
@authority = nil
|
473
|
+
|
474
|
+
# Ensure we haven't created an invalid URI
|
475
|
+
validate()
|
476
|
+
end
|
477
|
+
|
478
|
+
# Returns the host for this URI.
|
479
|
+
def host
|
480
|
+
return @host
|
481
|
+
end
|
482
|
+
|
483
|
+
# Sets the host for this URI.
|
484
|
+
def host=(new_host)
|
485
|
+
@host = new_host
|
486
|
+
|
487
|
+
# Reset dependant values
|
488
|
+
@authority = nil
|
489
|
+
|
490
|
+
# Ensure we haven't created an invalid URI
|
491
|
+
validate()
|
492
|
+
end
|
493
|
+
|
494
|
+
# Returns the authority segment of this URI.
|
495
|
+
def authority
|
496
|
+
if !defined?(@authority) || @authority.nil?
|
497
|
+
return nil if self.host.nil?
|
498
|
+
@authority = ""
|
499
|
+
if self.userinfo != nil
|
500
|
+
@authority << "#{self.userinfo}@"
|
501
|
+
end
|
502
|
+
@authority << self.host
|
503
|
+
if self.specified_port != nil
|
504
|
+
@authority << ":#{self.specified_port}"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
return @authority
|
508
|
+
end
|
509
|
+
|
510
|
+
# Sets the authority segment of this URI.
|
511
|
+
def authority=(new_authority)
|
512
|
+
if new_authority != nil
|
513
|
+
new_userinfo = new_authority.scan(/^([^\[\]]*)@/).flatten[0]
|
514
|
+
if new_userinfo != nil
|
515
|
+
new_user = new_userinfo.strip.scan(/^([^:]*):?/).flatten[0]
|
516
|
+
new_password = new_userinfo.strip.scan(/:(.*)$/).flatten[0]
|
517
|
+
end
|
518
|
+
new_host =
|
519
|
+
new_authority.gsub(/^([^\[\]]*)@/, "").gsub(/:([^:@\[\]]*?)$/, "")
|
520
|
+
new_port =
|
521
|
+
new_authority.scan(/:([^:@\[\]]*?)$/).flatten[0]
|
522
|
+
end
|
523
|
+
new_port = nil if new_port == ""
|
524
|
+
|
525
|
+
# Password assigned first to ensure validity in case of nil
|
526
|
+
self.password = new_password
|
527
|
+
self.user = new_user
|
528
|
+
self.host = new_host
|
529
|
+
|
530
|
+
# Port reset to allow port normalization
|
531
|
+
@port = nil
|
532
|
+
@specified_port = new_port
|
533
|
+
|
534
|
+
# Ensure we haven't created an invalid URI
|
535
|
+
validate()
|
536
|
+
end
|
537
|
+
|
538
|
+
# Returns an array of known ip-based schemes. These schemes typically
|
539
|
+
# use a similar URI form:
|
540
|
+
# //<user>:<password>@<host>:<port>/<url-path>
|
541
|
+
def self.ip_based_schemes
|
542
|
+
return self.scheme_mapping.keys
|
543
|
+
end
|
544
|
+
|
545
|
+
# Returns a hash of common IP-based schemes and their default port
|
546
|
+
# numbers. Adding new schemes to this hash, as necessary, will allow
|
547
|
+
# for better URI normalization.
|
548
|
+
def self.scheme_mapping
|
549
|
+
if !defined?(@protocol_mapping) || @protocol_mapping.nil?
|
550
|
+
@protocol_mapping = {
|
551
|
+
"http" => 80,
|
552
|
+
"https" => 443,
|
553
|
+
"ftp" => 21,
|
554
|
+
"tftp" => 69,
|
555
|
+
"ssh" => 22,
|
556
|
+
"svn+ssh" => 22,
|
557
|
+
"telnet" => 23,
|
558
|
+
"nntp" => 119,
|
559
|
+
"gopher" => 70,
|
560
|
+
"wais" => 210,
|
561
|
+
"ldap" => 389,
|
562
|
+
"prospero" => 1525
|
563
|
+
}
|
564
|
+
end
|
565
|
+
return @protocol_mapping
|
566
|
+
end
|
567
|
+
|
568
|
+
# Returns the port number for this URI. This method will normalize to the
|
569
|
+
# default port for the URI's scheme if the port isn't explicitly specified
|
570
|
+
# in the URI.
|
571
|
+
def port
|
572
|
+
if @port.to_i == 0
|
573
|
+
if self.scheme.nil?
|
574
|
+
@port = nil
|
575
|
+
else
|
576
|
+
@port = self.class.scheme_mapping[self.scheme.strip.downcase]
|
577
|
+
end
|
578
|
+
return @port
|
579
|
+
else
|
580
|
+
@port = @port.to_i
|
581
|
+
return @port
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
# Sets the port for this URI.
|
586
|
+
def port=(new_port)
|
587
|
+
@port = new_port.to_s.to_i
|
588
|
+
@specified_port = @port
|
589
|
+
@authority = nil
|
590
|
+
end
|
591
|
+
|
592
|
+
# Returns the port number that was actually specified in the URI string.
|
593
|
+
def specified_port
|
594
|
+
port = @specified_port.to_s.to_i
|
595
|
+
if port == 0
|
596
|
+
return nil
|
597
|
+
else
|
598
|
+
return port
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Returns the path for this URI.
|
603
|
+
def path
|
604
|
+
return @path
|
605
|
+
end
|
606
|
+
|
607
|
+
# Sets the path for this URI.
|
608
|
+
def path=(new_path)
|
609
|
+
@path = new_path
|
610
|
+
end
|
611
|
+
|
612
|
+
# Returns the basename, if any, of the file at the path being referenced.
|
613
|
+
# Returns nil if there is no path component.
|
614
|
+
def basename
|
615
|
+
return nil if self.path == nil
|
616
|
+
return File.basename(self.path).gsub(/;[^\/]*$/, "")
|
617
|
+
end
|
618
|
+
|
619
|
+
# Returns the extension, if any, of the file at the path being referenced.
|
620
|
+
# Returns "" if there is no extension or nil if there is no path
|
621
|
+
# component.
|
622
|
+
def extname
|
623
|
+
return nil if self.path == nil
|
624
|
+
return File.extname(self.basename.gsub(/;[^\/]*$/, ""))
|
625
|
+
end
|
626
|
+
|
627
|
+
# Returns the query string for this URI.
|
628
|
+
def query
|
629
|
+
return @query
|
630
|
+
end
|
631
|
+
|
632
|
+
# Sets the query string for this URI.
|
633
|
+
def query=(new_query)
|
634
|
+
@query = new_query
|
635
|
+
end
|
636
|
+
|
637
|
+
# Returns the fragment for this URI.
|
638
|
+
def fragment
|
639
|
+
return @fragment
|
640
|
+
end
|
641
|
+
|
642
|
+
# Sets the fragment for this URI.
|
643
|
+
def fragment=(new_fragment)
|
644
|
+
@fragment = new_fragment
|
645
|
+
end
|
646
|
+
|
647
|
+
# Returns true if the URI uses an IP-based protocol.
|
648
|
+
def ip_based?
|
649
|
+
return false if self.scheme.nil?
|
650
|
+
return self.class.ip_based_schemes.include?(self.scheme.strip.downcase)
|
651
|
+
end
|
652
|
+
|
653
|
+
# Returns true if this URI is known to be relative.
|
654
|
+
def relative?
|
655
|
+
return self.scheme.nil?
|
656
|
+
end
|
657
|
+
|
658
|
+
# Returns true if this URI is known to be absolute.
|
659
|
+
def absolute?
|
660
|
+
return !relative?
|
661
|
+
end
|
662
|
+
|
663
|
+
# Joins two URIs together.
|
664
|
+
def +(uri)
|
665
|
+
if !uri.kind_of?(self.class)
|
666
|
+
uri = URI.parse(uri.to_s)
|
667
|
+
end
|
668
|
+
if uri.to_s == ""
|
669
|
+
return self.dup
|
670
|
+
end
|
671
|
+
|
672
|
+
joined_scheme = nil
|
673
|
+
joined_user = nil
|
674
|
+
joined_password = nil
|
675
|
+
joined_host = nil
|
676
|
+
joined_port = nil
|
677
|
+
joined_path = nil
|
678
|
+
joined_query = nil
|
679
|
+
joined_fragment = nil
|
680
|
+
|
681
|
+
# Section 5.2.2 of RFC 3986
|
682
|
+
if uri.scheme != nil
|
683
|
+
joined_scheme = uri.scheme
|
684
|
+
joined_user = uri.user
|
685
|
+
joined_password = uri.password
|
686
|
+
joined_host = uri.host
|
687
|
+
joined_port = uri.specified_port
|
688
|
+
joined_path = self.class.normalize_path(uri.path)
|
689
|
+
joined_query = uri.query
|
690
|
+
else
|
691
|
+
if uri.authority != nil
|
692
|
+
joined_user = uri.user
|
693
|
+
joined_password = uri.password
|
694
|
+
joined_host = uri.host
|
695
|
+
joined_port = uri.specified_port
|
696
|
+
joined_path = self.class.normalize_path(uri.path)
|
697
|
+
joined_query = uri.query
|
698
|
+
else
|
699
|
+
if uri.path == nil || uri.path == ""
|
700
|
+
joined_path = self.path
|
701
|
+
if uri.query != nil
|
702
|
+
joined_query = uri.query
|
703
|
+
else
|
704
|
+
joined_query = self.query
|
705
|
+
end
|
706
|
+
else
|
707
|
+
if uri.path[0..0] == "/"
|
708
|
+
joined_path = self.class.normalize_path(uri.path)
|
709
|
+
else
|
710
|
+
base_path = self.path.nil? ? "" : self.path.dup
|
711
|
+
base_path = self.class.normalize_path(base_path)
|
712
|
+
base_path.gsub!(/\/[^\/]+$/, "/")
|
713
|
+
joined_path = self.class.normalize_path(base_path + uri.path)
|
714
|
+
end
|
715
|
+
joined_query = uri.query
|
716
|
+
end
|
717
|
+
joined_user = self.user
|
718
|
+
joined_password = self.password
|
719
|
+
joined_host = self.host
|
720
|
+
joined_port = self.specified_port
|
721
|
+
end
|
722
|
+
joined_scheme = self.scheme
|
723
|
+
end
|
724
|
+
joined_fragment = uri.fragment
|
725
|
+
|
726
|
+
return Addressable::URI.new(
|
727
|
+
joined_scheme,
|
728
|
+
joined_user,
|
729
|
+
joined_password,
|
730
|
+
joined_host,
|
731
|
+
joined_port,
|
732
|
+
joined_path,
|
733
|
+
joined_query,
|
734
|
+
joined_fragment
|
735
|
+
)
|
736
|
+
end
|
737
|
+
|
738
|
+
# Merges two URIs together.
|
739
|
+
def merge(uri)
|
740
|
+
return self + uri
|
741
|
+
end
|
742
|
+
|
743
|
+
# Destructive form of merge.
|
744
|
+
def merge!(uri)
|
745
|
+
replace_self(self.merge(uri))
|
746
|
+
end
|
747
|
+
|
748
|
+
# Returns the shortest normalized relative form of this URI that uses the
|
749
|
+
# supplied URI as a base for resolution. Returns an absolute URI if
|
750
|
+
# necessary.
|
751
|
+
def route_from(uri)
|
752
|
+
uri = uri.kind_of?(self.class) ? uri : self.class.parse(uri.to_s)
|
753
|
+
uri = uri.normalize
|
754
|
+
normalized_self = self.normalize
|
755
|
+
if normalized_self.relative?
|
756
|
+
raise ArgumentError, "Expected absolute URI, got: #{self.to_s}"
|
757
|
+
end
|
758
|
+
if uri.relative?
|
759
|
+
raise ArgumentError, "Expected absolute URI, got: #{self.to_s}"
|
760
|
+
end
|
761
|
+
if normalized_self == uri
|
762
|
+
return Addressable::URI.parse("##{normalized_self.fragment}")
|
763
|
+
end
|
764
|
+
segments = normalized_self.to_h
|
765
|
+
if normalized_self.scheme == uri.scheme
|
766
|
+
segments[:scheme] = nil
|
767
|
+
if normalized_self.authority == uri.authority
|
768
|
+
segments[:user] = nil
|
769
|
+
segments[:password] = nil
|
770
|
+
segments[:host] = nil
|
771
|
+
segments[:port] = nil
|
772
|
+
if normalized_self.path == uri.path
|
773
|
+
segments[:path] = nil
|
774
|
+
if normalized_self.query == uri.query
|
775
|
+
segments[:query] = nil
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
end
|
780
|
+
# Avoid network-path references.
|
781
|
+
if segments[:scheme] == nil && segments[:host] != nil
|
782
|
+
segments[:scheme] = normalized_self.scheme
|
783
|
+
end
|
784
|
+
return Addressable::URI.new(
|
785
|
+
segments[:scheme],
|
786
|
+
segments[:user],
|
787
|
+
segments[:password],
|
788
|
+
segments[:host],
|
789
|
+
segments[:port],
|
790
|
+
segments[:path],
|
791
|
+
segments[:query],
|
792
|
+
segments[:fragment]
|
793
|
+
)
|
794
|
+
end
|
795
|
+
|
796
|
+
# Returns the shortest normalized relative form of the supplied URI that
|
797
|
+
# uses this URI as a base for resolution. Returns an absolute URI if
|
798
|
+
# necessary.
|
799
|
+
def route_to(uri)
|
800
|
+
uri = uri.kind_of?(self.class) ? uri : self.class.parse(uri.to_s)
|
801
|
+
return uri.route_from(self)
|
802
|
+
end
|
803
|
+
|
804
|
+
# Returns a normalized URI object.
|
805
|
+
#
|
806
|
+
# NOTE: This method does not attempt to fully conform to specifications.
|
807
|
+
# It exists largely to correct other people's failures to read the
|
808
|
+
# specifications, and also to deal with caching issues since several
|
809
|
+
# different URIs may represent the same resource and should not be
|
810
|
+
# cached multiple times.
|
811
|
+
def normalize
|
812
|
+
normalized_scheme = nil
|
813
|
+
normalized_scheme = self.scheme.strip.downcase if self.scheme != nil
|
814
|
+
normalized_scheme = "svn+ssh" if normalized_scheme == "ssh+svn"
|
815
|
+
if normalized_scheme == "feed"
|
816
|
+
if self.to_s =~ /^feed:\/*http:\/*/
|
817
|
+
return self.class.parse(
|
818
|
+
self.to_s.scan(/^feed:\/*(http:\/*.*)/).flatten[0]).normalize
|
819
|
+
end
|
820
|
+
end
|
821
|
+
normalized_user = nil
|
822
|
+
normalized_user = self.user.strip if self.user != nil
|
823
|
+
normalized_password = nil
|
824
|
+
normalized_password = self.password.strip if self.password != nil
|
825
|
+
normalized_host = nil
|
826
|
+
normalized_host = self.host.strip.downcase if self.host != nil
|
827
|
+
if normalized_host != nil
|
828
|
+
begin
|
829
|
+
normalized_host = URI::IDNA.to_ascii(normalized_host)
|
830
|
+
rescue Exception
|
831
|
+
nil
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
normalized_port = self.port
|
836
|
+
if self.class.scheme_mapping[normalized_scheme] == normalized_port
|
837
|
+
normalized_port = nil
|
838
|
+
end
|
839
|
+
normalized_path = nil
|
840
|
+
normalized_path = self.path.strip if self.path != nil
|
841
|
+
if normalized_path == nil &&
|
842
|
+
normalized_scheme != nil &&
|
843
|
+
normalized_host != nil
|
844
|
+
normalized_path = "/"
|
845
|
+
end
|
846
|
+
if normalized_path != nil
|
847
|
+
normalized_path = self.class.normalize_path(normalized_path)
|
848
|
+
end
|
849
|
+
if normalized_path == ""
|
850
|
+
if ["http", "https", "ftp", "tftp"].include?(normalized_scheme)
|
851
|
+
normalized_path = "/"
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
normalized_query = nil
|
856
|
+
normalized_query = self.query.strip if self.query != nil
|
857
|
+
|
858
|
+
normalized_fragment = nil
|
859
|
+
normalized_fragment = self.fragment.strip if self.fragment != nil
|
860
|
+
return Addressable::URI.parse(
|
861
|
+
Addressable::URI.normalized_encode(Addressable::URI.new(
|
862
|
+
normalized_scheme,
|
863
|
+
normalized_user,
|
864
|
+
normalized_password,
|
865
|
+
normalized_host,
|
866
|
+
normalized_port,
|
867
|
+
normalized_path,
|
868
|
+
normalized_query,
|
869
|
+
normalized_fragment
|
870
|
+
)))
|
871
|
+
end
|
872
|
+
|
873
|
+
# Destructively normalizes this URI object.
|
874
|
+
def normalize!
|
875
|
+
replace_self(self.normalize)
|
876
|
+
end
|
877
|
+
|
878
|
+
# Creates a URI suitable for display to users. If semantic attacks are
|
879
|
+
# likely, the application should try to detect these and warn the user.
|
880
|
+
# See RFC 3986 section 7.6 for more information.
|
881
|
+
def display_uri
|
882
|
+
display_uri = self.normalize
|
883
|
+
begin
|
884
|
+
display_uri.instance_variable_set("@host",
|
885
|
+
URI::IDNA.to_unicode(display_uri.host))
|
886
|
+
rescue Exception
|
887
|
+
nil
|
888
|
+
end
|
889
|
+
return display_uri
|
890
|
+
end
|
891
|
+
|
892
|
+
# Returns true if the URI objects are equal. This method normalizes
|
893
|
+
# both URIs before doing the comparison, and allows comparison against
|
894
|
+
# strings.
|
895
|
+
def ===(uri)
|
896
|
+
uri_string = nil
|
897
|
+
if uri.respond_to?(:normalize)
|
898
|
+
uri_string = uri.normalize.to_s
|
899
|
+
else
|
900
|
+
begin
|
901
|
+
uri_string = URI.parse(uri.to_s).normalize.to_s
|
902
|
+
rescue Exception
|
903
|
+
return false
|
904
|
+
end
|
905
|
+
end
|
906
|
+
return self.normalize.to_s == uri_string
|
907
|
+
end
|
908
|
+
|
909
|
+
# Returns true if the URI objects are equal. This method normalizes
|
910
|
+
# both URIs before doing the comparison.
|
911
|
+
def ==(uri)
|
912
|
+
return false unless uri.kind_of?(self.class)
|
913
|
+
return self.normalize.to_s == uri.normalize.to_s
|
914
|
+
end
|
915
|
+
|
916
|
+
# Returns true if the URI objects are equal. This method does NOT
|
917
|
+
# normalize either URI before doing the comparison.
|
918
|
+
def eql?(uri)
|
919
|
+
return false unless uri.kind_of?(self.class)
|
920
|
+
return self.to_s == uri.to_s
|
921
|
+
end
|
922
|
+
|
923
|
+
# Clones the URI object.
|
924
|
+
def dup
|
925
|
+
duplicated_scheme = nil
|
926
|
+
duplicated_scheme = self.scheme.dup if self.scheme != nil
|
927
|
+
duplicated_user = nil
|
928
|
+
duplicated_user = self.user.dup if self.user != nil
|
929
|
+
duplicated_password = nil
|
930
|
+
duplicated_password = self.password.dup if self.password != nil
|
931
|
+
duplicated_host = nil
|
932
|
+
duplicated_host = self.host.dup if self.host != nil
|
933
|
+
duplicated_port = self.port
|
934
|
+
duplicated_path = nil
|
935
|
+
duplicated_path = self.path.dup if self.path != nil
|
936
|
+
duplicated_query = nil
|
937
|
+
duplicated_query = self.query.dup if self.query != nil
|
938
|
+
duplicated_fragment = nil
|
939
|
+
duplicated_fragment = self.fragment.dup if self.fragment != nil
|
940
|
+
duplicated_uri = Addressable::URI.new(
|
941
|
+
duplicated_scheme,
|
942
|
+
duplicated_user,
|
943
|
+
duplicated_password,
|
944
|
+
duplicated_host,
|
945
|
+
duplicated_port,
|
946
|
+
duplicated_path,
|
947
|
+
duplicated_query,
|
948
|
+
duplicated_fragment
|
949
|
+
)
|
950
|
+
@specified_port = nil if !defined?(@specified_port)
|
951
|
+
duplicated_uri.instance_variable_set("@specified_port", @specified_port)
|
952
|
+
return duplicated_uri
|
953
|
+
end
|
954
|
+
|
955
|
+
# Returns the assembled URI as a string.
|
956
|
+
def to_s
|
957
|
+
uri_string = ""
|
958
|
+
uri_string << "#{self.scheme}:" if self.scheme != nil
|
959
|
+
uri_string << "//#{self.authority}" if self.authority != nil
|
960
|
+
uri_string << self.path.to_s
|
961
|
+
uri_string << "?#{self.query}" if self.query != nil
|
962
|
+
uri_string << "##{self.fragment}" if self.fragment != nil
|
963
|
+
return uri_string
|
964
|
+
end
|
965
|
+
|
966
|
+
# Returns a Hash of the URI segments.
|
967
|
+
def to_h
|
968
|
+
return {
|
969
|
+
:scheme => self.scheme,
|
970
|
+
:user => self.user,
|
971
|
+
:password => self.password,
|
972
|
+
:host => self.host,
|
973
|
+
:port => self.specified_port,
|
974
|
+
:path => self.path,
|
975
|
+
:query => self.query,
|
976
|
+
:fragment => self.fragment
|
977
|
+
}
|
978
|
+
end
|
979
|
+
|
980
|
+
# Returns a string representation of the URI object's state.
|
981
|
+
def inspect
|
982
|
+
sprintf("#<%s:%#0x URI:%s>", self.class.to_s, self.object_id, self.to_s)
|
983
|
+
end
|
984
|
+
|
985
|
+
# This module handles internationalized domain names. When Ruby has an
|
986
|
+
# implementation of nameprep, stringprep, punycode, etc, this
|
987
|
+
# module should contain an actual implementation of IDNA instead of
|
988
|
+
# returning nil if libidn can't be used.
|
989
|
+
module IDNA
|
990
|
+
# Returns the ascii representation of the label.
|
991
|
+
def self.to_ascii(label)
|
992
|
+
return nil if label.nil?
|
993
|
+
if self.use_libidn?
|
994
|
+
return IDN::Idna.toASCII(label)
|
995
|
+
else
|
996
|
+
raise NotImplementedError,
|
997
|
+
"There is no available pure-ruby implementation. " +
|
998
|
+
"Install libidn bindings."
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Returns the unicode representation of the label.
|
1003
|
+
def self.to_unicode(label)
|
1004
|
+
return nil if label.nil?
|
1005
|
+
if self.use_libidn?
|
1006
|
+
return IDN::Idna.toUnicode(label)
|
1007
|
+
else
|
1008
|
+
raise NotImplementedError,
|
1009
|
+
"There is no available pure-ruby implementation. " +
|
1010
|
+
"Install libidn bindings."
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
private
|
1015
|
+
# Determines if the libidn bindings are available and able to be used.
|
1016
|
+
def self.use_libidn?
|
1017
|
+
if !defined?(@use_libidn) || @use_libidn.nil?
|
1018
|
+
begin
|
1019
|
+
require 'rubygems'
|
1020
|
+
rescue LoadError
|
1021
|
+
nil
|
1022
|
+
end
|
1023
|
+
begin
|
1024
|
+
require 'idn'
|
1025
|
+
rescue LoadError
|
1026
|
+
nil
|
1027
|
+
end
|
1028
|
+
@use_libidn = !!(defined?(IDN::Idna))
|
1029
|
+
end
|
1030
|
+
return @use_libidn
|
1031
|
+
end
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
private
|
1035
|
+
# Resolves paths to their simplest form.
|
1036
|
+
def self.normalize_path(path)
|
1037
|
+
return nil if path.nil?
|
1038
|
+
normalized_path = path.dup
|
1039
|
+
previous_state = normalized_path.dup
|
1040
|
+
begin
|
1041
|
+
previous_state = normalized_path.dup
|
1042
|
+
normalized_path.gsub!(/\/\.\//, "/")
|
1043
|
+
normalized_path.gsub!(/\/\.$/, "/")
|
1044
|
+
parent = normalized_path.scan(/\/([^\/]+)\/\.\.\//).flatten[0]
|
1045
|
+
if parent != "." && parent != ".."
|
1046
|
+
normalized_path.gsub!(/\/#{parent}\/\.\.\//, "/")
|
1047
|
+
end
|
1048
|
+
parent = normalized_path.scan(/\/([^\/]+)\/\.\.$/).flatten[0]
|
1049
|
+
if parent != "." && parent != ".."
|
1050
|
+
normalized_path.gsub!(/\/#{parent}\/\.\.$/, "/")
|
1051
|
+
end
|
1052
|
+
normalized_path.gsub!(/^\.\.?\/?/, "")
|
1053
|
+
normalized_path.gsub!(/^\/\.\.?\//, "/")
|
1054
|
+
end until previous_state == normalized_path
|
1055
|
+
return normalized_path
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
# Ensures that the URI is valid.
|
1059
|
+
def validate
|
1060
|
+
if self.scheme == nil && self.user == nil && self.password == nil &&
|
1061
|
+
self.host == nil && self.port == nil && self.path == nil &&
|
1062
|
+
self.query == nil && self.fragment == nil
|
1063
|
+
raise InvalidURIError, "All segments were nil."
|
1064
|
+
end
|
1065
|
+
if self.scheme != nil &&
|
1066
|
+
(self.host == nil || self.host == "") &&
|
1067
|
+
(self.path == nil || self.path == "")
|
1068
|
+
raise InvalidURIError,
|
1069
|
+
"Absolute URI missing hierarchical segment."
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
# Replaces the internal state of self with the specified URI's state.
|
1074
|
+
# Used in destructive operations to avoid massive code repetition.
|
1075
|
+
def replace_self(uri)
|
1076
|
+
# Reset dependant values
|
1077
|
+
@userinfo = nil
|
1078
|
+
@authority = nil
|
1079
|
+
|
1080
|
+
@scheme = uri.scheme
|
1081
|
+
@user = uri.user
|
1082
|
+
@password = uri.password
|
1083
|
+
@host = uri.host
|
1084
|
+
@specified_port = uri.instance_variable_get("@specified_port")
|
1085
|
+
@port = @specified_port.to_s.to_i
|
1086
|
+
@path = uri.path
|
1087
|
+
@query = uri.query
|
1088
|
+
@fragment = uri.fragment
|
1089
|
+
return self
|
1090
|
+
end
|
1091
|
+
end
|
1092
|
+
end
|