pyu-activesp 0.0.4.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,116 @@
1
+ # Copyright (c) 2010 XAOP bvba
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # 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
+ #
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ #
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module ActiveSP
27
+
28
+ class Role < Base
29
+
30
+ extend Caching
31
+ extend PersistentCaching
32
+ include Util
33
+ include InSite
34
+
35
+ persistent { |site, name, *a| [site.connection, [:role, name]] }
36
+ # @private
37
+ def initialize(site, name)
38
+ @site, @name = site, name
39
+ end
40
+
41
+ # See {Base#key}
42
+ # @return [String]
43
+ def key
44
+ encode_key("R", [@name])
45
+ end
46
+
47
+ # Returns the list of users in this role
48
+ # @return [User]
49
+ def users
50
+ call("UserGroup", "get_user_collection_from_role", "roleName" => @name).xpath("//spdir:User", NS).map do |row|
51
+ attributes = clean_attributes(row.attributes)
52
+ User.new(@site, attributes["LoginName"])
53
+ end
54
+ end
55
+ cache :users, :dup => :always
56
+
57
+ # Returns the list of groups in this role
58
+ # @return [Group]
59
+ def groups
60
+ call("UserGroup", "get_group_collection_from_role", "roleName" => @name).xpath("//spdir:Group", NS).map do |row|
61
+ attributes = clean_attributes(row.attributes)
62
+ Group.new(@site, attributes["Name"])
63
+ end
64
+ end
65
+ cache :groups, :dup => :always
66
+
67
+ # Returns true. The same method is present on {Group} where it returns false. Roles and groups can generally be
68
+ # duck-typed, and this method is there for the rare case where you do need to make the distinction
69
+ # @return [Boolean]
70
+ def is_role?
71
+ true
72
+ end
73
+
74
+ # See {Base#save}
75
+ # @return [void]
76
+ def save
77
+ p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes)
78
+ end
79
+
80
+ # @private
81
+ def to_s
82
+ "#<ActiveSP::Role name=#{@name}>"
83
+ end
84
+
85
+ # @private
86
+ alias inspect to_s
87
+
88
+ private
89
+
90
+ def data
91
+ call("UserGroup", "get_role_info", "roleName" => @name).xpath("//spdir:Role", NS).first
92
+ end
93
+ cache :data
94
+
95
+ def attributes_before_type_cast
96
+ clean_attributes(data.attributes)
97
+ end
98
+ cache :attributes_before_type_cast
99
+
100
+ def original_attributes
101
+ type_cast_attributes(@site, nil, internal_attribute_types, attributes_before_type_cast)
102
+ end
103
+ cache :original_attributes
104
+
105
+ def internal_attribute_types
106
+ @@internal_attribute_types ||= {
107
+ "Description" => GhostField.new("Description", "Text", false, true),
108
+ "ID" => GhostField.new("ID", "Text", false, true),
109
+ "Name" => GhostField.new("Name", "Text", false, true),
110
+ "Type" => GhostField.new("Type", "Integer", false, true)
111
+ }
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,128 @@
1
+ # Copyright (c) 2010 XAOP bvba
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # 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
+ #
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ #
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module ActiveSP
27
+
28
+ # @private
29
+ NS = {
30
+ "sp" => "http://schemas.microsoft.com/sharepoint/soap/",
31
+ "z" => "#RowsetSchema",
32
+ "spdir" => "http://schemas.microsoft.com/sharepoint/soap/directory/"
33
+ }
34
+
35
+ module Root
36
+
37
+ extend Caching
38
+
39
+ # Returns the root site as an object of class {Site}
40
+ # @return [Site]
41
+ def root
42
+ Site.new(self, @root_url)
43
+ end
44
+ cache :root
45
+
46
+ # Returns the list of users in the system
47
+ # @return [Array<User>]
48
+ def users
49
+ root.send(:call, "UserGroup", "get_user_collection_from_site").xpath("//spdir:User", NS).map do |row|
50
+ attributes = clean_attributes(row.attributes)
51
+ User.new(root, attributes["LoginName"])
52
+ end
53
+ end
54
+ cache :users, :dup => :always
55
+
56
+ # Returns the user with the given login, or nil when the user does not exist
57
+ # @param [String] login The login of the user
58
+ # @return [User, nil]
59
+ def user(login)
60
+ if user = users_by_login[login]
61
+ user
62
+ elsif data = root.send(:call, "UserGroup", "get_user_info", "userLoginName" => login).xpath("//spdir:User", NS).first
63
+ users_by_login[login] = User.new(root, login, clean_attributes(data))
64
+ end
65
+ end
66
+
67
+ # Returns the list of groups in the system
68
+ # @return [Array<Group>]
69
+ def groups
70
+ root.send(:call, "UserGroup", "get_group_collection_from_site").xpath("//spdir:Group", NS).map do |row|
71
+ attributes = clean_attributes(row.attributes)
72
+ Group.new(root, attributes["Name"])
73
+ end
74
+ end
75
+ cache :groups, :dup => :always
76
+
77
+ def group(name)
78
+ if group = groups_by_name[name]
79
+ group
80
+ end
81
+ end
82
+
83
+ # Returns the list of roles in the system
84
+ # @return [Array<Role>]
85
+ def roles
86
+ root.send(:call, "UserGroup", "get_role_collection_from_web").xpath("//spdir:Role", NS).map do |row|
87
+ attributes = clean_attributes(row.attributes)
88
+ Role.new(root, attributes["Name"])
89
+ end
90
+ end
91
+ cache :roles, :dup => :always
92
+
93
+ private
94
+
95
+ def users_by_login
96
+ users.inject({}) { |h, u| h[u.login_name] = u ; h }
97
+ end
98
+ cache :users_by_login
99
+
100
+ def groups_by_name
101
+ groups.inject({}) { |h, g| h[g.Name] = g ; h }
102
+ end
103
+ cache :groups_by_name
104
+
105
+ end
106
+
107
+ class Connection
108
+
109
+ include Root
110
+
111
+ end
112
+
113
+ # @private
114
+ module InSite
115
+
116
+ private
117
+
118
+ def call(*a, &b)
119
+ @site.send(:call, *a, &b)
120
+ end
121
+
122
+ def fetch(url)
123
+ @site.send(:fetch, url)
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,305 @@
1
+ # Copyright (c) 2010 XAOP bvba
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # 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
+ #
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ #
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module ActiveSP
27
+
28
+ class Site < Base
29
+
30
+ extend Caching
31
+ extend PersistentCaching
32
+ include Util
33
+
34
+ # The URL of this site
35
+ # @return [String]
36
+ attr_reader :url
37
+ # @private
38
+ attr_reader :connection
39
+
40
+ persistent { |connection, url, *a| [connection, [:site, url]] }
41
+ # @private
42
+ def initialize(connection, url, depth = 0)
43
+ @connection, @url, @depth = connection, url, depth
44
+ @services = {}
45
+ end
46
+
47
+ # @private
48
+ def relative_url(url = @url)
49
+ url[@connection.root_url.rindex("/") + 1..-1]
50
+ end
51
+
52
+ # Returns the containing site, or nil if this is the root site
53
+ # @return [Site]
54
+ def supersite
55
+ unless is_root_site?
56
+ Site.new(@connection, ::File.dirname(@url), @depth - 1)
57
+ end
58
+ end
59
+ cache :supersite
60
+
61
+ # Returns the root site, or this site if it is the root site
62
+ # @return [Site]
63
+ def rootsite
64
+ is_root_site? ? self : supersite.rootsite
65
+ end
66
+ cache :rootsite
67
+
68
+ # Returns true if this site is the root site
69
+ # @return [Boolean]
70
+ def is_root_site?
71
+ @depth == 0
72
+ end
73
+
74
+ # See {Base#key}
75
+ # @return [String]
76
+ def key # This documentation is not ideal. The ideal doesn't work out of the box
77
+ encode_key("S", [@url[@connection.root_url.length + 1..-1], @depth])
78
+ end
79
+
80
+ # Returns the list of sites below this site. Does not recurse
81
+ # @return [Array<List>]
82
+ def sites
83
+ result = call("Webs", "get_web_collection")
84
+ result.xpath("//sp:Web", NS).map { |web| Site.new(connection, web["Url"].to_s, @depth + 1) }
85
+ end
86
+ cache :sites, :dup => :always
87
+
88
+ # Returns the site with the given name. This name is what appears in the URL as name and is immutable. Return nil
89
+ # if such a site does not exist
90
+ # @param [String] name The name if the site
91
+ # @return [Site]
92
+ def site(name)
93
+ result = call("Webs", "get_web", "webUrl" => ::File.join(@url, name))
94
+ Site.new(connection, result.xpath("//sp:Web", NS).first["Url"].to_s, @depth + 1)
95
+ rescue Savon::SOAPFault
96
+ nil
97
+ end
98
+
99
+ # Returns the list if lists in this sute. Does not recurse
100
+ # @return [Array<List>]
101
+ def lists
102
+ result1 = call("Lists", "get_list_collection")
103
+ result2 = call("SiteData", "get_list_collection")
104
+ result2_by_id = {}
105
+ result2.xpath("//sp:_sList", NS).each do |element|
106
+ data = {}
107
+ element.children.each do |ch|
108
+ data[ch.name] = ch.inner_text
109
+ end
110
+ result2_by_id[data["InternalName"]] = data
111
+ end
112
+ result1.xpath("//sp:List", NS).select { |list| list["Title"] != "User Information List" }.map do |list|
113
+ List.new(self, list["ID"].to_s, list["Title"].to_s, clean_attributes(list.attributes), result2_by_id[list["ID"].to_s])
114
+ end
115
+ end
116
+ cache :lists, :dup => :always
117
+
118
+ # Returns the list with the given name. The name is what appears in the URL as name and is immutable. Returns nil
119
+ # if such a list does not exist
120
+ # @param [String] name The name of the list
121
+ # @return [List]
122
+ def list(name)
123
+ lists.find { |list| ::File.basename(list.url) == name }
124
+ end
125
+
126
+ # Returns the site or list with the given name, or nil if it does not exist
127
+ # @param [String] name The name of the site or list
128
+ # @return [Site, List]
129
+ def /(name)
130
+ list(name) || site(name)
131
+ end
132
+
133
+ # Returns the list of content types defined for this site. These include the content types defined on
134
+ # containing sites as they are automatically inherited
135
+ # @return [Array<ContentType>]
136
+ def content_types
137
+ result = call("Webs", "get_content_types", "listName" => @id)
138
+ result.xpath("//sp:ContentType", NS).map do |content_type|
139
+ supersite && supersite.content_type(content_type["ID"]) || ContentType.new(self, nil, content_type["ID"], content_type["Name"], content_type["Description"], content_type["Version"], content_type["Group"])
140
+ end
141
+ end
142
+ cache :content_types, :dup => :always
143
+
144
+ # @private
145
+ def content_type(id)
146
+ content_types.find { |t| t.id == id }
147
+ end
148
+
149
+ # Returns the permission set associated with this site. This returns the permission set of
150
+ # the containing site if it does not have a permission set of its own
151
+ # @return [PermissionSet]
152
+ def permission_set
153
+ if attributes["InheritedSecurity"]
154
+ supersite.permission_set
155
+ else
156
+ PermissionSet.new(self)
157
+ end
158
+ end
159
+ cache :permission_set
160
+
161
+ # Returns the list of fields for this site. This includes fields inherited from containing sites
162
+ # @return [Array<Field>]
163
+ def fields
164
+ call("Webs", "get_columns").xpath("//sp:Field", NS).map do |field|
165
+ attributes = clean_attributes(field.attributes)
166
+ supersite && supersite.field(attributes["ID"].downcase) || Field.new(self, attributes["ID"].downcase, attributes["StaticName"], attributes["Type"], nil, attributes) if attributes["ID"] && attributes["StaticName"]
167
+ end.compact
168
+ end
169
+ cache :fields, :dup => :always
170
+
171
+ # Returns the result of {Site#fields} hashed by name
172
+ # @return [Hash{String => Field}]
173
+ def fields_by_name
174
+ fields.inject({}) { |h, f| h[f.attributes["StaticName"]] = f ; h }
175
+ end
176
+ cache :fields_by_name, :dup => :always
177
+
178
+ # @private
179
+ def field(id)
180
+ fields.find { |f| f.ID == id }
181
+ end
182
+
183
+ # See {Base#save}
184
+ # @return [void]
185
+ def save
186
+ p untype_cast_attributes(self, nil, internal_attribute_types, changed_attributes)
187
+ end
188
+
189
+ def accessible?
190
+ data
191
+ true
192
+ rescue Savon::HTTPError
193
+ false
194
+ end
195
+
196
+ # @private
197
+ def to_s
198
+ "#<ActiveSP::Site url=#{@url}>"
199
+ end
200
+
201
+ # @private
202
+ alias inspect to_s
203
+
204
+ private
205
+
206
+ def call(service, m, *args, &blk)
207
+ result = service(service).call(m, *args, &blk)
208
+ Nokogiri::XML.parse(result.http.body)
209
+ end
210
+
211
+ def fetch(url)
212
+ @connection.fetch(url)
213
+ end
214
+
215
+ def service(name)
216
+ @services[name] ||= Service.new(self, name)
217
+ end
218
+
219
+ def data
220
+ # Looks like you can't call this as a non-admin. To investigate further
221
+ call("SiteData", "get_web")
222
+ rescue Savon::HTTPError
223
+ # This can fail when you don't have access to this site
224
+ call("Webs", "get_web", "webUrl" => ".")
225
+ end
226
+ cache :data
227
+
228
+ def attributes_before_type_cast
229
+ if element = data.xpath("//sp:sWebMetadata", NS).first
230
+ result = {}
231
+ element.children.each do |ch|
232
+ result[ch.name] = ch.inner_text
233
+ end
234
+ result
235
+ else
236
+ element = data.xpath("//sp:Web", NS).first
237
+ clean_attributes(element.attributes)
238
+ end
239
+ end
240
+ cache :attributes_before_type_cast
241
+
242
+ def original_attributes
243
+ type_cast_attributes(self, nil, internal_attribute_types, attributes_before_type_cast)
244
+ end
245
+ cache :original_attributes
246
+
247
+ def internal_attribute_types
248
+ @@internal_attribute_types ||= {
249
+ "AllowAnonymousAccess" => GhostField.new("AllowAnonymousAccess", "Bool", false, true),
250
+ "AnonymousViewListItems" => GhostField.new("AnonymousViewListItems", "Bool", false, true),
251
+ "Author" => GhostField.new("Author", "InternalUser", false, true),
252
+ "Description" => GhostField.new("Description", "Text", false, true),
253
+ "ExternalSecurity" => GhostField.new("ExternalSecurity", "Bool", false, true),
254
+ "InheritedSecurity" => GhostField.new("InheritedSecurity", "Bool", false, true),
255
+ "IsBucketWeb" => GhostField.new("IsBucketWeb", "Bool", false, true),
256
+ "Language" => GhostField.new("Language", "Integer", false, true),
257
+ "LastModified" => GhostField.new("LastModified", "XMLDateTime", false, true),
258
+ "LastModifiedForceRecrawl" => GhostField.new("LastModifiedForceRecrawl", "XMLDateTime", false, true),
259
+ "Permissions" => GhostField.new("Permissions", "Text", false, true),
260
+ "Title" => GhostField.new("Title", "Text", false, true),
261
+ "UsedInAutocat" => GhostField.new("UsedInAutocat", "Bool", false, true),
262
+ "ValidSecurityInfo" => GhostField.new("ValidSecurityInfo", "Bool", false, true),
263
+ "WebID" => GhostField.new("WebID", "Text", false, true)
264
+ }
265
+ end
266
+
267
+ def permissions
268
+ result = call("Permissions", "get_permission_collection", "objectName" => ::File.basename(@url), "objectType" => "Web")
269
+ result.xpath("//spdir:Permission", NS).map do |row|
270
+ accessor = row["MemberIsUser"][/true/i] ? User.new(rootsite, row["UserLogin"]) : Group.new(rootsite, row["GroupName"])
271
+ { :mask => Integer(row["Mask"]), :accessor => accessor }
272
+ end
273
+ end
274
+ cache :permissions, :dup => :always
275
+
276
+ # @private
277
+ class Service
278
+
279
+ def initialize(site, name)
280
+ @site, @name = site, name
281
+ @client = Savon::Client.new(::File.join(URI.escape(site.url), "_vti_bin", name + ".asmx?WSDL"))
282
+ @client.request.ntlm_auth(site.connection.login, site.connection.password) if site.connection.login
283
+ end
284
+
285
+ def call(m, *args)
286
+ t1 = Time.now
287
+ if Hash === args[-1]
288
+ body = args.pop
289
+ end
290
+ @client.send(m, *args) do |soap|
291
+ if body
292
+ soap.body = body.inject({}) { |h, (k, v)| h["wsdl:#{k}"] = v ; h }
293
+ end
294
+ yield soap if block_given?
295
+ end
296
+ ensure
297
+ t2 = Time.now
298
+ puts "SP - time: %.3fs, site: %s, service: %s, method: %s, body: %s" % [t2 - t1, @site.url, @name, m, body.inspect] if @site.connection.trace
299
+ end
300
+
301
+ end
302
+
303
+ end
304
+
305
+ end
@@ -0,0 +1,146 @@
1
+ # Copyright (c) 2010 XAOP bvba
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # 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
+ #
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ #
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ # @private
27
+ def URL(*args)
28
+ case args.length
29
+ when 1
30
+ url = args[0]
31
+ if URL === url
32
+ url
33
+ else
34
+ URL.parse(url)
35
+ end
36
+ when 2..6
37
+ URL.new(*args)
38
+ else
39
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1..6)"
40
+ end
41
+ end
42
+
43
+ # @private
44
+ class URL < Struct.new(:protocol, :host, :port, :path, :query, :fragment)
45
+
46
+ def self.parse(url)
47
+ if /^(?:([^:\/?#]+):)?(?:\/\/([^\/?#:]*)(?::(\d+))?)?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/ === url.strip
48
+ new($1 ? $1.downcase : nil, $2 ? $2.downcase : nil, $3 ? $3.to_i : nil, $4, $5, $6)
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ def to_s
55
+ "%s://%s%s" % [protocol, authority, full_path]
56
+ end
57
+
58
+ def authority
59
+ "%s%s" % [host, (!port || port == (protocol == "http" ? 80 : 443)) ? "" : ":#{port}"]
60
+ end
61
+
62
+ def full_path
63
+ result = path.dup
64
+ result << "?" << query if query
65
+ result << "#" << fragment if fragment
66
+ result
67
+ end
68
+
69
+ def join(url)
70
+ url = URL(url)
71
+ if url
72
+ if url.protocol == protocol
73
+ url.protocol = nil
74
+ end
75
+ unless url.protocol
76
+ url.protocol = protocol
77
+ unless url.host
78
+ url.host = host
79
+ url.port = port
80
+ if url.path.empty?
81
+ url.path = path
82
+ unless url.query
83
+ url.query = query
84
+ end
85
+ else
86
+ url.path = join_url_paths(url.path, path)
87
+ end
88
+ end
89
+ end
90
+ url.complete
91
+ else
92
+ nil
93
+ end
94
+ end
95
+
96
+ def complete
97
+ self.protocol ||= "http"
98
+ self.port ||= self.protocol == "http" ? 80 : 443
99
+ self.path = "/" if self.path.empty?
100
+ self
101
+ end
102
+
103
+ def self.unescape(s)
104
+ s.gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
105
+ [$1.delete('%')].pack('H*')
106
+ end
107
+ end
108
+
109
+ def self.escape(s)
110
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) do
111
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
112
+ end
113
+ end
114
+
115
+ def self.parse_query(qs, d = '&;')
116
+ params = {}
117
+ (qs || '').split(/[&;] */n).inject(params) do |h, p|
118
+ k, v = unescape(p).split('=', 2)
119
+ if cur = params[k]
120
+ if Array === cur
121
+ params[k] << v
122
+ else
123
+ params[k] = [cur, v]
124
+ end
125
+ else
126
+ params[k] = v
127
+ end
128
+ end
129
+ params
130
+ end
131
+
132
+ def self.construct_query(hash)
133
+ hash.map { |k, v| "%s=%s" % [k, escape(v)] }.join('&')
134
+ end
135
+
136
+ private
137
+
138
+ def join_url_paths(url, base)
139
+ if url[0] == ?/
140
+ url
141
+ else
142
+ base[0..base.rindex("/")] + url
143
+ end
144
+ end
145
+
146
+ end