From 884881ad81a59973f9f63841babe77494ddc7d6b Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Mon, 8 Nov 2010 12:21:28 -0500 Subject: [PATCH 001/130] Support cache clients that return boolean values like Dalli --- lib/openid/store/memcache.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/openid/store/memcache.rb b/lib/openid/store/memcache.rb index bb4b106a..9cc065c0 100644 --- a/lib/openid/store/memcache.rb +++ b/lib/openid/store/memcache.rb @@ -63,7 +63,11 @@ def use_nonce(server_url, timestamp, salt) ts = timestamp.to_s # base 10 seconds since epoch nonce_key = key_prefix + 'N' + server_url + '|' + ts + '|' + salt result = @cache_client.add(nonce_key, '', expiry(Nonce.skew + 5)) - return !!(result =~ /^STORED/) + if result.is_a? String + return !!(result =~ /^STORED/) + else + return result == true + end end def assoc_key(server_url, assoc_handle=nil) @@ -87,7 +91,11 @@ def cleanup_associations def delete(key) result = @cache_client.delete(key) - return !!(result =~ /^DELETED/) + if result.is_a? String + return !!(result =~ /^DELETED/) + else + return result == true + end end def serialize(assoc) From c4be2ad25792017bcf070296886a6fc21f4bcdf1 Mon Sep 17 00:00:00 2001 From: Steven Davidovitz Date: Tue, 22 Mar 2011 11:09:16 -0400 Subject: [PATCH 002/130] encode form inputs so that values with elements such as & or ' are still properly sent --- lib/openid/message.rb | 2 +- lib/openid/util.rb | 6 ++++++ test/test_message.rb | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/openid/message.rb b/lib/openid/message.rb index 8700378b..c494f469 100644 --- a/lib/openid/message.rb +++ b/lib/openid/message.rb @@ -288,7 +288,7 @@ def to_form_markup(action_url, form_tag_attrs=nil, submit_text='Continue') markup += ">\n" to_post_args.each { |k,v| - markup += "\n" + markup += "\n" } markup += "\n" markup += "\n" diff --git a/lib/openid/util.rb b/lib/openid/util.rb index c5a6716b..5d6e78c6 100644 --- a/lib/openid/util.rb +++ b/lib/openid/util.rb @@ -105,6 +105,12 @@ def Util.auto_submit_html(form, title='OpenID transaction in progress') " end + + ESCAPE_TABLE = { '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' } + # Modified from ERb's html_encode + def Util.html_encode(s) + s.to_s.gsub(/[&<>"']/) {|s| ESCAPE_TABLE[s] } + end end end diff --git a/test/test_message.rb b/test/test_message.rb index f8ef9187..6ae1d8fd 100644 --- a/test/test_message.rb +++ b/test/test_message.rb @@ -902,6 +902,7 @@ def setup 'openid.identity' => 'http://bogus.example.invalid:port/', 'openid.assoc_handle' => 'FLUB', 'openid.return_to' => 'Neverland', + 'openid.ax.value.fullname' => "Bob&Smith'" } @action_url = 'scheme://host:port/path?query' From aab900758e3d5aace7a2ece70824fbdc0968332d Mon Sep 17 00:00:00 2001 From: Doug Puchalski Date: Sat, 23 Apr 2011 11:04:46 -0700 Subject: [PATCH 003/130] Rename gemspec to ruby-openid.gemspec --- gemspec => ruby-openid.gemspec | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gemspec => ruby-openid.gemspec (100%) diff --git a/gemspec b/ruby-openid.gemspec similarity index 100% rename from gemspec rename to ruby-openid.gemspec From 4b352f89cf4d785a1165ef9082e8a0e69306dae6 Mon Sep 17 00:00:00 2001 From: Marty Zalega Date: Sun, 24 Jul 2011 11:01:17 +1000 Subject: [PATCH 004/130] bug in 1.9.2 interpreter causes crash when using 'zip' on enumerables of bytes, this is a work around until ruby is fixed --- lib/openid/dh.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/dh.rb b/lib/openid/dh.rb index cbe53114..0c15b867 100644 --- a/lib/openid/dh.rb +++ b/lib/openid/dh.rb @@ -57,7 +57,7 @@ def DiffieHellman.strxor(s, t) end if String.method_defined? :bytes - s.bytes.zip(t.bytes).map{|sb,tb| sb^tb}.pack('C*') + s.bytes.to_a.zip(t.bytes.to_a).map{|sb,tb| sb^tb}.pack('C*') else indices = 0...(s.length) chrs = indices.collect {|i| (s[i]^t[i]).chr} From 4e3f81b77403b68d95c7b26cac6e4d105c449c34 Mon Sep 17 00:00:00 2001 From: Narihiro Nakamura Date: Fri, 26 Aug 2011 06:57:57 +0900 Subject: [PATCH 005/130] String#[] is obsolete since Ruby 1.9. --- lib/openid/store/filesystem.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/store/filesystem.rb b/lib/openid/store/filesystem.rb index e2993eea..6a555ade 100644 --- a/lib/openid/store/filesystem.rb +++ b/lib/openid/store/filesystem.rb @@ -236,7 +236,7 @@ def filename_escape(s) if @@FILENAME_ALLOWED.index(c) filename_chunks << c else - filename_chunks << sprintf("_%02X", c[0]) + filename_chunks << sprintf("_%02X", c.bytes.first) end end filename_chunks.join("") From 8250493379acbb3ed67eaef6287de0e29637df6b Mon Sep 17 00:00:00 2001 From: mcary Date: Tue, 22 Nov 2011 19:32:46 -0800 Subject: [PATCH 006/130] Fix cleanup AR associations whose expiry is past, not upcoming --- examples/active_record_openid_store/lib/openid_ar_store.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/active_record_openid_store/lib/openid_ar_store.rb b/examples/active_record_openid_store/lib/openid_ar_store.rb index 276569c5..c2436744 100644 --- a/examples/active_record_openid_store/lib/openid_ar_store.rb +++ b/examples/active_record_openid_store/lib/openid_ar_store.rb @@ -51,7 +51,7 @@ def cleanup_nonces def cleanup_associations now = Time.now.to_i - Association.delete_all(['issued + lifetime > ?',now]) + Association.delete_all(['issued + lifetime < ?',now]) end end From f2df9431a7e4ac79a7eca725ecd7fe152939d256 Mon Sep 17 00:00:00 2001 From: mcary Date: Tue, 22 Nov 2011 19:39:14 -0800 Subject: [PATCH 007/130] Fix AR store class name in gc rake task and explicitly require it --- examples/active_record_openid_store/README | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/active_record_openid_store/README b/examples/active_record_openid_store/README index 11787298..ed1f1f57 100644 --- a/examples/active_record_openid_store/README +++ b/examples/active_record_openid_store/README @@ -46,7 +46,8 @@ task in your app's Rakefile like so: desc 'GC OpenID store' task :gc_openid_store => :environment do - ActiveRecordOpenIDStore.new.cleanup + require 'openid_ar_store' + ActiveRecordStore.new.cleanup end Run it by typing: From dc26a4c57a0462e738498bfbe34a13f0a5ba447c Mon Sep 17 00:00:00 2001 From: mcary Date: Tue, 22 Nov 2011 19:42:06 -0800 Subject: [PATCH 008/130] Improve desc and user feedback in AR gc rake task --- examples/active_record_openid_store/README | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/active_record_openid_store/README b/examples/active_record_openid_store/README index ed1f1f57..9d15c62f 100644 --- a/examples/active_record_openid_store/README +++ b/examples/active_record_openid_store/README @@ -44,10 +44,11 @@ You may garbage collect unused nonces and expired associations using the gc instance method of ActiveRecordOpenIDStore. Hook it up to a task in your app's Rakefile like so: - desc 'GC OpenID store' + desc 'GC OpenID store, deleting expired nonces and associations' task :gc_openid_store => :environment do require 'openid_ar_store' - ActiveRecordStore.new.cleanup + nonces, associations = ActiveRecordStore.new.cleanup + puts "Deleted #{nonces} nonces, #{associations} associations" end Run it by typing: From 037885bc14144c38816bbbc1ca4295e24ddb3796 Mon Sep 17 00:00:00 2001 From: Cal Heldenbrand Date: Fri, 20 Apr 2012 15:01:24 -0500 Subject: [PATCH 009/130] Adding support to manually define NS aliases in the AX FetchResponse. Also added a short-hand response for attributes containing only one value. Very similar to Google's AX responses -- openid.ax.value.email=guy@example.com --- lib/openid/extensions/ax.rb | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/openid/extensions/ax.rb b/lib/openid/extensions/ax.rb index 4c8e899e..62d9ee09 100644 --- a/lib/openid/extensions/ax.rb +++ b/lib/openid/extensions/ax.rb @@ -296,12 +296,21 @@ def _get_extension_kv_args(aliases = nil) @data.each{|type_uri, values| name = aliases.add(type_uri) ax_args['type.'+name] = type_uri - ax_args['count.'+name] = values.size.to_s + if values.size > 1 + ax_args['count.'+name] = values.size.to_s - values.each_with_index{|value, i| - key = "value.#{name}.#{i+1}" - ax_args[key] = value - } + values.each_with_index{|value, i| + key = "value.#{name}.#{i+1}" + ax_args[key] = value + } + # for attributes with only a single value, use a + # nice shortcut to only show the value w/o the count + else + values.each do |value| + key = "value.#{name}" + ax_args[key] = value + end + end } return ax_args end @@ -381,11 +390,17 @@ def count(type_uri) # A fetch_response attribute exchange message class FetchResponse < KeyValueMessage attr_reader :update_url + # Use the aliases variable to manually add alias names in the response. + # They'll be returned to the client in the format: + # openid.ax.type.email=http://openid.net/schema/contact/internet/email + # openid.ax.value.email=guy@example.com + attr_accessor :aliases def initialize(update_url = nil) super() @mode = 'fetch_response' @update_url = update_url + @aliases = NamespaceMap.new end # Serialize this object into arguments in the attribute @@ -394,7 +409,6 @@ def initialize(update_url = nil) # validated against this request, and empty responses for requested # fields with no data will be sent. def get_extension_args(request = nil) - aliases = NamespaceMap.new zero_value_types = [] if request @@ -412,9 +426,9 @@ def get_extension_args(request = nil) # Copy the aliases from the request so that reading # the response in light of the request is easier if attr_info.ns_alias.nil? - aliases.add(attr_info.type_uri) + @aliases.add(attr_info.type_uri) else - aliases.add_alias(attr_info.type_uri, attr_info.ns_alias) + @aliases.add_alias(attr_info.type_uri, attr_info.ns_alias) end values = @data[attr_info.type_uri] if values.empty? # @data defaults to [] @@ -426,14 +440,14 @@ def get_extension_args(request = nil) } end - kv_args = _get_extension_kv_args(aliases) + kv_args = _get_extension_kv_args(@aliases) # Add the KV args into the response with the args that are # unique to the fetch_response ax_args = new_args zero_value_types.each{|attr_info| - name = aliases.get_alias(attr_info.type_uri) + name = @aliases.get_alias(attr_info.type_uri) kv_args['type.' + name] = attr_info.type_uri kv_args['count.' + name] = '0' } From 866204be4ba1da96ee70c697679901bb60cc1bc3 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 21 Jun 2012 23:19:30 +0200 Subject: [PATCH 010/130] Fixed test suite. Closes #30 and #34 --- Rakefile | 12 +++++++ lib/openid/trustroot.rb | 1 + test/data/trustroot.txt | 4 +-- test/test_accept.rb | 2 +- test/test_associationmanager.rb | 2 +- test/test_ax.rb | 27 ++++++++-------- test/test_checkid_request.rb | 2 +- test/test_consumer.rb | 2 +- test/test_cryptutil.rb | 2 +- test/test_dh.rb | 2 +- test/test_discover.rb | 6 ++-- test/test_discovery_manager.rb | 4 +-- test/test_extension.rb | 4 +-- test/test_fetchers.rb | 10 ++---- test/test_filters.rb | 56 +++++++++++++++------------------ test/test_idres.rb | 34 ++++++++++---------- test/test_kvpost.rb | 4 +-- test/test_message.rb | 2 -- test/test_oauth.rb | 5 +-- test/test_openid_yadis.rb | 1 - test/test_pape.rb | 13 ++++---- test/test_parsehtml.rb | 4 +-- test/test_server.rb | 39 ++++++++++------------- test/test_sreg.rb | 16 +++++----- test/test_stores.rb | 3 +- test/test_trustroot.rb | 29 +++++++++-------- test/test_urinorm.rb | 5 ++- test/test_util.rb | 5 ++- test/test_xrds.rb | 28 ++++++++--------- test/test_xrires.rb | 41 +++++++++++++++--------- test/test_yadis_discovery.rb | 4 +-- 31 files changed, 184 insertions(+), 185 deletions(-) create mode 100644 Rakefile diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..0f6ca552 --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +#!/usr/bin/env rake +require 'rake/testtask' + +desc "Run tests" +Rake::TestTask.new('test') do |t| + t.libs << 'lib' + t.libs << 'test' + t.test_files = FileList["test/**/test_*.rb"] + t.verbose = false +end + +task :default => :test diff --git a/lib/openid/trustroot.rb b/lib/openid/trustroot.rb index 16695f05..d6bcc704 100644 --- a/lib/openid/trustroot.rb +++ b/lib/openid/trustroot.rb @@ -210,6 +210,7 @@ def TrustRoot.parse(trust_root) return nil if parts.nil? proto, host, port, path = parts + return nil if host[0] == '.' # check for URI fragment if path and !path.index('#').nil? diff --git a/test/data/trustroot.txt b/test/data/trustroot.txt index 73681657..f58650fb 100644 --- a/test/data/trustroot.txt +++ b/test/data/trustroot.txt @@ -25,8 +25,8 @@ http://foo.com\/ http://π.pi.com/ http://lambda.com/Λ - - + + 5 ---------------------------------------- diff --git a/test/test_accept.rb b/test/test_accept.rb index 06db85bf..a746cad3 100644 --- a/test/test_accept.rb +++ b/test/test_accept.rb @@ -1,5 +1,5 @@ - require 'test/unit' +require 'testutil' require 'openid/yadis/accept' require 'openid/extras' require 'openid/util' diff --git a/test/test_associationmanager.rb b/test/test_associationmanager.rb index 041449ff..2d751976 100644 --- a/test/test_associationmanager.rb +++ b/test/test_associationmanager.rb @@ -1,3 +1,4 @@ +require "test/unit" require "openid/consumer/associationmanager" require "openid/association" require "openid/dh" @@ -5,7 +6,6 @@ require "openid/cryptutil" require "openid/message" require "openid/store/memory" -require "test/unit" require "util" require "time" diff --git a/test/test_ax.rb b/test/test_ax.rb index 2d17c1f5..2daaec66 100644 --- a/test/test_ax.rb +++ b/test/test_ax.rb @@ -1,3 +1,4 @@ +require 'test/unit' require 'openid/extensions/ax' require 'openid/message' require 'openid/consumer/responses' @@ -119,7 +120,7 @@ def test_count_present_but_not_value def test_invalid_count_value msg = FetchRequest.new assert_raises(Error) { - msg.parse_extension_args({'type.foo'=>'urn:foo', + msg.parse_extension_args({'type.foo'=>'urn:foo', 'count.foo' => 'bogus'}) } end @@ -216,7 +217,7 @@ def singleton_value 'value.foo'=>'something', }, {'urn:foo'=>['something']} - ) + ) end end @@ -371,12 +372,12 @@ def test_from_openid_request_no_ax ax_req = FetchRequest.from_openid_request(openid_req) assert(ax_req.nil?) end - + def test_from_openid_request_wrong_ax_mode uri = 'http://under.the.sea/' name = 'ext0' value = 'snarfblat' - + message = OpenID::Message.from_openid_args({ 'mode' => 'id_res', 'ns' => OPENID2_NS, @@ -392,7 +393,7 @@ def test_from_openid_request_wrong_ax_mode ax_req = FetchRequest.from_openid_request(openid_req) assert(ax_req.nil?) end - + def test_openid_update_url_verification_error openid_req_msg = Message.from_openid_args({ 'mode' => 'checkid_setup', @@ -404,7 +405,7 @@ def test_openid_update_url_verification_error }) openid_req = Server::OpenIDRequest.new openid_req.message = openid_req_msg - assert_raises(Error) { + assert_raises(Error) { FetchRequest.from_openid_request(openid_req) } end @@ -419,7 +420,7 @@ def test_openid_no_realm }) openid_req = Server::OpenIDRequest.new openid_req.message = openid_req_msg - assert_raises(Error) { + assert_raises(Error) { FetchRequest.from_openid_request(openid_req) } end @@ -623,12 +624,12 @@ def test_get_extension_args_empty } assert_equal(eargs, @msg.get_extension_args) end - + def test_from_openid_request_wrong_ax_mode uri = 'http://under.the.sea/' name = 'ext0' value = 'snarfblat' - + message = OpenID::Message.from_openid_args({ 'mode' => 'id_res', 'ns' => OPENID2_NS, @@ -644,7 +645,7 @@ def test_from_openid_request_wrong_ax_mode ax_req = StoreRequest.from_openid_request(openid_req) assert(ax_req.nil?) end - + def test_get_extension_args_nonempty @msg.set_values(@type_a, ['foo','bar']) aliases = NamespaceMap.new @@ -665,7 +666,7 @@ def test_success msg = StoreResponse.new assert(msg.succeeded?) assert(!msg.error_message) - assert_equal({'mode' => 'store_response_success'}, + assert_equal({'mode' => 'store_response_success'}, msg.get_extension_args) end @@ -673,7 +674,7 @@ def test_fail_nomsg msg = StoreResponse.new(false) assert(! msg.succeeded? ) assert(! msg.error_message ) - assert_equal({'mode' => 'store_response_failure'}, + assert_equal({'mode' => 'store_response_failure'}, msg.get_extension_args) end @@ -682,7 +683,7 @@ def test_fail_msg msg = StoreResponse.new(false, reason) assert(! msg.succeeded? ) assert_equal(reason, msg.error_message) - assert_equal({'mode' => 'store_response_failure', 'error' => reason}, + assert_equal({'mode' => 'store_response_failure', 'error' => reason}, msg.get_extension_args) end end diff --git a/test/test_checkid_request.rb b/test/test_checkid_request.rb index e1d95c91..1f5e8f3e 100644 --- a/test/test_checkid_request.rb +++ b/test/test_checkid_request.rb @@ -1,6 +1,6 @@ +require "test/unit" require "openid/consumer/checkid_request" require "openid/message" -require "test/unit" require "testutil" require "util" diff --git a/test/test_consumer.rb b/test/test_consumer.rb index dc4a09e6..f6a85a30 100644 --- a/test/test_consumer.rb +++ b/test/test_consumer.rb @@ -1,6 +1,6 @@ -require "openid/consumer" require "test/unit" require "testutil" +require "openid/consumer" module OpenID class Consumer diff --git a/test/test_cryptutil.rb b/test/test_cryptutil.rb index f6a38a0d..6a4455ef 100644 --- a/test/test_cryptutil.rb +++ b/test/test_cryptutil.rb @@ -1,5 +1,5 @@ # coding: ASCII-8BIT -require 'test/unit' +require "test/unit" require "openid/cryptutil" require "pathname" diff --git a/test/test_dh.rb b/test/test_dh.rb index 397c15e5..e0002360 100644 --- a/test/test_dh.rb +++ b/test/test_dh.rb @@ -1,6 +1,6 @@ require 'test/unit' -require 'openid/dh' require 'testutil' +require 'openid/dh' module OpenID class DiffieHellmanExposed < OpenID::DiffieHellman diff --git a/test/test_discover.rb b/test/test_discover.rb index 71511c3e..155718a5 100644 --- a/test/test_discover.rb +++ b/test/test_discover.rb @@ -1,8 +1,6 @@ - +require 'test/unit' require 'testutil' require 'util' - -require 'test/unit' require 'openid/fetchers' require 'openid/yadis/discovery' require 'openid/consumer/discovery' @@ -48,7 +46,7 @@ def test_discovery_failure @responses.each { |response_set| @url = response_set[0].final_url OpenID.fetcher = SimpleMockFetcher.new(self, response_set) - + expected_status = response_set[-1].code begin OpenID.discover(@url) diff --git a/test/test_discovery_manager.rb b/test/test_discovery_manager.rb index da966f38..3d91ebd2 100644 --- a/test/test_discovery_manager.rb +++ b/test/test_discovery_manager.rb @@ -1,8 +1,6 @@ - require 'test/unit' require 'openid/consumer/discovery_manager' require 'openid/extras' - require 'testutil' module OpenID @@ -41,7 +39,7 @@ def test_started @disco_services.next assert(@disco_services.started?) @disco_services.next - assert(@disco_services.started?) + assert(@disco_services.started?) @disco_services.next assert(!@disco_services.started?) end diff --git a/test/test_extension.rb b/test/test_extension.rb index bfe14e9b..13c05b16 100644 --- a/test/test_extension.rb +++ b/test/test_extension.rb @@ -1,6 +1,6 @@ +require 'test/unit' require 'openid/extension' require 'openid/message' -require 'test/unit' module OpenID class DummyExtension < OpenID::Extension @@ -29,7 +29,7 @@ def test_OpenID1 assert_equal(DummyExtension::TEST_ALIAS, namespaces.get_alias(DummyExtension::TEST_URI)) end - + def test_OpenID2 oid2_msg = Message.new(OPENID2_NS) ext = DummyExtension.new diff --git a/test/test_fetchers.rb b/test/test_fetchers.rb index 6b033314..2e4554f8 100644 --- a/test/test_fetchers.rb +++ b/test/test_fetchers.rb @@ -1,14 +1,10 @@ -# -*- coding: utf-8 -*- - +# encoding: utf-8 require 'test/unit' require 'net/http' require 'webrick' - require 'testutil' require 'util' - require 'openid/fetchers' - require 'stringio' begin @@ -412,7 +408,7 @@ def test_fetchingerror f.fetch("https://github.com/service/https://bogus.com/") } end - + class TestingException < OpenID::FetchingError; end class NoSSLSupportConnection @@ -543,7 +539,7 @@ def test_proxy_unreachable def test_proxy_env ENV['http_proxy'] = 'http://127.0.0.1:3128/' OpenID.fetcher_use_env_http_proxy - + # make_http just to give us something with readable attributes to inspect. conn = OpenID.fetcher.make_http(URI.parse('http://127.0.0.2/')) assert_equal('127.0.0.1', conn.proxy_address) diff --git a/test/test_filters.rb b/test/test_filters.rb index 990201f0..079009e7 100644 --- a/test/test_filters.rb +++ b/test/test_filters.rb @@ -1,6 +1,8 @@ - -require 'test/unit' -require 'openid/yadis/filters' +require "test/unit" +require "testutil" +require "rexml/document" +require "openid/yadis/xrds" +require "openid/yadis/filters" module OpenID class BasicServiceEndpointTest < Test::Unit::TestCase @@ -12,8 +14,7 @@ def test_match_types no_types_endpoint = Yadis::BasicServiceEndpoint.new(yadis_url, [], nil, nil) - some_types_endpoint = Yadis::BasicServiceEndpoint.new(yadis_url, types, - nil, nil) + some_types_endpoint = Yadis::BasicServiceEndpoint.new(yadis_url, types, nil, nil) assert(no_types_endpoint.match_types([]) == []) assert(no_types_endpoint.match_types(["urn:absent"]) == []) @@ -158,15 +159,14 @@ def test_get_service_endpoints ] cf = Yadis::CompoundFilter.new(subfilters) - assert(cf.get_service_endpoints("unused", "unused") == all) + assert cf.get_service_endpoints("unused", "unused") == all end end class MakeFilterTest < Test::Unit::TestCase def test_parts_nil result = Yadis.make_filter(nil) - assert(result.is_a?(Yadis::TransformFilterMaker), - result) + assert result.is_a?(Yadis::TransformFilterMaker) end def test_parts_array @@ -174,53 +174,49 @@ def test_parts_array e2 = Yadis::BasicServiceEndpoint.new(nil, [], nil, nil) result = Yadis.make_filter([e1, e2]) - assert(result.is_a?(Yadis::TransformFilterMaker), - result) - assert(result.filter_procs[0] == e1.method('from_basic_service_endpoint')) - assert(result.filter_procs[1] == e2.method('from_basic_service_endpoint')) + assert result.is_a?(Yadis::TransformFilterMaker) + assert result.filter_procs[0] == e1.method('from_basic_service_endpoint') + assert result.filter_procs[1] == e2.method('from_basic_service_endpoint') end def test_parts_single e = Yadis::BasicServiceEndpoint.new(nil, [], nil, nil) result = Yadis.make_filter(e) - assert(result.is_a?(Yadis::TransformFilterMaker), - result) + assert result.is_a?(Yadis::TransformFilterMaker) end end class MakeCompoundFilterTest < Test::Unit::TestCase def test_no_filters result = Yadis.mk_compound_filter([]) - assert(result.subfilters == []) + assert result.subfilters == [] end def test_single_transform_filter f = Yadis::TransformFilterMaker.new([]) - assert(Yadis.mk_compound_filter([f]) == f) + assert_equal f, Yadis.mk_compound_filter([f]) end def test_single_endpoint e = Yadis::BasicServiceEndpoint.new(nil, [], nil, nil) result = Yadis.mk_compound_filter([e]) - assert(result.is_a?(Yadis::TransformFilterMaker)) + assert result.is_a?(Yadis::TransformFilterMaker) # Expect the transform filter to call # from_basic_service_endpoint on the endpoint filter = result.filter_procs[0] - assert(filter == e.method('from_basic_service_endpoint'), - filter) + assert_equal filter, e.method('from_basic_service_endpoint') end def test_single_proc # Create a proc that just returns nil for any endpoint p = Proc.new { |endpoint| nil } result = Yadis.mk_compound_filter([p]) - assert(result.is_a?(Yadis::TransformFilterMaker)) + assert result.is_a?(Yadis::TransformFilterMaker) # Expect the transform filter to call # from_basic_service_endpoint on the endpoint - filter = result.filter_procs[0] - assert(filter == p) + assert_equal result.filter_procs[0], p end def test_multiple_filters_same_type @@ -231,8 +227,8 @@ def test_multiple_filters_same_type # from f1 and f2. result = Yadis.mk_compound_filter([f1, f2]) - assert(result.is_a?(Yadis::CompoundFilter)) - assert(result.subfilters == [f1, f2]) + assert result.is_a?(Yadis::CompoundFilter) + assert result.subfilters == [f1, f2] end def test_multiple_filters_different_type @@ -247,14 +243,12 @@ def test_multiple_filters_different_type # from f1 and f2. result = Yadis.mk_compound_filter([f1, f2, f3, f4]) - assert(result.is_a?(Yadis::CompoundFilter)) + assert result.is_a?(Yadis::CompoundFilter) - assert(result.subfilters[0] == f1) - assert(result.subfilters[1].filter_procs[0] == - e.method('from_basic_service_endpoint')) - assert(result.subfilters[2].filter_procs[0] == - f2.method('from_basic_service_endpoint')) - assert(result.subfilters[2].filter_procs[1] == f3) + assert result.subfilters[0] == f1 + assert result.subfilters[1].filter_procs[0] == e.method('from_basic_service_endpoint') + assert result.subfilters[2].filter_procs[0] == f2.method('from_basic_service_endpoint') + assert result.subfilters[2].filter_procs[1] == f3 end def test_filter_type_error diff --git a/test/test_idres.rb b/test/test_idres.rb index 9d7364f4..7940bf1e 100644 --- a/test/test_idres.rb +++ b/test/test_idres.rb @@ -1,6 +1,6 @@ +require "test/unit" require "testutil" require "util" -require "test/unit" require "openid/consumer/idres" require "openid/protocolerror" require "openid/store/memory" @@ -103,21 +103,21 @@ def mkMsg(ns, fields, signed_fields) end def test_112 - args = {'openid.assoc_handle' => 'fa1f5ff0-cde4-11dc-a183-3714bfd55ca8', - 'openid.claimed_id' => 'http://binkley.lan/user/test01', - 'openid.identity' => 'http://test01.binkley.lan/', - 'openid.mode' => 'id_res', - 'openid.ns' => 'http://specs.openid.net/auth/2.0', - 'openid.ns.pape' => 'http://specs.openid.net/extensions/pape/1.0', - 'openid.op_endpoint' => 'http://binkley.lan/server', - 'openid.pape.auth_policies' => 'none', - 'openid.pape.auth_time' => '2008-01-28T20:42:36Z', - 'openid.pape.nist_auth_level' => '0', - 'openid.response_nonce' => '2008-01-28T21:07:04Z99Q=', - 'openid.return_to' => 'http://binkley.lan:8001/process?janrain_nonce=2008-01-28T21%3A07%3A02Z0tMIKx', - 'openid.sig' => 'YJlWH4U6SroB1HoPkmEKx9AyGGg=', - 'openid.signed' => 'assoc_handle,identity,response_nonce,return_to,claimed_id,op_endpoint,pape.auth_time,ns.pape,pape.nist_auth_level,pape.auth_policies' - } + args = {'openid.assoc_handle' => 'fa1f5ff0-cde4-11dc-a183-3714bfd55ca8', + 'openid.claimed_id' => 'http://binkley.lan/user/test01', + 'openid.identity' => 'http://test01.binkley.lan/', + 'openid.mode' => 'id_res', + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.ns.pape' => 'http://specs.openid.net/extensions/pape/1.0', + 'openid.op_endpoint' => 'http://binkley.lan/server', + 'openid.pape.auth_policies' => 'none', + 'openid.pape.auth_time' => '2008-01-28T20:42:36Z', + 'openid.pape.nist_auth_level' => '0', + 'openid.response_nonce' => '2008-01-28T21:07:04Z99Q=', + 'openid.return_to' => 'http://binkley.lan:8001/process?janrain_nonce=2008-01-28T21%3A07%3A02Z0tMIKx', + 'openid.sig' => 'YJlWH4U6SroB1HoPkmEKx9AyGGg=', + 'openid.signed' => 'assoc_handle,identity,response_nonce,return_to,claimed_id,op_endpoint,pape.auth_time,ns.pape,pape.nist_auth_level,pape.auth_policies' + } assert_equal(args['openid.ns'], OPENID2_NS) incoming = Message.from_post_args(args) assert(incoming.is_openid2) @@ -129,7 +129,7 @@ def test_112 assert(expected.is_openid2) assert_equal(expected, car) assert_equal(expected_args, car.to_post_args) - end + end def test_no_signed_list msg = Message.new(OPENID2_NS) diff --git a/test/test_kvpost.rb b/test/test_kvpost.rb index 8f648049..7aa0f544 100644 --- a/test/test_kvpost.rb +++ b/test/test_kvpost.rb @@ -1,8 +1,8 @@ +require "test/unit" +require "testutil" require "openid/kvpost" require "openid/kvform" require "openid/message" -require "test/unit" -require 'testutil' module OpenID class KVPostTestCase < Test::Unit::TestCase diff --git a/test/test_message.rb b/test/test_message.rb index f8ef9187..3a28a224 100644 --- a/test/test_message.rb +++ b/test/test_message.rb @@ -1,9 +1,7 @@ # last synced with Python openid.test.test_message on 6/29/2007. - require 'test/unit' require 'util' require 'openid/message' - require 'rexml/document' module OpenID diff --git a/test/test_oauth.rb b/test/test_oauth.rb index 8c07dfb2..445b064e 100644 --- a/test/test_oauth.rb +++ b/test/test_oauth.rb @@ -1,3 +1,4 @@ +require 'test/unit' require 'openid/extensions/oauth' require 'openid/message' require 'openid/server' @@ -8,7 +9,7 @@ module OpenID module OAuthTest class OAuthRequestTestCase < Test::Unit::TestCase def setup - @req = OAuth::Request.new + @req = OAuth::Request.new end def test_construct @@ -137,7 +138,7 @@ def test_parse_extension_args_empty end def test_from_success_response - + openid_req_msg = Message.from_openid_args({ 'mode' => 'id_res', 'ns' => OPENID2_NS, diff --git a/test/test_openid_yadis.rb b/test/test_openid_yadis.rb index fb8c71e6..77ffa2ba 100644 --- a/test/test_openid_yadis.rb +++ b/test/test_openid_yadis.rb @@ -1,4 +1,3 @@ - require 'test/unit' require 'openid/consumer/discovery' require 'openid/yadis/services' diff --git a/test/test_pape.rb b/test/test_pape.rb index bd8289c4..ddc0ba8a 100644 --- a/test/test_pape.rb +++ b/test/test_pape.rb @@ -1,3 +1,4 @@ +require 'test/unit' require 'openid/extensions/pape' require 'openid/message' require 'openid/server' @@ -170,11 +171,11 @@ def test_parse_extension_args_empty assert_equal(nil, @req.auth_time) assert_equal([], @req.auth_policies) end - + def test_parse_extension_args_strict_bogus1 args = {'auth_policies' => 'http://foo http://bar', 'auth_time' => 'this one time'} - assert_raises(ArgumentError) { + assert_raises(ArgumentError) { @req.parse_extension_args(args, true) } end @@ -183,11 +184,11 @@ def test_parse_extension_args_strict_bogus2 args = {'auth_policies' => 'http://foo http://bar', 'auth_time' => '1983-11-05T12:30:24Z', 'nist_auth_level' => 'some'} - assert_raises(ArgumentError) { + assert_raises(ArgumentError) { @req.parse_extension_args(args, true) } end - + def test_parse_extension_args_strict_good args = {'auth_policies' => 'http://foo http://bar', 'auth_time' => '2007-10-11T05:25:18Z', @@ -208,9 +209,9 @@ def test_parse_extension_args_nostrict_bogus assert_equal(nil, @req.nist_auth_level) end - + def test_from_success_response - + openid_req_msg = Message.from_openid_args({ 'mode' => 'id_res', 'ns' => OPENID2_NS, diff --git a/test/test_parsehtml.rb b/test/test_parsehtml.rb index 49542a6a..dbf6fabc 100644 --- a/test/test_parsehtml.rb +++ b/test/test_parsehtml.rb @@ -1,6 +1,6 @@ -require 'test/unit' -require "openid/yadis/parsehtml" +require "test/unit" require "testutil" +require "openid/yadis/parsehtml" module OpenID class ParseHTMLTestCase < Test::Unit::TestCase diff --git a/test/test_server.rb b/test/test_server.rb index 11d0f567..d733ad6c 100644 --- a/test/test_server.rb +++ b/test/test_server.rb @@ -1,3 +1,7 @@ +require 'test/unit' +require 'testutil' +require 'util' +require 'uri' require 'openid/server' require 'openid/cryptutil' require 'openid/association' @@ -6,11 +10,6 @@ require 'openid/store/memory' require 'openid/dh' require 'openid/consumer/associationmanager' -require 'util' -require "testutil" - -require 'test/unit' -require 'uri' # In general, if you edit or add tests here, try to move in the # direction of testing smaller units. For testing the external @@ -208,7 +207,7 @@ def test_dictOfLists begin result = @decode.call(args) rescue ArgumentError => err - assert(!err.to_s.index('values').nil?, err) + assert !err.to_s.index('values').nil? else flunk("Expected ArgumentError, but got result #{result}") end @@ -1008,7 +1007,7 @@ def test_cancel assert(webresponse.headers.has_key?('location')) location = webresponse.headers['location'] query = Util.parse_query(URI::parse(location).query) - assert(!query.has_key?('openid.sig'), response.fields.to_post_args()) + assert !query.has_key?('openid.sig') end def test_assocReply @@ -1530,7 +1529,7 @@ def test_addField {'blue' => 'star', 'mode' => 'id_res', }) - + assert_equal(@response.fields.get_args(namespace), {'bright' => 'potato'}) end @@ -1613,7 +1612,7 @@ def test_invalid r = @request.answer(@signatory) assert_equal({'is_valid' => 'false'}, r.fields.get_args(OPENID_NS)) - + end def test_replay @@ -1750,7 +1749,7 @@ def test_protoError invalid_s1, invalid_s1_2, ] - + bad_request_argss.each { |request_args| message = Message.from_post_args(request_args) assert_raise(Server::ProtocolError) { @@ -2299,8 +2298,7 @@ def test_verifyBadHandle verified = @signatory.verify(assoc_handle, signed) } - assert(!verified) - #assert(@messages) + assert !verified end def test_verifyAssocMismatch @@ -2322,16 +2320,14 @@ def test_verifyAssocMismatch verified = @signatory.verify(assoc_handle, signed) } - assert(!verified) - #assert(@messages) + assert !verified end def test_getAssoc assoc_handle = makeAssoc(true) assoc = @signatory.get_association(assoc_handle, true) - assert(assoc) - assert_equal(assoc.handle, assoc_handle) - # @failIf(@messages, @messages) + assert assoc + assert_equal assoc.handle, assoc_handle end def test_getAssocExpired @@ -2340,8 +2336,7 @@ def test_getAssocExpired silence_logging { assoc = @signatory.get_association(assoc_handle, true) } - assert(!assoc, assoc) - # assert(@messages) + assert !assoc end def test_getAssocInvalid @@ -2428,7 +2423,7 @@ def test_openid1_assoc_checkid 'openid.assoc_type' => 'HMAC-SHA1'} areq = @server.decode_request(assoc_args) aresp = @server.handle_request(areq) - + amess = aresp.fields assert(amess.is_openid1) ahandle = amess.get_arg(OPENID_NS, 'assoc_handle') @@ -2441,7 +2436,7 @@ def test_openid1_assoc_checkid 'openid.return_to' => 'http://example.com/openid/consumer', 'openid.assoc_handle' => ahandle, 'openid.identity' => 'http://foo.com/'} - + cireq = @server.decode_request(checkid_args) ciresp = cireq.answer(true) @@ -2449,7 +2444,7 @@ def test_openid1_assoc_checkid assert_equal(assoc.get_message_signature(signed_resp.fields), signed_resp.fields.get_arg(OPENID_NS, 'sig')) - + assert(assoc.check_message_signature(signed_resp.fields)) end diff --git a/test/test_sreg.rb b/test/test_sreg.rb index 7438d592..ebafe189 100644 --- a/test/test_sreg.rb +++ b/test/test_sreg.rb @@ -1,7 +1,7 @@ +require 'test/unit' require 'openid/extensions/sreg' require 'openid/message' require 'openid/server' -require 'test/unit' module OpenID module SReg @@ -71,7 +71,7 @@ def initialize @openid1 = false @namespaces = NamespaceMap.new end - + def is_openid1 return @openid1 end @@ -95,7 +95,7 @@ def test_openid1_empty assert_equal('sreg', @msg.namespaces.get_alias(ns_uri)) assert_equal(NS_URI, ns_uri) end - + def test_openid1defined_1_0 @msg.openid1 = true @msg.namespaces.add(NS_URI_1_0) @@ -117,7 +117,7 @@ def test_openid1_defined_1_0_override_alias } } end - + def test_openid1_defined_badly @msg.openid1 = true @msg.namespaces.add_alias('http://invalid/', 'sreg') @@ -182,7 +182,7 @@ def test_from_openid_request_message_copied end def test_from_openid_request_ns_1_0 - message = Message.from_openid_args({'ns.sreg' => NS_URI_1_0, + message = Message.from_openid_args({'ns.sreg' => NS_URI_1_0, "sreg.required" => "nickname"}) openid_req = Server::OpenIDRequest.new openid_req.message = message @@ -214,7 +214,7 @@ def test_parse_extension_args_non_strict req.parse_extension_args({'required' => 'stuff'}) assert_equal([], req.required) end - + def test_parse_extension_args_strict req = Request.new assert_raises(ArgumentError) { @@ -351,7 +351,7 @@ def test_request_fields req.request_fields(fields) assert_equal(fields, req.optional) assert_equal([], req.required) - + # By default, adding the same fields over again has no effect req.request_fields(fields) assert_equal(fields, req.optional) @@ -439,7 +439,7 @@ def test_from_success_response_unsigned }) success_resp = DummySuccessResponse.new(message, {}) sreg_resp = Response.from_success_response(success_resp, false) - assert_equal({'nickname' => 'The Mad Stork'}, + assert_equal({'nickname' => 'The Mad Stork'}, sreg_resp.get_extension_args) end end diff --git a/test/test_stores.rb b/test/test_stores.rb index 115bcc43..df8aeb38 100644 --- a/test/test_stores.rb +++ b/test/test_stores.rb @@ -119,8 +119,7 @@ def test_store ret_assoc = @store.get_association(server_url, nil) unexpected = [assoc2.handle, assoc3.handle] - assert(ret_assoc.nil? || !unexpected.member?(ret_assoc.handle), - ret_assoc) + assert ret_assoc.nil? || !unexpected.member?(ret_assoc.handle) _check_retrieve(server_url, assoc.handle, assoc) _check_retrieve(server_url, assoc2.handle, nil) diff --git a/test/test_trustroot.rb b/test/test_trustroot.rb index 2616021a..8b1cd1bf 100644 --- a/test/test_trustroot.rb +++ b/test/test_trustroot.rb @@ -1,22 +1,21 @@ require 'test/unit' +require 'testutil' require 'openid/trustroot' -require "testutil" - class TrustRootTest < Test::Unit::TestCase include OpenID::TestDataMixin def _test_sanity(case_, sanity, desc) tr = OpenID::TrustRoot::TrustRoot.parse(case_) if sanity == 'sane' - assert(! tr.nil?) - assert(tr.sane?, [case_, desc]) - assert(OpenID::TrustRoot::TrustRoot.check_sanity(case_), [case_, desc]) + assert !tr.nil? + assert tr.sane?, [case_, desc].join(' ') + assert OpenID::TrustRoot::TrustRoot.check_sanity(case_), [case_, desc].join(' ') elsif sanity == 'insane' - assert(!tr.sane?, [case_, desc]) - assert(!OpenID::TrustRoot::TrustRoot.check_sanity(case_), [case_, desc]) + assert !tr.sane?, [case_, desc].join(' ') + assert !OpenID::TrustRoot::TrustRoot.check_sanity(case_), [case_, desc].join(' ') else - assert(tr.nil?, case_) + assert tr.nil?, case_ end end @@ -24,11 +23,11 @@ def _test_match(trust_root, url, expected_match) tr = OpenID::TrustRoot::TrustRoot.parse(trust_root) actual_match = tr.validate_https://github.com/url(url) if expected_match - assert(actual_match, [trust_root, url]) - assert(OpenID::TrustRoot::TrustRoot.check_url(https://github.com/trust_root,%20url)) + assert actual_match, [trust_root, url].join(' ') + assert OpenID::TrustRoot::TrustRoot.check_url(https://github.com/trust_root,%20url) else - assert(!actual_match, [expected_match, actual_match, trust_root, url]) - assert(!OpenID::TrustRoot::TrustRoot.check_url(https://github.com/trust_root,%20url)) + assert !actual_match, [expected_match, actual_match, trust_root, url].join(' ') + assert !OpenID::TrustRoot::TrustRoot.check_url(https://github.com/trust_root,%20url) end end @@ -55,8 +54,8 @@ def getTests(grps, head, dat) tests = [] top = head.strip() gdat = dat.split('-' * 40 + "\n").collect { |i| i.strip() } - assert(gdat[0] == '') - assert(gdat.length == (grps.length * 2 + 1), [gdat, grps]) + assert gdat[0] == '' + assert gdat.length == (grps.length * 2 + 1) i = 1 grps.each { |x| n, desc = gdat[i].split(': ') @@ -107,7 +106,7 @@ def test_build_discovery_url trust_root, expected_disco_url = case_ tr = OpenID::TrustRoot::TrustRoot.parse(trust_root) actual_disco_url = tr.build_discovery_url() - assert(actual_disco_url == expected_disco_url, case_ + [actual_disco_url]) + assert actual_disco_url == expected_disco_url } end end diff --git a/test/test_urinorm.rb b/test/test_urinorm.rb index 55c50e1c..64235035 100644 --- a/test/test_urinorm.rb +++ b/test/test_urinorm.rb @@ -1,7 +1,6 @@ -require 'test/unit' - -require "openid/urinorm" +require "test/unit" require "testutil" +require "openid/urinorm" class URINormTestCase < Test::Unit::TestCase include OpenID::TestDataMixin diff --git a/test/test_util.rb b/test/test_util.rb index ce1138ae..b9a82b75 100644 --- a/test/test_util.rb +++ b/test/test_util.rb @@ -1,6 +1,5 @@ -# coding: ASCII-8BIT -require 'test/unit' - +# encoding: ASCII-8BIT +require "test/unit" require "openid/util" module OpenID diff --git a/test/test_xrds.rb b/test/test_xrds.rb index ced78028..31157832 100644 --- a/test/test_xrds.rb +++ b/test/test_xrds.rb @@ -1,8 +1,6 @@ - require 'test/unit' -require 'openid/yadis/xrds' - require 'testutil' +require 'openid/yadis/xrds' module OpenID module Yadis @@ -16,8 +14,8 @@ module XRDSTestMixin XRDS_DATA_DIR = TEST_DATA_DIR.join('test_xrds') - def read_data_file(filename) - super(filename, false, XRDS_DATA_DIR) + def read_xrds_data_file(filename) + read_data_file(filename, false, XRDS_DATA_DIR) end end @@ -26,12 +24,12 @@ class ParseXRDSTestCase < Test::Unit::TestCase # Check that parsing succeeds at all. def test_parse - result = Yadis.parseXRDS(read_data_file(XRD_FILE)) + result = Yadis.parseXRDS(read_xrds_data_file(XRD_FILE)) assert_not_nil result end def test_parse_no_xrds_xml - xmldoc = read_data_file(NOXRDS_FILE) + xmldoc = read_xrds_data_file(NOXRDS_FILE) assert_raise(Yadis::XRDSError) { Yadis.parseXRDS(xmldoc) } @@ -44,8 +42,8 @@ def test_parse_no_xrds_empty end def test_is_xrds - isnt = REXML::Document.new(read_data_file(NOXRDS_FILE)) - should_be = Yadis.parseXRDS(read_data_file(XRD_FILE)) + isnt = REXML::Document.new(read_xrds_data_file(NOXRDS_FILE)) + should_be = Yadis.parseXRDS(read_xrds_data_file(XRD_FILE)) assert_equal false, Yadis::is_xrds?(isnt) assert Yadis::is_xrds?(should_be) end @@ -56,7 +54,7 @@ class GetYadisXRDTestCase < Test::Unit::TestCase # XXX: Test to make sure this really gets the _right_ XRD. def test_get_xrd - doc = Yadis.parseXRDS(read_data_file(XRD_FILE)) + doc = Yadis.parseXRDS(read_xrds_data_file(XRD_FILE)) result = Yadis::get_yadis_xrd(doc) assert_not_nil result assert_equal 'XRD', result.name @@ -64,7 +62,7 @@ def test_get_xrd end def test_no_xrd - xmldoc = read_data_file(NOXRD_FILE) + xmldoc = read_xrds_data_file(NOXRD_FILE) doc = Yadis.parseXRDS(xmldoc) assert_raise(Yadis::XRDSError) { Yadis.get_yadis_xrd(doc) @@ -76,7 +74,7 @@ class EachServiceTestCase < Test::Unit::TestCase include XRDSTestMixin def test_get_xrd - doc = Yadis.parseXRDS(read_data_file(XRD_FILE)) + doc = Yadis.parseXRDS(read_xrds_data_file(XRD_FILE)) count = 0 result = Yadis::each_service(doc) { |e| assert_equal 'Service', e.name @@ -87,7 +85,7 @@ def test_get_xrd end def test_no_xrd - xmldoc = read_data_file(NOXRD_FILE) + xmldoc = read_xrds_data_file(NOXRD_FILE) doc = Yadis.parseXRDS(xmldoc) assert_raise(Yadis::XRDSError) { Yadis.each_service(doc) @@ -95,7 +93,7 @@ def test_no_xrd end def test_equal_j3h - doc = Yadis.parseXRDS(read_data_file('=j3h.2007.11.14.xrds')) + doc = Yadis.parseXRDS(read_xrds_data_file('=j3h.2007.11.14.xrds')) count = 0 result = Yadis::each_service(doc) { |e| assert_equal 'Service', e.name @@ -161,7 +159,7 @@ class GetCanonicalIDTestCase < Test::Unit::TestCase include XRDSTestMixin def test_multisegment_xri - xmldoc = Yadis.parseXRDS(read_data_file('subsegments.xrds')) + xmldoc = Yadis.parseXRDS(read_xrds_data_file('subsegments.xrds')) result = Yadis.get_canonical_id('xri://=nishitani*masaki', xmldoc) end end diff --git a/test/test_xrires.rb b/test/test_xrires.rb index 3959361b..6db1bdff 100644 --- a/test/test_xrires.rb +++ b/test/test_xrires.rb @@ -1,4 +1,3 @@ - require 'test/unit' require 'openid/yadis/xrires' @@ -30,33 +29,47 @@ def setup def test_proxy_url st = @servicetype ste = @servicetype_enc - args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" + ste + args_esc = ["_xrd_r=application%2Fxrds%2Bxml", "_xrd_t=#{ste}"] pqu = @proxy.method('query_url') h = @proxy_url - assert_equal(h + '=foo?' + args_esc, pqu.call('=foo', st)) - assert_equal(h + '=foo/bar?baz&' + args_esc, - pqu.call('=foo/bar?baz', st)) - assert_equal(h + '=foo/bar?baz=quux&' + args_esc, - pqu.call('=foo/bar?baz=quux', st)) - assert_equal(h + '=foo/bar?mi=fa&so=la&' + args_esc, - pqu.call('=foo/bar?mi=fa&so=la', st)) + assert_match h + '=foo?', pqu.call('=foo', st) + assert_match args_esc[0], pqu.call('=foo', st) + assert_match args_esc[1], pqu.call('=foo', st) + + assert_match h + '=foo/bar?baz&', pqu.call('=foo/bar?baz', st) + assert_match args_esc[0], pqu.call('=foo/bar?baz', st) + assert_match args_esc[1], pqu.call('=foo/bar?baz', st) + + assert_match h + '=foo/bar?baz=quux&', pqu.call('=foo/bar?baz=quux', st) + assert_match args_esc[0], pqu.call('=foo/bar?baz=quux', st) + assert_match args_esc[1], pqu.call('=foo/bar?baz=quux', st) + + assert_match h + '=foo/bar?mi=fa&so=la&', pqu.call('=foo/bar?mi=fa&so=la', st) + assert_match args_esc[0], pqu.call('=foo/bar?mi=fa&so=la', st) + assert_match args_esc[1], pqu.call('=foo/bar?mi=fa&so=la', st) # With no service endpoint selection. args_esc = "_xrd_r=application%2Fxrds%2Bxml%3Bsep%3Dfalse" - assert_equal(h + '=foo?' + args_esc, pqu.call('=foo', nil)) + + assert_match h + '=foo?', pqu.call('=foo', nil) + assert_match args_esc, pqu.call('=foo', nil) end def test_proxy_url_qmarks st = @servicetype ste = @servicetype_enc - args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" + ste + args_esc = ["_xrd_r=application%2Fxrds%2Bxml", "_xrd_t=#{ste}"] pqu = @proxy.method('query_url') h = @proxy_url - assert_equal(h + '=foo/bar??' + args_esc, pqu.call('=foo/bar?', st)) - assert_equal(h + '=foo/bar????' + args_esc, - pqu.call('=foo/bar???', st)) + assert_match h + '=foo/bar??', pqu.call('=foo/bar?', st) + assert_match args_esc[0], pqu.call('=foo/bar?', st) + assert_match args_esc[1], pqu.call('=foo/bar?', st) + + assert_match h + '=foo/bar????', pqu.call('=foo/bar???', st) + assert_match args_esc[0], pqu.call('=foo/bar???', st) + assert_match args_esc[1], pqu.call('=foo/bar???', st) end end end diff --git a/test/test_yadis_discovery.rb b/test/test_yadis_discovery.rb index b0fdd178..95f2c35a 100644 --- a/test/test_yadis_discovery.rb +++ b/test/test_yadis_discovery.rb @@ -1,8 +1,6 @@ - require 'test/unit' -require 'uri' require 'testutil' - +require 'uri' require 'openid/yadis/discovery' require 'openid/fetchers' require 'openid/util' From b02591b8b4e7ccb024b159762d2f441a475f7bdd Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 21 Jun 2012 23:50:27 +0200 Subject: [PATCH 011/130] Converted README to Markdown --- README | 81 ------------------------------------------------------- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 81 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 2abba7e7..00000000 --- a/README +++ /dev/null @@ -1,81 +0,0 @@ -=Ruby OpenID - -A Ruby library for verifying and serving OpenID identities. - -==Features -* Easy to use API for verifying OpenID identites - OpenID::Consumer -* Support for serving OpenID identites - OpenID::Server -* Does not depend on underlying web framework -* Supports multiple storage mechanisms (Filesystem, ActiveRecord, Memory) -* Example code to help you get started, including: - * Ruby on Rails based consumer and server - * OpenIDLoginGenerator for quickly getting creating a rails app that uses - OpenID for authentication - * ActiveRecordOpenIDStore plugin -* Comprehensive test suite -* Supports both OpenID 1 and OpenID 2 transparently - -==Installing -Before running the examples or writing your own code you'll need to install -the library. See the INSTALL file or use rubygems: - - gem install ruby-openid - -Check the installation: - - $ irb - irb> require 'rubygems' - irb> require_gem 'ruby-openid' - => true - -The library is known to work with Ruby 1.8.4 on Unix, Max OSX and -Win32. Examples have been tested with Rails 1.1 and 1.2, and 2.0. - -==Getting Started -The best way to start is to look at the rails_openid example. -You can run it with: - cd examples/rails_openid - script/server - -If you are writing an OpenID Relying Party, a good place to start is: -examples/rails_openid/app/controllers/consumer_controller.rb - -And if you are writing an OpenID provider: -examples/rails_openid/app/controllers/server_controller.rb - -The library code is quite well documented, so don't be squeamish, and -look at the library itself if there's anything you don't understand in -the examples. - -==Homepage -http://github.com/openid/ruby-openid - -See also: -http://openid.net/ - -==Community -Discussion regarding the Ruby OpenID library and other JanRain OpenID -libraries takes place on the the OpenID mailing list on -openid.net. - -http://openid.net/developers/dev-mailing-lists/ - -Please join this list to discuss, ask implementation questions, report -bugs, etc. Also check out the openid channel on the freenode IRC -network. - -If you have a bugfix or feature you'd like to contribute, don't -hesitate to send it to us. For more detailed information on how to -contribute, see - - http://openidenabled.com/contribute/ - -==Author -Copyright 2006-2008, JanRain, Inc. - -Contact openid@janrain.com or visit the OpenID channel on pibb.com: - -http://pibb.com/go/openid - -==License -Apache Software License. For more information see the LICENSE file. diff --git a/README.md b/README.md new file mode 100644 index 00000000..4dc9b83c --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# Ruby OpenID + +A Ruby library for verifying and serving OpenID identities. + +## Features + + * Easy to use API for verifying OpenID identites - OpenID::Consumer + * Support for serving OpenID identites - OpenID::Server + * Does not depend on underlying web framework + * Supports multiple storage mechanisms (Filesystem, ActiveRecord, Memory) + * Example code to help you get started, including: + * Ruby on Rails based consumer and server + * OpenIDLoginGenerator for quickly getting creating a rails app that uses + OpenID for authentication + * ActiveRecordOpenIDStore plugin + * Comprehensive test suite + * Supports both OpenID 1 and OpenID 2 transparently + +## Installing + +Before running the examples or writing your own code you'll need to install +the library. See the INSTALL file or use rubygems: + + gem install ruby-openid + +Check the installation: + + $ irb + irb> require 'rubygems' + irb> require_gem 'ruby-openid' + => true + +The library is known to work with Ruby 1.8.4 on Unix, Max OSX and +Win32. Examples have been tested with Rails 1 to 3. + +## Getting Started + +The best way to start is to look at the rails_openid example. +You can run it with: + + cd examples/rails_openid + script/server + +If you are writing an OpenID Relying Party, a good place to start is: +`examples/rails_openid/app/controllers/consumer_controller.rb` + +And if you are writing an OpenID provider: +`examples/rails_openid/app/controllers/server_controller.rb` + +The library code is quite well documented, so don't be squeamish, and +look at the library itself if there's anything you don't understand in +the examples. + +## Homepage + + * [GitHub](http://github.com/openid/ruby-openid) + * [Website](http://openid.net/) + +## Community + +Discussion regarding the Ruby OpenID library and other JanRain OpenID +libraries takes place on the [OpenID mailing list](http://openid.net/developers/dev-mailing-lists/). + +Please join this list to discuss, ask implementation questions, report +bugs, etc. Also check out the openid channel on the freenode IRC +network. + +If you have a bugfix or feature you'd like to contribute, don't +hesitate to send it to us: [How to contribute](http://openidenabled.com/contribute/). + +## Author + +Copyright 2006-2012, JanRain, Inc. + +Contact openid@janrain.com or visit the [OpenID channel on pibb.com](http://pibb.com/go/openid). + +## License + +Apache Software License. For more information see the LICENSE file. From 81b85d4c80d2bcee1c5807e91fd4ddc20a65bde7 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 21 Jun 2012 23:52:13 +0200 Subject: [PATCH 012/130] Added travis config --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b8aba58c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: ruby +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 +script: rake From 75a7e98005542ede6db3fc7f1fc551e0a2ca044a Mon Sep 17 00:00:00 2001 From: Tom Quackenbush Date: Fri, 25 Feb 2011 13:02:43 -0500 Subject: [PATCH 013/130] Add attr_reader for setup_url on SetupNeededResponse. Add asserts to setup_needed tests to verify setup_url in response. Signed-off-by: Dennis Reimann --- lib/openid/consumer/responses.rb | 2 ++ test/test_consumer.rb | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/openid/consumer/responses.rb b/lib/openid/consumer/responses.rb index 4947c1ca..89a551fb 100644 --- a/lib/openid/consumer/responses.rb +++ b/lib/openid/consumer/responses.rb @@ -139,6 +139,8 @@ def initialize(endpoint) class SetupNeededResponse include Response STATUS = SETUP_NEEDED + + attr_reader :setup_url def initialize(endpoint, setup_url) @endpoint = endpoint @setup_url = setup_url diff --git a/test/test_consumer.rb b/test/test_consumer.rb index f6a85a30..05944884 100644 --- a/test/test_consumer.rb +++ b/test/test_consumer.rb @@ -182,9 +182,11 @@ def test_setup_needed_openid1 end def test_setup_needed_openid2 - args = {'openid.ns' => OPENID2_NS, 'openid.mode' => 'setup_needed'} + setup_url = 'http://setup.url/' + args = {'openid.ns' => OPENID2_NS, 'openid.mode' => 'setup_needed', 'openid.user_setup_url' => setup_url} response = @consumer.complete(args, nil) assert_equal(SETUP_NEEDED, response.status) + assert_equal(setup_url, response.setup_url) end def test_idres_setup_needed_openid1 @@ -195,6 +197,7 @@ def test_idres_setup_needed_openid1 } response = @consumer.complete(args, nil) assert_equal(SETUP_NEEDED, response.status) + assert_equal(setup_url, response.setup_url) end def test_error From 53de5a9128c5cb11fb211ebc12ad5ccbe5cd8605 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 00:31:32 +0200 Subject: [PATCH 014/130] Removed require_gem references. Closes #12. --- README.md | 3 ++- examples/rails_openid/app/controllers/server_controller.rb | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4dc9b83c..465664a8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Check the installation: $ irb irb> require 'rubygems' - irb> require_gem 'ruby-openid' + => false + irb> gem 'ruby-openid' => true The library is known to work with Ruby 1.8.4 on Unix, Max OSX and diff --git a/examples/rails_openid/app/controllers/server_controller.rb b/examples/rails_openid/app/controllers/server_controller.rb index af0b1a7c..9a37c7e7 100644 --- a/examples/rails_openid/app/controllers/server_controller.rb +++ b/examples/rails_openid/app/controllers/server_controller.rb @@ -1,16 +1,10 @@ require 'pathname' -# load the openid library, first trying rubygems -#begin -# require "rubygems" -# require_gem "ruby-openid", ">= 1.0" -#rescue LoadError require "openid" require "openid/consumer/discovery" require 'openid/extensions/sreg' require 'openid/extensions/pape' require 'openid/store/filesystem' -#end class ServerController < ApplicationController From 4613050e0af2b6b097a9b0fe522814ca2f2e42c1 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 01:04:52 +0200 Subject: [PATCH 015/130] [ci skip] Added Travis build status to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 465664a8..e8f7a722 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Ruby library for verifying and serving OpenID identities. +[![Build Status](https://secure.travis-ci.org/openid/ruby-openid.png)](http://travis-ci.org/openid/ruby-openid) + ## Features * Easy to use API for verifying OpenID identites - OpenID::Consumer From a276a63d68639e985c1f327cf817489ccc5f9a17 Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 18 Jan 2011 14:58:50 +0900 Subject: [PATCH 016/130] add UI extension support Signed-off-by: Dennis Reimann --- lib/openid/extensions/ui.rb | 53 +++++++++++++++++++++ test/test_ui.rb | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 lib/openid/extensions/ui.rb create mode 100644 test/test_ui.rb diff --git a/lib/openid/extensions/ui.rb b/lib/openid/extensions/ui.rb new file mode 100644 index 00000000..f0db5dca --- /dev/null +++ b/lib/openid/extensions/ui.rb @@ -0,0 +1,53 @@ +# An implementation of the OpenID User Interface Extension 1.0 - DRAFT 0.5 +# see: http://svn.openid.net/repos/specifications/user_interface/1.0/trunk/openid-user-interface-extension-1_0.html + +require 'openid/extension' + +module OpenID + + module UI + NS_URI = "http://specs.openid.net/extensions/ui/1.0" + + class Request < Extension + attr_accessor :lang, :icon, :mode, :ns_alias, :ns_uri + def initialize(mode = nil, icon = nil, lang = nil) + @ns_alias = 'ui' + @ns_uri = NS_URI + @lang = lang + @icon = icon + @mode = mode + end + + def get_extension_args + ns_args = {} + ns_args['lang'] = @lang if @lang + ns_args['icon'] = @icon if @icon + ns_args['mode'] = @mode if @mode + return ns_args + end + + # Instantiate a Request object from the arguments in a + # checkid_* OpenID message + # return nil if the extension was not requested. + def self.from_openid_request(oid_req) + oauth_req = new + args = oid_req.message.get_args(NS_URI) + if args == {} + return nil + end + oauth_req.parse_extension_args(args) + return oauth_req + end + + # Set UI extention parameters + def parse_extension_args(args) + @lang = args["lang"] + @icon = args["icon"] + @mode = args["mode"] + end + + end + + end + +end diff --git a/test/test_ui.rb b/test/test_ui.rb new file mode 100644 index 00000000..ad01a84e --- /dev/null +++ b/test/test_ui.rb @@ -0,0 +1,93 @@ +require 'openid/extensions/ui' +require 'openid/message' +require 'openid/server' +require 'test/unit' + +module OpenID + module UITest + class UIRequestTestCase < Test::Unit::TestCase + + def setup + @req = UI::Request.new + end + + def test_construct + assert_nil @req.mode + assert_nil @req.icon + assert_nil @req.lang + assert_equal 'ui', @req.ns_alias + + req2 = UI::Request.new("popup", "http://sample.com/favicon.png", "ja-JP") + assert_equal "popup", req2.mode + assert_equal "http://sample.com/favicon.png", req2.icon + assert_equal "ja-JP", req2.lang + end + + def test_add_mode + @req.mode = "popup" + assert_equal "popup", @req.mode + end + + def test_add_icon + @req.icon = "http://sample.com/favicon.png" + assert_equal "http://sample.com/favicon.png", @req.icon + end + + def test_add_lang + @req.lang = "ja-JP" + assert_equal "ja-JP", @req.lang + end + + def test_get_extension_args + assert_equal({}, @req.get_extension_args) + @req.mode = "popup" + assert_equal({'mode' => 'popup'}, @req.get_extension_args) + @req.icon = "http://sample.com/favicon.png" + assert_equal({'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png'}, @req.get_extension_args) + @req.lang = "ja-JP" + assert_equal({'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png', 'lang' => 'ja-JP'}, @req.get_extension_args) + end + + def test_parse_extension_args + args = {'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png', 'lang' => 'ja-JP'} + @req.parse_extension_args args + assert_equal "popup", @req.mode + assert_equal "http://sample.com/favicon.png", @req.icon + assert_equal "ja-JP", @req.lang + end + + def test_parse_extension_args_empty + @req.parse_extension_args({}) + assert_nil @req.mode + assert_nil @req.icon + assert_nil @req.lang + end + + def test_from_openid_request + openid_req_msg = Message.from_openid_args( + 'mode' => 'checkid_setup', + 'ns' => OPENID2_NS, + 'ns.ui' => UI::NS_URI, + 'ui.mode' => 'popup', + 'ui.icon' => "http://sample.com/favicon.png", + 'ui.lang' => 'ja-JP' + ) + oid_req = Server::OpenIDRequest.new + oid_req.message = openid_req_msg + req = UI::Request.from_openid_request oid_req + assert_equal "popup", req.mode + assert_equal "http://sample.com/favicon.png", req.icon + assert_equal "ja-JP", req.lang + end + + def test_from_openid_request_no_ui_params + message = Message.new + openid_req = Server::OpenIDRequest.new + openid_req.message = message + ui_req = UI::Request.from_openid_request openid_req + assert ui_req.nil? + end + + end + end +end From 5326d0d320ea3379b6da0e9e6190cda463744edb Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 18 Jan 2011 15:01:18 +0900 Subject: [PATCH 017/130] fix typo Signed-off-by: Dennis Reimann --- lib/openid/extensions/ui.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/extensions/ui.rb b/lib/openid/extensions/ui.rb index f0db5dca..6c156d7f 100644 --- a/lib/openid/extensions/ui.rb +++ b/lib/openid/extensions/ui.rb @@ -39,7 +39,7 @@ def self.from_openid_request(oid_req) return oauth_req end - # Set UI extention parameters + # Set UI extension parameters def parse_extension_args(args) @lang = args["lang"] @icon = args["icon"] From 6b41492c01efcc2ae48057ad1f6c8aafe41e4249 Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 18 Jan 2011 17:15:45 +0900 Subject: [PATCH 018/130] icon spec update it should be "true" when required. Signed-off-by: Dennis Reimann --- test/test_ui.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/test_ui.rb b/test/test_ui.rb index ad01a84e..92e74a49 100644 --- a/test/test_ui.rb +++ b/test/test_ui.rb @@ -17,9 +17,9 @@ def test_construct assert_nil @req.lang assert_equal 'ui', @req.ns_alias - req2 = UI::Request.new("popup", "http://sample.com/favicon.png", "ja-JP") + req2 = UI::Request.new("popup", true, "ja-JP") assert_equal "popup", req2.mode - assert_equal "http://sample.com/favicon.png", req2.icon + assert_equal true, req2.icon assert_equal "ja-JP", req2.lang end @@ -29,8 +29,8 @@ def test_add_mode end def test_add_icon - @req.icon = "http://sample.com/favicon.png" - assert_equal "http://sample.com/favicon.png", @req.icon + @req.icon = true + assert_equal true, @req.icon end def test_add_lang @@ -42,17 +42,17 @@ def test_get_extension_args assert_equal({}, @req.get_extension_args) @req.mode = "popup" assert_equal({'mode' => 'popup'}, @req.get_extension_args) - @req.icon = "http://sample.com/favicon.png" - assert_equal({'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png'}, @req.get_extension_args) + @req.icon = true + assert_equal({'mode' => 'popup', 'icon' => true}, @req.get_extension_args) @req.lang = "ja-JP" - assert_equal({'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png', 'lang' => 'ja-JP'}, @req.get_extension_args) + assert_equal({'mode' => 'popup', 'icon' => true, 'lang' => 'ja-JP'}, @req.get_extension_args) end def test_parse_extension_args - args = {'mode' => 'popup', 'icon' => 'http://sample.com/favicon.png', 'lang' => 'ja-JP'} + args = {'mode' => 'popup', 'icon' => true, 'lang' => 'ja-JP'} @req.parse_extension_args args assert_equal "popup", @req.mode - assert_equal "http://sample.com/favicon.png", @req.icon + assert_equal true, @req.icon assert_equal "ja-JP", @req.lang end @@ -69,14 +69,14 @@ def test_from_openid_request 'ns' => OPENID2_NS, 'ns.ui' => UI::NS_URI, 'ui.mode' => 'popup', - 'ui.icon' => "http://sample.com/favicon.png", + 'ui.icon' => true, 'ui.lang' => 'ja-JP' ) oid_req = Server::OpenIDRequest.new oid_req.message = openid_req_msg req = UI::Request.from_openid_request oid_req assert_equal "popup", req.mode - assert_equal "http://sample.com/favicon.png", req.icon + assert_equal true, req.icon assert_equal "ja-JP", req.lang end From 3cb623e3a16e2fe32822234a0ce57ca74ff7535f Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 18 Jan 2011 17:42:27 +0900 Subject: [PATCH 019/130] remove 1 space Signed-off-by: Dennis Reimann --- test/test_ui.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ui.rb b/test/test_ui.rb index 92e74a49..e20227a4 100644 --- a/test/test_ui.rb +++ b/test/test_ui.rb @@ -8,7 +8,7 @@ module UITest class UIRequestTestCase < Test::Unit::TestCase def setup - @req = UI::Request.new + @req = UI::Request.new end def test_construct From 1a486f9fc69f76f6048deb7dbb5f3f5c5fe0dc82 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 10:47:05 +0200 Subject: [PATCH 020/130] Added jruby (1.9) and REE to travis config --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b8aba58c..65878148 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,6 @@ rvm: - 1.8.7 - 1.9.2 - 1.9.3 + - ree + - jruby-19mode script: rake From 40baed6cf7326025058a131c2b76047345618539 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 13:53:04 +0200 Subject: [PATCH 021/130] Fixed JRuby (1.9 mode) incompatibilty For further information see the ticket for JRUBY-6389: http://jira.codehaus.org/browse/JRUBY-6389 This potentially closes #6. Also added travis config for jruby-head and jruby 1.8 mode --- .travis.yml | 2 ++ lib/openid/store/filesystem.rb | 15 +++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65878148..3a1177b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,7 @@ rvm: - 1.9.2 - 1.9.3 - ree + - jruby-18mode - jruby-19mode + - jruby-head script: rake diff --git a/lib/openid/store/filesystem.rb b/lib/openid/store/filesystem.rb index 6a555ade..3eb3c488 100644 --- a/lib/openid/store/filesystem.rb +++ b/lib/openid/store/filesystem.rb @@ -13,10 +13,9 @@ class Filesystem < Interface # Create a Filesystem store instance, putting all data in +directory+. def initialize(directory) - p_dir = Pathname.new(directory) - @nonce_dir = p_dir.join('nonces') - @association_dir = p_dir.join('associations') - @temp_dir = p_dir.join('temp') + @nonce_dir = File.join(directory, 'nonces') + @association_dir = File.join(directory, 'associations') + @temp_dir = File.join(directory, 'temp') self.ensure_dir(@nonce_dir) self.ensure_dir(@association_dir) @@ -40,7 +39,7 @@ def get_association_filename(server_url, handle) handle_hash = '' end filename = [proto,domain,url_hash,handle_hash].join('-') - @association_dir.join(filename) + File.join(@association_dir, filename) end # Store an association in the assoc directory @@ -155,7 +154,7 @@ def use_nonce(server_url, timestamp, salt) nonce_fn = '%08x-%s-%s-%s-%s'%[timestamp, proto, domain, url_hash, salt_hash] - filename = @nonce_dir.join(nonce_fn) + filename = File.join(@nonce_dir, nonce_fn) begin fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0200) @@ -174,7 +173,7 @@ def cleanup end def cleanup_associations - association_filenames = Dir[@association_dir.join("*").to_s] + association_filenames = Dir[File.join(@association_dir, "*")] count = 0 association_filenames.each do |af| begin @@ -204,7 +203,7 @@ def cleanup_associations end def cleanup_nonces - nonces = Dir[@nonce_dir.join("*").to_s] + nonces = Dir[File.join(@nonce_dir, "*")] now = Time.now.to_i count = 0 From 90005f2634ac823ae57372c0ceb6548d509793b8 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 13:59:38 +0200 Subject: [PATCH 022/130] Fixed inconsistent server_url types. Closes #18 --- .../active_record_openid_store/XXX_add_open_id_store_to_db.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb b/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb index 99625453..28f980f1 100644 --- a/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +++ b/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb @@ -2,7 +2,7 @@ class AddOpenIdStoreToDb < ActiveRecord::Migration def self.up create_table "open_id_associations", :force => true do |t| - t.column "server_url", :binary, :null => false + t.column "server_url", :string, :null => false t.column "handle", :string, :null => false t.column "secret", :binary, :null => false t.column "issued", :integer, :null => false From 53d5386ee9712cb06673b2f4e16a8905b0014e4a Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 14:38:21 +0200 Subject: [PATCH 023/130] Fixed and tests for requiring signed AX attributes. Closes #32. --- lib/openid/extensions/ax.rb | 24 ++++++++--------- test/test_ax.rb | 54 ++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/lib/openid/extensions/ax.rb b/lib/openid/extensions/ax.rb index 4c8e899e..d52ccb94 100644 --- a/lib/openid/extensions/ax.rb +++ b/lib/openid/extensions/ax.rb @@ -38,7 +38,7 @@ def initialize # Raise an exception if the mode in the attribute exchange # arguments does not match what is expected for this class. def check_mode(ax_args) - actual_mode = ax_args['mode'] + actual_mode = ax_args ? ax_args['mode'] : nil if actual_mode != @mode raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}" end @@ -110,7 +110,7 @@ def self.to_type_uris(namespace_map, alias_list_s) class FetchRequest < AXMessage attr_reader :requested_attributes attr_accessor :update_url - + MODE = 'fetch_request' def initialize(update_url = nil) @@ -137,7 +137,7 @@ def get_extension_args aliases = NamespaceMap.new required = [] if_available = [] - ax_args = new_args + ax_args = new_args @requested_attributes.each{|type_uri, attribute| if attribute.ns_alias name = aliases.add_alias(type_uri, attribute.ns_alias) @@ -328,7 +328,7 @@ def parse_extension_args(ax_args) if count_s.nil? value = ax_args['value.'+name] if value.nil? - raise IndexError, "Missing #{'value.'+name} in FetchResponse" + raise IndexError, "Missing #{'value.'+name} in FetchResponse" elsif value.empty? values = [] else @@ -365,7 +365,7 @@ def get_single(type_uri, default = nil) def get(type_uri) @data[type_uri] end - + # retrieve the list of values for this attribute def [](type_uri) @data[type_uri] @@ -469,26 +469,26 @@ def self.from_success_response(success_response, signed=true) # A store request attribute exchange message representation class StoreRequest < KeyValueMessage - + MODE = 'store_request' - + def initialize super @mode = MODE end - + # Extract a StoreRequest from an OpenID message # message: OpenID::Message # return a StoreRequest or nil if AX arguments are not present def self.from_openid_request(oidreq) - message = oidreq.message + message = oidreq.message ax_args = message.get_args(NS_URI) return nil if ax_args.empty? or ax_args['mode'] != MODE req = new req.parse_extension_args(ax_args) req end - + def get_extension_args(aliases=nil) ax_args = new_args kv_args = _get_extension_kv_args(aliases) @@ -516,13 +516,13 @@ def initialize(succeeded = true, error_message = nil) end @error_message = error_message end - + def self.from_success_response(success_response) resp = nil ax_args = success_response.message.get_args(NS_URI) resp = ax_args.key?('error') ? new(false, ax_args['error']) : new end - + def succeeded? @mode == SUCCESS_MODE end diff --git a/test/test_ax.rb b/test/test_ax.rb index 2daaec66..47bdba1b 100644 --- a/test/test_ax.rb +++ b/test/test_ax.rb @@ -573,7 +573,7 @@ def test_get_single_extra assert_raises(Error) { @msg.get_single(@type_a) } end - def test_from_success_response + def test_from_unsigned_success_response uri = 'http://under.the.sea/' name = 'ext0' value = 'snarfblat' @@ -586,7 +586,7 @@ def test_from_success_response 'ax.mode' => 'fetch_response', 'ax.type.' + name => uri, 'ax.count.' + name => '1', - 'ax.value.' + name + '.1' => value, + 'ax.value.' + name + '.1' => value }) e = OpenID::OpenIDServiceEndpoint.new() @@ -598,7 +598,55 @@ def test_from_success_response assert_equal(values, [value]) end - def test_from_success_response_empty + def test_from_signed_success_response + uri = 'http://under.the.sea/' + name = 'ext0' + value = 'snarfblat' + oid_fields = { + 'mode' => 'id_res', + 'ns' => OPENID2_NS, + 'ns.ax' => AXMessage::NS_URI, + 'ax.update_url' => 'http://example.com/realm/update_path', + 'ax.mode' => 'fetch_response', + 'ax.type.' + name => uri, + 'ax.count.' + name => '1', + 'ax.value.' + name + '.1' => value + } + signed_fields = oid_fields.keys.map{|f| "openid.#{f}"} + + m = OpenID::Message.from_openid_args(oid_fields) + e = OpenID::OpenIDServiceEndpoint.new() + resp = OpenID::Consumer::SuccessResponse.new(e, m, signed_fields) + + ax_resp = FetchResponse.from_success_response(resp, true) + + values = ax_resp[uri] + assert_equal(values, [value]) + end + + def test_from_signed_success_response_with_unsigned_attributes + uri = 'http://under.the.sea/' + name = 'ext0' + value = 'snarfblat' + + m = OpenID::Message.from_openid_args({ + 'mode' => 'id_res', + 'ns' => OPENID2_NS, + 'ns.ax' => AXMessage::NS_URI, + 'ax.update_url' => 'http://example.com/realm/update_path', + 'ax.mode' => 'fetch_response', + 'ax.type.' + name => uri, + 'ax.count.' + name => '1', + 'ax.value.' + name + '.1' => value + }) + + e = OpenID::OpenIDServiceEndpoint.new() + resp = OpenID::Consumer::SuccessResponse.new(e, m, []) + + assert_nil FetchResponse.from_success_response(resp, true) + end + + def test_from_empty_success_response e = OpenID::OpenIDServiceEndpoint.new() m = OpenID::Message.from_openid_args({'mode' => 'id_res'}) resp = OpenID::Consumer::SuccessResponse.new(e, m, []) From aeaf050d21aeb681a220758f1cc61b9086f73152 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 14:50:09 +0200 Subject: [PATCH 024/130] register_namespace_alias for AX message. Closes #15. --- lib/openid/extensions/ax.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/openid/extensions/ax.rb b/lib/openid/extensions/ax.rb index d52ccb94..b8286596 100644 --- a/lib/openid/extensions/ax.rb +++ b/lib/openid/extensions/ax.rb @@ -27,6 +27,13 @@ class AXMessage < Extension attr_accessor :ns_alias, :mode, :ns_uri NS_URI = 'http://openid.net/srv/ax/1.0' + + begin + Message.register_namespace_alias(NS_URI, 'ax') + rescue NamespaceAliasRegistrationError => e + Util.log(e) + end + def initialize @ns_alias = 'ax' @ns_uri = NS_URI From 72d551945f9577bf5d0e516c673c648791b0e795 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Jun 2012 15:10:02 +0200 Subject: [PATCH 025/130] Bundler compatibility and bundler gem tasks. Closes #35. --- .gitignore | 17 +++++++++++++++++ Gemfile | 4 ++++ Rakefile | 2 ++ lib/openid.rb | 4 ++-- lib/openid/version.rb | 3 +++ ruby-openid.gemspec | 26 +++++++++++++++----------- 6 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 lib/openid/version.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d87d4be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..5f0daab6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org/' + +# Specify your gem's dependencies in ruby-openid.gemspec +gemspec diff --git a/Rakefile b/Rakefile index 0f6ca552..cd74a42a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,6 @@ #!/usr/bin/env rake +require 'bundler/gem_tasks' + require 'rake/testtask' desc "Run tests" diff --git a/lib/openid.rb b/lib/openid.rb index 1024de7c..55cbdd6d 100644 --- a/lib/openid.rb +++ b/lib/openid.rb @@ -13,8 +13,8 @@ # permissions and limitations under the License. module OpenID - VERSION = "2.1.8" end -require "openid/consumer" +require 'openid/version' +require 'openid/consumer' require 'openid/server' diff --git a/lib/openid/version.rb b/lib/openid/version.rb new file mode 100644 index 00000000..3358380f --- /dev/null +++ b/lib/openid/version.rb @@ -0,0 +1,3 @@ +module OpenID + VERSION = "2.1.8" +end \ No newline at end of file diff --git a/ruby-openid.gemspec b/ruby-openid.gemspec index 68435249..aec1dce8 100644 --- a/ruby-openid.gemspec +++ b/ruby-openid.gemspec @@ -1,21 +1,25 @@ -require 'rubygems' +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/openid/version', __FILE__) -SPEC = Gem::Specification.new do |s| - s.name = `cat admin/library-name`.strip -# s.version = `darcs changes --tags= | awk '$1 == "tagged" { print $2 }' | head -n 1`.strip - s.version = '2.1.8' +Gem::Specification.new do |s| + s.name = 'ruby-openid' s.author = 'JanRain, Inc' s.email = 'openid@janrain.com' - s.homepage = 'http://github.com/openid/ruby-openid' - s.platform = Gem::Platform::RUBY + s.homepage = 'https://github.com/openid/ruby-openid' s.summary = 'A library for consuming and serving OpenID identities.' + s.version = OpenID::VERSION + + # Files files = Dir.glob("{examples,lib,test}/**/*") files << 'NOTICE' << 'CHANGELOG' s.files = files.delete_if {|f| f.include?('_darcs') || f.include?('admin')} - s.require_path = 'lib' + s.require_paths = ['lib'] s.autorequire = 'openid' - s.test_file = 'admin/runtests.rb' + s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + s.test_files = s.files.grep(%r{^(test|spec|features)/}) + + # RDoc s.has_rdoc = true - s.extra_rdoc_files = ['README','INSTALL','LICENSE','UPGRADE'] - s.rdoc_options << '--main' << 'README' + s.extra_rdoc_files = ['README.md','INSTALL','LICENSE','UPGRADE'] + s.rdoc_options << '--main' << 'README.md' end From 6ba6817c0eba1b23d2c7993b23989aa573cc89b2 Mon Sep 17 00:00:00 2001 From: Cal Heldenbrand Date: Thu, 28 Jun 2012 13:40:01 -0500 Subject: [PATCH 026/130] Added tests for AX responses using a single value, and multiple array values --- lib/openid/extensions/ax.rb | 1 + test/test_ax.rb | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/openid/extensions/ax.rb b/lib/openid/extensions/ax.rb index 62d9ee09..4e3c0b2a 100644 --- a/lib/openid/extensions/ax.rb +++ b/lib/openid/extensions/ax.rb @@ -434,6 +434,7 @@ def get_extension_args(request = nil) if values.empty? # @data defaults to [] zero_value_types << attr_info end + if attr_info.count != UNLIMITED_VALUES and attr_info.count < values.size raise Error, "More than the number of requested values were specified for #{attr_info.type_uri.inspect}" end diff --git a/test/test_ax.rb b/test/test_ax.rb index 2d17c1f5..2d5020ee 100644 --- a/test/test_ax.rb +++ b/test/test_ax.rb @@ -486,6 +486,7 @@ class FetchResponseTest < Test::Unit::TestCase def setup @msg = FetchResponse.new @value_a = 'commodity' + @value_a1 = 'value2' @type_a = 'http://blood.transfusion/' @name_a = 'george' @request_update_url = 'http://some.url.that.is.awesome/' @@ -538,12 +539,13 @@ def test_update_url_in_response assert_equal(eargs, @msg.get_extension_args(req)) end - def test_get_extension_args_some_request + def test_get_extension_args_single_value_response + # Single values do NOT have a count, and + # do not use the array extension eargs = { 'mode' => 'fetch_response', 'type.' + @name_a => @type_a, - 'value.' + @name_a + '.1' => @value_a, - 'count.' + @name_a => '1' + 'value.' + @name_a => @value_a } req = FetchRequest.new req.add(AttrInfo.new(@type_a, @name_a)) @@ -551,6 +553,25 @@ def test_get_extension_args_some_request assert_equal(eargs, @msg.get_extension_args(req)) end + def test_get_extension_args_array_value_response + # Multiple array values add the count, and array index + # to each value + eargs = { + 'mode' => 'fetch_response', + 'type.' + @name_a => @type_a, + 'value.' + @name_a + ".1" => @value_a, + 'value.' + @name_a + ".2" => @value_a1, + 'count.' + @name_a => '2' + } + req = FetchRequest.new + # Specify that this URI should have a count of 2 + req.add(AttrInfo.new(@type_a, @name_a, true, 2)) + # Push both values onto the array + @msg.add_value(@type_a, @value_a) + @msg.add_value(@type_a, @value_a1) + assert_equal(eargs, @msg.get_extension_args(req)) + end + def test_get_extension_args_some_not_request req = FetchRequest.new @msg.add_value(@type_a, @value_a) From ae64c5a888bb55c11a03710fcfc4ae58c0facb72 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Sat, 7 Jul 2012 15:59:38 +0200 Subject: [PATCH 027/130] Docfile changes --- CHANGELOG | 215 ------------------------------------------ CHANGELOG.md | 13 +++ CHANGES-2.0.0 | 36 +++++++ INSTALL | 47 --------- INSTALL.md | 47 +++++++++ UPGRADE => UPGRADE.md | 109 +++++++++++---------- 6 files changed, 150 insertions(+), 317 deletions(-) delete mode 100644 CHANGELOG create mode 100644 CHANGELOG.md create mode 100644 CHANGES-2.0.0 delete mode 100644 INSTALL create mode 100644 INSTALL.md rename UPGRADE => UPGRADE.md (51%) diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index b088ff25..00000000 --- a/CHANGELOG +++ /dev/null @@ -1,215 +0,0 @@ -Mon Jan 23 12:48:00 PST 2006 brian@janrain.com - * fixed bug in expiresIn. added expired? method - - M ./lib/openid/filestore.rb -1 +1 - M ./lib/openid/stores.rb +4 - -Mon Jan 23 12:46:37 PST 2006 brian@janrain.com - * removed deps section from INSTALL file. deps are now included in lib because they are so small and to lower to bar of installing the library. - - M ./INSTALL -9 - -Tue Jan 17 14:45:57 PST 2006 brian@janrain.com - * added better handling of non-URL input - - M ./lib/openid/consumer.rb -1 +5 - -Sat Jan 14 19:39:57 PST 2006 brian@janrain.com - * added html and hmac deps into lib since they are so small - - A ./lib/hmac-md5.rb - A ./lib/hmac-rmd160.rb - A ./lib/hmac-sha1.rb - A ./lib/hmac-sha2.rb - A ./lib/hmac.rb - A ./lib/html/ - A ./lib/html/htmltokenizer.rb - -Mon Jan 16 15:04:05 PST 2006 Josh Hoyt - * Add script that will prepare the repository for release - - A ./admin/fixperms - A ./admin/prepare-release - -Mon Jan 16 14:35:27 PST 2006 Josh Hoyt - * Add custom boring file - - A ./admin/darcs-ignore - -Mon Jan 16 14:07:13 PST 2006 Josh Hoyt - * Put the build-docs script into the admin directory - - ./build-docs -> ./admin/build-docs - A ./admin/ - -Mon Jan 16 14:05:47 PST 2006 Josh Hoyt - * Add script to build documentation - - A ./build-docs - -Wed Jan 4 16:06:41 PST 2006 brian@janrain.com - tagged ruby-openid-0.9.2 - - -Wed Jan 4 16:02:32 PST 2006 brian@janrain.com - * added openid_login_generator rails generator to examples - - A ./examples/openid_login_generator/ - A ./examples/openid_login_generator/USAGE - A ./examples/openid_login_generator/openid_login_generator.rb - A ./examples/openid_login_generator/templates/ - A ./examples/openid_login_generator/templates/README - A ./examples/openid_login_generator/templates/controller.rb - A ./examples/openid_login_generator/templates/helper.rb - A ./examples/openid_login_generator/templates/login_system.rb - A ./examples/openid_login_generator/templates/user.rb - A ./examples/openid_login_generator/templates/view_login.rhtml - A ./examples/openid_login_generator/templates/view_logout.rhtml - A ./examples/openid_login_generator/templates/view_signup.rhtml - A ./examples/openid_login_generator/templates/view_welcome.rhtml - -Wed Jan 4 16:01:12 PST 2006 brian@janrain.com - * updated examples README to include openid_login_generator - - M ./examples/README +11 - -Wed Jan 4 14:58:24 PST 2006 brian@janrain.com - * added link to ruby library from consumer.rb example - - M ./examples/consumer.rb -1 +1 - -Wed Jan 4 10:56:45 PST 2006 brian@janrain.com - * ensure Content-type header is present for POSTs - - M ./lib/openid/fetchers.rb -1 +2 - -Fri Dec 30 17:05:25 PST 2005 brian@janrain.com - tagged ruby-openid-0.9.1 - - -Fri Dec 30 17:03:54 PST 2005 brian@janrain.com - * added Ruby on Rails example consumer - - M ./examples/README -1 +14 - A ./examples/openid_rails.tar.gz - -Thu Dec 29 16:00:20 PST 2005 brian@janrain.com - tagged ruby-openid-0.9.0 - - -Thu Dec 29 15:43:07 PST 2005 brian@janrain.com - * removed docs directory. generated rdoc html will be added manually to tarballs, and not be kept in repository - - R ./docs/ - R ./docs/README - -Thu Dec 29 15:21:21 PST 2005 brian@janrain.com - * added more docs for stores - - M ./TODO -2 +4 - M ./lib/openid/filestore.rb -16 +3 - M ./lib/openid/stores.rb -9 +1 - -Thu Dec 29 14:58:52 PST 2005 brian@janrain.com - * Huge documentation patch - - M ./INSTALL -12 +22 - M ./README -1 +1 - M ./lib/openid/consumer.rb -24 +370 - M ./lib/openid/fetchers.rb -2 +1 - M ./lib/openid/filestore.rb -6 +4 - M ./lib/openid/stores.rb -2 +1 - -Thu Dec 29 10:59:54 PST 2005 brian@janrain.com - * added more info and rdoc formatting to README - - M ./README -10 +26 - -Thu Dec 29 09:45:51 PST 2005 brian@janrain.com - * fixed bad comment - - M ./examples/consumer.rb -1 +1 - -Wed Dec 28 17:59:48 PST 2005 brian@janrain.com - * added platform agnositc temp dir discovery - - M ./examples/consumer.rb -1 +5 - -Wed Dec 28 17:13:21 PST 2005 brian@janrain.com - * moved getOpenIDParamerters to util - - M ./lib/openid/consumer.rb -10 +2 - M ./lib/openid/util.rb +8 - -Wed Dec 28 15:47:51 PST 2005 brian@janrain.com - * code cleanup - - M ./lib/openid/consumer.rb -5 - -Wed Dec 28 15:29:31 PST 2005 brian@janrain.com - * added linkparse to test suite script - - M ./test/runtests -1 +1 - -Wed Dec 28 15:29:07 PST 2005 brian@janrain.com - * added link parsing tests, lots of em - - A ./test/linkparse.rb - -Wed Dec 28 15:28:07 PST 2005 brian@janrain.com - * link parsing more robust: handle non-html data, and make sure link tag is in head - - M ./lib/openid/parse.rb -5 +13 - -Tue Dec 27 16:11:09 PST 2005 brian@janrain.com - * added more tests for openid/util - - M ./test/dh.rb -2 +1 - M ./test/runtests +1 - A ./test/util.rb - -Tue Dec 27 16:10:28 PST 2005 brian@janrain.com - * change util methods to use all use /dev/urandom if available - - M ./lib/openid/util.rb -15 +35 - -Tue Dec 27 16:09:53 PST 2005 brian@janrain.com - * changed tmp pathname to something more useful - - M ./examples/consumer.rb -1 +1 - -Fri Dec 16 09:04:59 PST 2005 Josh Hoyt - * Removed (now obsolete) interface.rb - - This has been subsumed by consumer.rb - - R ./lib/openid/interface.rb - -Thu Dec 15 18:25:04 PST 2005 brian@janrain.com - * initial checkin - - A ./COPYING - A ./INSTALL - A ./README - A ./TODO - A ./docs/ - A ./docs/README - A ./examples/ - A ./examples/README - A ./examples/consumer.rb - A ./lib/ - A ./lib/openid/ - A ./lib/openid/consumer.rb - A ./lib/openid/dh.rb - A ./lib/openid/fetchers.rb - A ./lib/openid/filestore.rb - A ./lib/openid/interface.rb - A ./lib/openid/parse.rb - A ./lib/openid/stores.rb - A ./lib/openid/util.rb - A ./setup.rb - A ./test/ - A ./test/assoc.rb - A ./test/dh.rb - A ./test/runtests - A ./test/teststore.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eb3430dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 2.2.0 + +* Bundler compatibility and bundler gem tasks - 72d551945f9577bf5d0e516c673c648791b0e795 +* register_namespace_alias for AX message - aeaf050d21aeb681a220758f1cc61b9086f73152 +* Fixed JRuby (1.9 mode) incompatibilty - 40baed6cf7326025058a131c2b76047345618539 +* Added UI extension support - a276a63d68639e985c1f327cf817489ccc5f9a17 +* Add attr_reader for setup_url on SetupNeededResponse - 75a7e98005542ede6db3fc7f1fc551e0a2ca044a +* Encode form inputs - c9e9b5b52f8a23df3159c2387b6330d5df40f35b +* Fixed cleanup AR associations whose expiry is past, not upcoming - 2265179a6d5c8b51ccc741180db46b618dd3caf9 +* Fixed issue with Memcache store and Dalli - ef84bf73da9c99c67b0632252bf0349e2360cbc7 +* Improvements to ActiveRecordStore's gc rake task - 847e19bf60a6b8163c1e0d2e96dbd805c64e2880 \ No newline at end of file diff --git a/CHANGES-2.0.0 b/CHANGES-2.0.0 new file mode 100644 index 00000000..4bca206f --- /dev/null +++ b/CHANGES-2.0.0 @@ -0,0 +1,36 @@ + +* API Changes + * PAPE (Provider Authentication Policy Extension) module + * Updated extension for specification draft 2 + * PAPE::Request::from_success_response returns nil if PAPE + response arguments were not signed + * Added functions to generate request/response HTML forms with + auto-submission javascript + * Consumer (relying party) API: + Auth_OpenID_AuthRequest::htmlMarkup + * Server API: Auth_OpenID_OpenIDResponse::toHTML + * Removed Rails login generator + * SReg::Response::from_success_response returns nil when no signed + arguments were found + +* New Features + * Fetchers now only read/request first megabyte of response + +* Bug fixes + * NOT NULL constraints to tables created by ActiveRecordStore + * check_authentication requests: copy entire response, not just + signed fields. Fixes missing namespace in check_authentication + requests + * OpenID 1 association requests no longer explicitly set + no-encryption session type + * Improved HTML parsing + * AssociationRequest::answer: include session_type in + no-encryption assoc responses + * normalize return_to URL before performing return_to verification + * OpenID::Consumer::IdResHandler.verify_discovery_results_openid1: + fall back to OpenID 1.0 type if 1.1 endpoint cannot be found + * StandardFetcher now includes a timeout setting + * Handle blank content types in + OpenID::Yadis::DiscoveryResult.where_is_yadis? + * Properly convert timestamps to ints before storing in DB, and vise + versa diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 89f1b9bd..00000000 --- a/INSTALL +++ /dev/null @@ -1,47 +0,0 @@ -= Ruby OpenID Library Installation - -== Rubygems Installation - -Rubygems is a tool for installing ruby libraries and their -dependancies. If you have rubygems installed, simply: - - gem install ruby-openid - -== Manual Installation - -Unpack the archive and run setup.rb to install: - - ruby setup.rb - -setup.rb installs the library into your system ruby. If don't want to -add openid to you system ruby, you may instead add the *lib* directory of -the extracted tarball to your RUBYLIB environment variable: - - $ export RUBYLIB=${RUBYLIB}:/path/to/ruby-openid/lib - - -== Testing the Installation - -Make sure everything installed ok: - $> irb - irb$> require "openid" - => true - -Or, if you installed via rubygems: - - $> irb - irb$> require "rubygems" - => true - irb$> require_gem "ruby-openid" - => true - -== Run the test suite - -Go into the test directory and execute the *runtests.rb* script. - -== Next steps - -* Run consumer.rb in the examples directory. -* Get started writing your own consumer using OpenID::Consumer -* Write your own server with OpenID::Server -* Use the OpenIDLoginGenerator! Read example/README for more info. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..79be34c5 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,47 @@ +# Ruby OpenID Library Installation + +## Rubygems Installation + +Rubygems is a tool for installing ruby libraries and their +dependancies. If you have rubygems installed, simply: + + gem install ruby-openid + +## Manual Installation + +Unpack the archive and run setup.rb to install: + + ruby setup.rb + +setup.rb installs the library into your system ruby. If don't want to +add openid to you system ruby, you may instead add the `lib` directory of +the extracted tarball to your RUBYLIB environment variable: + + $ export RUBYLIB=${RUBYLIB}:/path/to/ruby-openid/lib + +## Testing the Installation + +Make sure everything installed ok: + + $> irb + irb$> require "openid" + => true + +Or, if you installed via rubygems: + + $> irb + irb$> require "rubygems" + => true + irb$> require_gem "ruby-openid" + => true + +## Run the test suite + +Go into the test directory and execute the `runtests.rb` script. + +## Next steps + +* Run `consumer.rb` in the examples directory. +* Get started writing your own consumer using OpenID::Consumer +* Write your own server with `OpenID::Server` +* Use the `OpenIDLoginGenerator`! Read `example/README` for more info. diff --git a/UPGRADE b/UPGRADE.md similarity index 51% rename from UPGRADE rename to UPGRADE.md index b80e8232..6029878e 100644 --- a/UPGRADE +++ b/UPGRADE.md @@ -1,125 +1,124 @@ -= Upgrading from the OpenID 1.x series library +# Upgrading from the OpenID 1.x series library -== Consumer Upgrade +## Consumer Upgrade -The flow is largely the same, however there are a number of significant -changes. The consumer example is helpful to look at: -examples/rails_openid/app/controllers/consumer_controller.rb +The flow is largely the same, however there are a number of significant +changes. The consumer example is helpful to look at: +`examples/rails_openid/app/controllers/consumer_controller.rb` - -=== Stores +### Stores You will need to require the file for the store that you are using. For the filesystem store, this is 'openid/stores/filesystem' -They are also now in modules. The filesystem store is - OpenID::Store::Filesystem +They are also now in modules. The filesystem store is + `OpenID::Store::Filesystem` The format has changed, and you should remove your old store directory. -The ActiveRecord store ( examples/active_record_openid_store ) still needs +The ActiveRecord store (`examples/active_record_openid_store`) still needs to be put in a plugin directory for your rails app. There's a migration -that needs to be run; examine the README in that directory. +that needs to be run; examine the `README` in that directory. Also, note that the stores now can be garbage collected with the method - store.cleanup - + `store.cleanup` -=== Starting the OpenID transaction +### Starting the OpenID transaction The OpenIDRequest object no longer has status codes. Instead, consumer.begin raises an OpenID::OpenIDError if there is a problem initiating the transaction, so you'll want something along the lines of: - begin - openid_request = consumer.begin(params[:openid_identifier]) - rescue OpenID::OpenIDError => e - # display error e - return - end - #success case + begin + openid_request = consumer.begin(params[:openid_identifier]) + rescue OpenID::OpenIDError => e + # display error e + return + end + #success case Data regarding the OpenID server once lived in - openid_request.service + `openid_request.service` The corresponding object in the 2.0 lib can be retrieved with - openid_request.endpoint + `openid_request.endpoint` Getting the unverified identifier: Where you once had - openid_request.identity_url + `openid_request.identity_url` you will now want - openid_request.endpoint.claimed_id + `openid_request.endpoint.claimed_id` which might be different from what you get at the end of the transaction, since it is now possible for users to enter their server's url directly. Arguments on the return_to URL are now verified, so if you want to add additional arguments to the return_to url, use - openid_request.return_to_args['param'] = value + `openid_request.return_to_args['param'] = value` Generating the redirect is the same as before, but add any extensions first. If you need to set up an SSL certificate authority list for the fetcher, -use the 'ca_file' attr_accessor on the OpenID::StandardFetcher. This has -changed from 'ca_path' in the 1.x.x series library. That is, set -OpenID.fetcher.ca_file = '/path/to/ca.list' +use the 'ca_file' attr_accessor on the `OpenID::StandardFetcher`. This has +changed from 'ca_path' in the 1.x.x series library. That is, set +`OpenID.fetcher.ca_file = '/path/to/ca.list'` before calling consumer.begin. -=== Requesting Simple Registration Data +### Requesting Simple Registration Data You'll need to require the code for the extension - require 'openid/extensions/sreg' + + require 'openid/extensions/sreg' The new code for adding an SReg request now looks like: - sreg_request = OpenID::SReg::Request.new - sreg_request.request_fields(['email', 'dob'], true) # required - sreg_request.request_fields(['nickname', 'fullname'], false) # optional - sreg_request.policy_url = policy_url - openid_request.add_extension(sreg_request) + sreg_request = OpenID::SReg::Request.new + sreg_request.request_fields(['email', 'dob'], true) # required + sreg_request.request_fields(['nickname', 'fullname'], false) # optional + sreg_request.policy_url = policy_url + openid_request.add_extension(sreg_request) The code for adding other extensions is similar. Code for the Attribute Exchange (AX) and Provider Authentication Policy Extension (PAPE) are included with the library, and additional extensions can be implemented -subclassing OpenID::Extension. +subclassing `OpenID::Extension`. - -=== Completing the transaction +### Completing the transaction The return_to and its arguments are verified, so you need to pass in the base URL and the arguments. With Rails, the params method mashes together parameters from GET, POST, and the path, so you'll need to pull off the path "parameters" with something like - return_to = url_for(:only_path => false, - :controller => 'openid', - :action => 'complete') - parameters = params.reject{|k,v| request.path_parameters[k] } - openid_response = consumer.complete(parameters, return_to) + return_to = url_for(:only_path => false, + :controller => 'openid', + :action => 'complete') + parameters = params.reject{|k,v| request.path_parameters[k] } + openid_response = consumer.complete(parameters, return_to) The response still uses the status codes, but they are now namespaced -slightly differently, for example OpenID::Consumer::SUCCESS +slightly differently, for example `OpenID::Consumer::SUCCESS` In the case of failure, the error message is now found in - openid_response.message + `openid_response.message` The identifier to display to the user can be found in - openid_response.endpoint.display_identifier + `openid_response.endpoint.display_identifier` The Simple Registration response can be read from the OpenID response with - sreg_response = OpenID::SReg::Response.from_success_response(openid_response) - nickname = sreg_response['nickname'] - # etc. + sreg_response = OpenID::SReg::Response.from_success_response(openid_response) + nickname = sreg_response['nickname'] + # etc. -== Server Upgrade +## Server Upgrade The server code is mostly the same as before, with the exception of -extensions. Also, you must pass in the endpoint URL to the server +extensions. Also, you must pass in the endpoint URL to the server constructor: - @server = OpenID::Server.new(store, server_url) -I recommend looking at -examples/rails_openid/app/controllers/server_controller.rb + @server = OpenID::Server.new(store, server_url) + +I recommend looking at +`examples/rails_openid/app/controllers/server_controller.rb` for an example of the new way of doing extensions. -- From 37360a6aeb61aa49ee62af6dfbc205b55bf9d853 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Sat, 7 Jul 2012 16:01:26 +0200 Subject: [PATCH 028/130] Bumped version to 2.2.0 --- lib/openid/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/version.rb b/lib/openid/version.rb index 3358380f..072501a4 100644 --- a/lib/openid/version.rb +++ b/lib/openid/version.rb @@ -1,3 +1,3 @@ module OpenID - VERSION = "2.1.8" + VERSION = "2.2.0" end \ No newline at end of file From f7b92ff36835b3ca870f4cc4c2845b5013cfb3f7 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Sat, 7 Jul 2012 16:04:10 +0200 Subject: [PATCH 029/130] Added docfile changes to gemspec --- ruby-openid.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby-openid.gemspec b/ruby-openid.gemspec index aec1dce8..9458036a 100644 --- a/ruby-openid.gemspec +++ b/ruby-openid.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| # Files files = Dir.glob("{examples,lib,test}/**/*") - files << 'NOTICE' << 'CHANGELOG' + files << 'NOTICE' << 'CHANGELOG.md' s.files = files.delete_if {|f| f.include?('_darcs') || f.include?('admin')} s.require_paths = ['lib'] s.autorequire = 'openid' @@ -20,6 +20,6 @@ Gem::Specification.new do |s| # RDoc s.has_rdoc = true - s.extra_rdoc_files = ['README.md','INSTALL','LICENSE','UPGRADE'] + s.extra_rdoc_files = ['README.md', 'INSTALL.md', 'LICENSE', 'UPGRADE.md'] s.rdoc_options << '--main' << 'README.md' end From 2100f281172427d1557ebe76afbd24072a22d04f Mon Sep 17 00:00:00 2001 From: grosser Date: Wed, 26 Sep 2012 13:58:14 -0700 Subject: [PATCH 030/130] make bundle exec rake work --- Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 5f0daab6..b6f811a5 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org/' # Specify your gem's dependencies in ruby-openid.gemspec gemspec + +gem 'rake' From 2d5c3cd8f2476b28d60609822120c79d71919b7b Mon Sep 17 00:00:00 2001 From: grosser Date: Wed, 26 Sep 2012 14:00:22 -0700 Subject: [PATCH 031/130] state license in gemspec for automated tools / rubygems.org page --- ruby-openid.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/ruby-openid.gemspec b/ruby-openid.gemspec index 9458036a..a844f5b9 100644 --- a/ruby-openid.gemspec +++ b/ruby-openid.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/openid/ruby-openid' s.summary = 'A library for consuming and serving OpenID identities.' s.version = OpenID::VERSION + s.license = "Apache Software License" # Files files = Dir.glob("{examples,lib,test}/**/*") From a68d2591ac350459c874da10108e6ff5a8c08750 Mon Sep 17 00:00:00 2001 From: grosser Date: Wed, 26 Sep 2012 14:29:40 -0700 Subject: [PATCH 032/130] use default-external encoding instead of ascii for badly encoded pages + add tests for all cases of the encoding handling --- lib/openid/fetchers.rb | 10 ++++----- test/test_fetchers.rb | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/openid/fetchers.rb b/lib/openid/fetchers.rb index a7dff2c6..7c6376b4 100644 --- a/lib/openid/fetchers.rb +++ b/lib/openid/fetchers.rb @@ -238,15 +238,15 @@ def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) private def setup_encoding(response) - return unless defined?(::Encoding::ASCII_8BIT) - charset = response.type_params["charset"] - return if charset.nil? - encoding = nil + return unless defined?(Encoding.default_external) + return unless charset = response.type_params["charset"] + begin encoding = Encoding.find(charset) rescue ArgumentError end - encoding ||= Encoding::ASCII_8BIT + encoding ||= Encoding.default_external + body = response.body if body.respond_to?(:force_encoding) body.force_encoding(encoding) diff --git a/test/test_fetchers.rb b/test/test_fetchers.rb index 2e4554f8..b468a7e1 100644 --- a/test/test_fetchers.rb +++ b/test/test_fetchers.rb @@ -126,7 +126,28 @@ def _utf8_page } end + def _unencoded_page + lambda { |req, resp| + resp['Content-Type'] = "text/html" + body = "unencoded-body" + body.force_encoding("ASCII-8BIT") if body.respond_to?(:force_encoding) + resp.body = body + } + end + + def _badly_encoded_page + lambda { |req, resp| + resp['Content-Type'] = "text/html; charset=wtf" + body = "badly-encoded-body" + body.force_encoding("ASCII-8BIT") if body.respond_to?(:force_encoding) + resp.body = body + } + end + def setup + if defined?(Encoding.default_external) + @encoding_was = Encoding.default_external + end @fetcher = OpenID::StandardFetcher.new @logfile = StringIO.new @weblog = WEBrick::Log.new(logfile=@logfile) @@ -152,6 +173,8 @@ def setup @server.mount_proc('/post', _require_post) @server.mount_proc('/redirect_loop', _redirect_loop) @server.mount_proc('/utf8_page', _utf8_page) + @server.mount_proc('/unencoded_page', _unencoded_page) + @server.mount_proc('/badly_encoded_page', _badly_encoded_page) @server.start } @uri = _uri_build @@ -168,6 +191,9 @@ def _uri_build(path='/') end def teardown + if defined?(Encoding.default_external) + Encoding.default_external = @encoding_was + end @server.shutdown # Sleep a little because sometimes this blocks forever. @server_thread.join @@ -234,6 +260,30 @@ def test_utf8_page end end + def test_unencoded_page + if defined?(Encoding.default_external) + Encoding.default_external = Encoding::SHIFT_JIS + end + uri = _uri_build('/unencoded_page') + response = @fetcher.fetch(uri) + assert_equal("unencoded-body", response.body) + if defined?(Encoding.default_external) + assert_equal(Encoding::US_ASCII, response.body.encoding) + end + end + + def test_badly_encoded_page + if defined?(Encoding.default_external) + Encoding.default_external = Encoding::SHIFT_JIS + end + uri = _uri_build('/badly_encoded_page') + response = @fetcher.fetch(uri) + assert_equal("badly-encoded-body", response.body) + if defined?(Encoding.default_external) + assert_equal(Encoding::SHIFT_JIS, response.body.encoding) + end + end + def test_cases for path, expected_code, expected_url in @@cases uri = _uri_build(path) From d765772a5a8e297df55fdeb1834fa0e81d84dfc9 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 27 Sep 2012 11:27:49 +0200 Subject: [PATCH 033/130] Travis: Removed jruby-head and added rbx --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a1177b7..93018c4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby +script: "bundle exec rake" rvm: - 1.8.7 - 1.9.2 @@ -6,5 +7,5 @@ rvm: - ree - jruby-18mode - jruby-19mode - - jruby-head -script: rake + - rbx-18mode + - rbx-19mode From 4b0143f0a3b10060d5f52346954219bba3375039 Mon Sep 17 00:00:00 2001 From: grosser Date: Thu, 27 Sep 2012 08:29:09 -0700 Subject: [PATCH 034/130] colorize output and reveal tests that never ran --- Gemfile | 1 + test/test_idres.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index b6f811a5..0f4868e2 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ source 'https://rubygems.org/' gemspec gem 'rake' +gem 'test-unit', '>= 2.5.2' diff --git a/test/test_idres.rb b/test/test_idres.rb index 7940bf1e..88b79fc3 100644 --- a/test/test_idres.rb +++ b/test/test_idres.rb @@ -63,7 +63,7 @@ def mkMsg(ns, fields, signed_fields) [["openid1", OPENID1_NS, OPENID1_FIELDS], ["openid1", OPENID11_NS, OPENID1_FIELDS], ["openid2", OPENID2_NS, OPENID2_FIELDS], - ].each do |ver, ns, all_fields| + ].each_with_index do |(ver, ns, all_fields), i| all_fields.each do |field| test = lambda do fields = all_fields.dup @@ -74,7 +74,7 @@ def mkMsg(ns, fields, signed_fields) idres.send(:check_for_fields) } end - define_method("test_#{ver}_check_missing_#{field}", test) + define_method("test_#{i}_#{ver}_check_missing_#{field}", test) end end end @@ -84,7 +84,7 @@ def mkMsg(ns, fields, signed_fields) [["openid1", OPENID1_NS, OPENID1_FIELDS, OPENID1_SIGNED], ["openid1", OPENID11_NS, OPENID1_FIELDS, OPENID1_SIGNED], ["openid2", OPENID2_NS, OPENID2_FIELDS, OPENID2_SIGNED], - ].each do |ver, ns, all_fields, signed_fields| + ].each_with_index do |(ver, ns, all_fields, signed_fields), i| signed_fields.each do |signed_field| test = lambda do fields = signed_fields.dup @@ -97,7 +97,7 @@ def mkMsg(ns, fields, signed_fields) idres.send(:check_for_fields) } end - define_method("test_#{ver}_check_missing_signed_#{signed_field}", test) + define_method("test_#{i}_#{ver}_check_missing_signed_#{signed_field}", test) end end end From 791d02dec11f4cec9df80b3812369480f78ec7d5 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 27 Sep 2012 22:27:03 +0200 Subject: [PATCH 035/130] travis: removed rbx because the rbx-builds time out --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93018c4d..667ee61e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: ruby -script: "bundle exec rake" +script: rake rvm: - 1.8.7 - 1.9.2 @@ -7,5 +7,3 @@ rvm: - ree - jruby-18mode - jruby-19mode - - rbx-18mode - - rbx-19mode From 578d3b04e5c5aed873e1bc4fcd9540756431e6ba Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 27 Sep 2012 22:32:25 +0200 Subject: [PATCH 036/130] bumped version to 2.2.1 --- lib/openid/version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/openid/version.rb b/lib/openid/version.rb index 072501a4..495a91ce 100644 --- a/lib/openid/version.rb +++ b/lib/openid/version.rb @@ -1,3 +1,3 @@ module OpenID - VERSION = "2.2.0" -end \ No newline at end of file + VERSION = "2.2.1" +end From be2bab5c21f04735045e071411b349afb790078f Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 23 Oct 2012 01:16:28 +0900 Subject: [PATCH 037/130] limit fetching file size & disable XML entity expansion --- lib/openid/fetchers.rb | 17 ++++++++++++++--- lib/openid/yadis/xrds.rb | 34 ++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/openid/fetchers.rb b/lib/openid/fetchers.rb index 7c6376b4..2cd99f75 100644 --- a/lib/openid/fetchers.rb +++ b/lib/openid/fetchers.rb @@ -10,7 +10,7 @@ require 'net/http' end -MAX_RESPONSE_KB = 1024 +MAX_RESPONSE_KB = 10485760 # 10 MB (can be smaller, I guess) module Net class HTTP @@ -192,6 +192,16 @@ def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) conn = make_connection(url) response = nil + whole_body = '' + body_size_limitter = lambda do |r| + r.read_body do |partial| # read body now + whole_body << partial + if whole_body.length > MAX_RESPONSE_KB + raise FetchingError.new("Response Too Large") + end + end + whole_body + end response = conn.start { # Check the certificate against the URL's hostname if supports_ssl?(conn) and conn.use_ssl? @@ -199,12 +209,13 @@ def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) end if body.nil? - conn.request_get(url.request_uri, headers) + conn.request_get(url.request_uri, headers, &body_size_limitter) else headers["Content-type"] ||= "application/x-www-form-urlencoded" - conn.request_post(url.request_uri, body, headers) + conn.request_post(url.request_uri, body, headers, &body_size_limitter) end } + response.body = whole_body setup_encoding(response) rescue Timeout::Error => why raise FetchingError, "Error fetching #{url}: #{why}" diff --git a/lib/openid/yadis/xrds.rb b/lib/openid/yadis/xrds.rb index 0ebf34ab..5ac80a5e 100644 --- a/lib/openid/yadis/xrds.rb +++ b/lib/openid/yadis/xrds.rb @@ -88,23 +88,33 @@ class XRDSError < StandardError end def Yadis::parseXRDS(text) - if text.nil? - raise XRDSError.new("Not an XRDS document.") - end + disable_entity_expansion do + if text.nil? + raise XRDSError.new("Not an XRDS document.") + end - begin - d = REXML::Document.new(text) - rescue RuntimeError => why - raise XRDSError.new("Not an XRDS document. Failed to parse XML.") - end + begin + d = REXML::Document.new(text) + rescue RuntimeError => why + raise XRDSError.new("Not an XRDS document. Failed to parse XML.") + end - if is_xrds?(d) - return d - else - raise XRDSError.new("Not an XRDS document.") + if is_xrds?(d) + return d + else + raise XRDSError.new("Not an XRDS document.") + end end end + def Yadis::disable_entity_expansion + _previous_ = REXML::Document::entity_expansion_limit + REXML::Document::entity_expansion_limit = 0 + yield + ensure + REXML::Document::entity_expansion_limit = _previous_ + end + def Yadis::is_xrds?(xrds_tree) xrds_root = xrds_tree.root return (!xrds_root.nil? and From 3540a51e6f2f7fc7033f906fbd0a6c5153155e5a Mon Sep 17 00:00:00 2001 From: nov matake Date: Tue, 23 Oct 2012 14:37:48 +0900 Subject: [PATCH 038/130] Avoid using Net::HTTPResponse#body= It's not available before ruby 1.9.1 p378. Use OpenID::HTTPResponse instead. --- lib/openid/fetchers.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/openid/fetchers.rb b/lib/openid/fetchers.rb index 2cd99f75..db5443f0 100644 --- a/lib/openid/fetchers.rb +++ b/lib/openid/fetchers.rb @@ -215,8 +215,6 @@ def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) conn.request_post(url.request_uri, body, headers, &body_size_limitter) end } - response.body = whole_body - setup_encoding(response) rescue Timeout::Error => why raise FetchingError, "Error fetching #{url}: #{why}" rescue RuntimeError => why @@ -243,7 +241,10 @@ def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) raise FetchingError, "Error encountered in redirect from #{url}: #{why}" end else - return HTTPResponse._from_net_response(response, unparsed_url) + response = HTTPResponse._from_net_response(response, unparsed_url) + response.body = whole_body + setup_encoding(response) + return response end end From e152ab9adc5983f6741efa7c0f380462d34c82f0 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Tue, 23 Oct 2012 11:01:04 +0200 Subject: [PATCH 039/130] Bumped version to 2.2.2 and updated CHANGELOG --- CHANGELOG.md | 15 ++++++++++++++- lib/openid/version.rb | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3430dd..24deddba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2.2.2 + +* Limit fetching file size & disable XML entity expansion - be2bab5c21f04735045e071411b349afb790078f + + Avoid DoS attack to RPs using large XRDS / too many XML entity expansion in XRDS. + +## 2.2.1 + +* Make bundle exec rake work - 2100f281172427d1557ebe76afbd24072a22d04f +* State license in gemspec for automated tools / rubygems.org page - 2d5c3cd8f2476b28d60609822120c79d71919b7b +* Use default-external encoding instead of ascii for badly encoded pages - a68d2591ac350459c874da10108e6ff5a8c08750 +* Colorize output and reveal tests that never ran - 4b0143f0a3b10060d5f52346954219bba3375039 + ## 2.2.0 * Bundler compatibility and bundler gem tasks - 72d551945f9577bf5d0e516c673c648791b0e795 @@ -10,4 +23,4 @@ * Encode form inputs - c9e9b5b52f8a23df3159c2387b6330d5df40f35b * Fixed cleanup AR associations whose expiry is past, not upcoming - 2265179a6d5c8b51ccc741180db46b618dd3caf9 * Fixed issue with Memcache store and Dalli - ef84bf73da9c99c67b0632252bf0349e2360cbc7 -* Improvements to ActiveRecordStore's gc rake task - 847e19bf60a6b8163c1e0d2e96dbd805c64e2880 \ No newline at end of file +* Improvements to ActiveRecordStore's gc rake task - 847e19bf60a6b8163c1e0d2e96dbd805c64e2880 diff --git a/lib/openid/version.rb b/lib/openid/version.rb index 495a91ce..e11361a9 100644 --- a/lib/openid/version.rb +++ b/lib/openid/version.rb @@ -1,3 +1,3 @@ module OpenID - VERSION = "2.2.1" + VERSION = "2.2.2" end From beee5e8d1dc24ad55725cfcc720eefba6bdbd279 Mon Sep 17 00:00:00 2001 From: Jordan Phillips Date: Tue, 8 Jan 2013 14:35:47 -0800 Subject: [PATCH 040/130] Update starts/ends_with? to handle nil prefix --- lib/openid/extras.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/openid/extras.rb b/lib/openid/extras.rb index 0d9560ab..6b37bf39 100644 --- a/lib/openid/extras.rb +++ b/lib/openid/extras.rb @@ -1,10 +1,12 @@ class String def starts_with?(other) + other = other.to_s head = self[0, other.length] head == other end def ends_with?(other) + other = other.to_s tail = self[-1 * other.length, other.length] tail == other end From f032e949e1ca9078ab7508d9629398ca2c36980a Mon Sep 17 00:00:00 2001 From: Jordi Massaguer Pla Date: Wed, 6 Feb 2013 17:52:21 +0100 Subject: [PATCH 041/130] fix license information in gemspec According to the LICENSE file, the licenses are "ruby" and "apache license 2.0". I am updating the gemspec accordingly. --- ruby-openid.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby-openid.gemspec b/ruby-openid.gemspec index a844f5b9..c9e1881b 100644 --- a/ruby-openid.gemspec +++ b/ruby-openid.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/openid/ruby-openid' s.summary = 'A library for consuming and serving OpenID identities.' s.version = OpenID::VERSION - s.license = "Apache Software License" + s.licenses = ["Ruby", "Apache Software License 2.0"] # Files files = Dir.glob("{examples,lib,test}/**/*") From 0f46921a97677b83b106366c805063105c5e9f20 Mon Sep 17 00:00:00 2001 From: Zaid Zawaideh Date: Mon, 11 Feb 2013 14:17:32 -0500 Subject: [PATCH 042/130] added handling of invalide UTF-8 byte sequence exceptions --- lib/openid/consumer/html_parse.rb | 6 +++++- test/test_linkparse.rb | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/openid/consumer/html_parse.rb b/lib/openid/consumer/html_parse.rb index fca39456..222fc0b9 100644 --- a/lib/openid/consumer/html_parse.rb +++ b/lib/openid/consumer/html_parse.rb @@ -34,7 +34,11 @@ def OpenID.unescape_hash(h) def OpenID.parse_link_attrs(html) - stripped = html.gsub(REMOVED_RE,'') + begin + stripped = html.gsub(REMOVED_RE,'') + rescue ArgumentError + stripped = html.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(REMOVED_RE,'') + end parser = HTMLTokenizer.new(stripped) links = [] diff --git a/test/test_linkparse.rb b/test/test_linkparse.rb index 6360d507..ef19a9c2 100644 --- a/test/test_linkparse.rb +++ b/test/test_linkparse.rb @@ -84,7 +84,8 @@ def test_linkparse assert(false, "datafile parsing error: bad header #{h}") end } - links = OpenID::parse_link_attrs(html) + + links = OpenID::parse_link_attrs(html.force_encoding('UTF-8')) found = links.dup expected = expected_links.dup @@ -97,5 +98,12 @@ def test_linkparse end } assert_equal(numtests, testnum, "Number of tests") + + # test handling of invalid UTF-8 byte sequences + html = "hello joel\255".force_encoding("UTF-8") + assert_nothing_raised do + OpenID::parse_link_attrs(html) + end + end end From a647c12316e859dfbf2a10eb812f3d1d585baddb Mon Sep 17 00:00:00 2001 From: Zaid Date: Tue, 12 Feb 2013 14:06:58 -0500 Subject: [PATCH 043/130] Update to use 1.8 style hash --- lib/openid/consumer/html_parse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/consumer/html_parse.rb b/lib/openid/consumer/html_parse.rb index 222fc0b9..559cd4fe 100644 --- a/lib/openid/consumer/html_parse.rb +++ b/lib/openid/consumer/html_parse.rb @@ -37,7 +37,7 @@ def OpenID.parse_link_attrs(html) begin stripped = html.gsub(REMOVED_RE,'') rescue ArgumentError - stripped = html.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(REMOVED_RE,'') + stripped = html.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '').gsub(REMOVED_RE,'') end parser = HTMLTokenizer.new(stripped) From abdcf65e1e7c6cc58aded36c86db866384ae639b Mon Sep 17 00:00:00 2001 From: Zaid Date: Tue, 12 Feb 2013 14:34:48 -0500 Subject: [PATCH 044/130] fix problem in force_encoding in tests force_encoding doesn't exist in ruby 1.8. Pass string as is in 1.8 and only force_encoding if string responds to it --- test/test_linkparse.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_linkparse.rb b/test/test_linkparse.rb index ef19a9c2..dc9d394a 100644 --- a/test/test_linkparse.rb +++ b/test/test_linkparse.rb @@ -84,8 +84,8 @@ def test_linkparse assert(false, "datafile parsing error: bad header #{h}") end } - - links = OpenID::parse_link_attrs(html.force_encoding('UTF-8')) + html = html.force_encoding('UTF-8') if html.respond_to? :force_encoding + links = OpenID::parse_link_attrs(html) found = links.dup expected = expected_links.dup @@ -100,7 +100,8 @@ def test_linkparse assert_equal(numtests, testnum, "Number of tests") # test handling of invalid UTF-8 byte sequences - html = "hello joel\255".force_encoding("UTF-8") + html = "hello joel\255" + html = html.force_encoding('UTF-8') if html.respond_to? :force_encoding assert_nothing_raised do OpenID::parse_link_attrs(html) end From 542cac428d93aed3101677a591334650b8db1f4e Mon Sep 17 00:00:00 2001 From: Zaid Zawaideh Date: Tue, 12 Feb 2013 14:54:58 -0500 Subject: [PATCH 045/130] catch Encoding::UndefinedConversionError for compatibility with JRuby 1.9 mode --- lib/openid/consumer/html_parse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid/consumer/html_parse.rb b/lib/openid/consumer/html_parse.rb index 559cd4fe..2e12bb6c 100644 --- a/lib/openid/consumer/html_parse.rb +++ b/lib/openid/consumer/html_parse.rb @@ -36,7 +36,7 @@ def OpenID.unescape_hash(h) def OpenID.parse_link_attrs(html) begin stripped = html.gsub(REMOVED_RE,'') - rescue ArgumentError + rescue ArgumentError, Encoding::UndefinedConversionError stripped = html.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '').gsub(REMOVED_RE,'') end parser = HTMLTokenizer.new(stripped) From b1d0c38fe8dd6d64b58ce417ac20e76900807781 Mon Sep 17 00:00:00 2001 From: Zaid Zawaideh Date: Tue, 12 Feb 2013 15:23:36 -0500 Subject: [PATCH 046/130] jruby 1.9 mode still complaining about string encoding, try forcing it immediately --- test/test_linkparse.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test_linkparse.rb b/test/test_linkparse.rb index dc9d394a..9a504290 100644 --- a/test/test_linkparse.rb +++ b/test/test_linkparse.rb @@ -100,8 +100,11 @@ def test_linkparse assert_equal(numtests, testnum, "Number of tests") # test handling of invalid UTF-8 byte sequences - html = "hello joel\255" - html = html.force_encoding('UTF-8') if html.respond_to? :force_encoding + if "".respond_to? :force_encoding + html = "hello joel\255".force_encoding('UTF-8') + else + html = "hello joel\255" + end assert_nothing_raised do OpenID::parse_link_attrs(html) end From d3dca2faa653695cdaf2823ee6b9c4622a83ece3 Mon Sep 17 00:00:00 2001 From: Zaid Zawaideh Date: Tue, 12 Feb 2013 23:09:04 -0500 Subject: [PATCH 047/130] fixed issue with jruby in 1.9 mode not handling string encoding from binary properly. Now falling back to using ASCII as source --- lib/openid/consumer/html_parse.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/openid/consumer/html_parse.rb b/lib/openid/consumer/html_parse.rb index 2e12bb6c..e127dbef 100644 --- a/lib/openid/consumer/html_parse.rb +++ b/lib/openid/consumer/html_parse.rb @@ -36,8 +36,12 @@ def OpenID.unescape_hash(h) def OpenID.parse_link_attrs(html) begin stripped = html.gsub(REMOVED_RE,'') - rescue ArgumentError, Encoding::UndefinedConversionError - stripped = html.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '').gsub(REMOVED_RE,'') + rescue ArgumentError + begin + stripped = html.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '').gsub(REMOVED_RE,'') + rescue Encoding::UndefinedConversionError #needed for a problem in JRuby where it can't handle the conversion + stripped = html.encode('UTF-8', 'ASCII', :invalid => :replace, :undef => :replace, :replace => '').gsub(REMOVED_RE,'') + end end parser = HTMLTokenizer.new(stripped) From a32745778600f2163debf8712aed0d45f9418c10 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Wed, 13 Feb 2013 22:25:44 +0100 Subject: [PATCH 048/130] Bumped version to 2.2.3 and updated CHANGELOG --- CHANGELOG.md | 6 ++++++ lib/openid/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24deddba..2922fef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.2.3 + +* Fixed 'invalid byte sequence in UTF-8' error in parse_link_attrs - 0f46921a97677b83b106366c805063105c5e9f20 +* Fixed license information in gemspec - f032e949e1ca9078ab7508d9629398ca2c36980a +* Update starts/ends_with? to handle nil prefix - beee5e8d1dc24ad55725cfcc720eefba6bdbd279 + ## 2.2.2 * Limit fetching file size & disable XML entity expansion - be2bab5c21f04735045e071411b349afb790078f diff --git a/lib/openid/version.rb b/lib/openid/version.rb index e11361a9..1c743ec6 100644 --- a/lib/openid/version.rb +++ b/lib/openid/version.rb @@ -1,3 +1,3 @@ module OpenID - VERSION = "2.2.2" + VERSION = "2.2.3" end From 40356a16c7dfee1825800d66fe3ef2c25a617b08 Mon Sep 17 00:00:00 2001 From: "Marcel M. Cary" Date: Sun, 31 Mar 2013 07:18:51 -0700 Subject: [PATCH 049/130] Upgrade example Rails provider/consumer app to Rails 3 Upgrade the example Rails provider/consumer app to work on Rails 3: * Run rake rails:upgrade * Run rails new . * Remove script/* files (except for script/rails) * Rename templates * Update routes for Rails 3 * Install CSRF token in provider form * Exclude action and controller options from parameters when verifying return_to url * Cleanup a few unneeded files like .gitignore for unused directories and application layout that were generated for Rails 3 --- examples/rails_openid/Gemfile | 41 + examples/rails_openid/README.rdoc | 261 + examples/rails_openid/Rakefile | 11 +- .../rails_openid/app/assets/images/rails.png | Bin 0 -> 6646 bytes .../app/assets/javascripts/application.js | 15 + .../app/assets/stylesheets/application.css | 13 + .../app/controllers/application.rb | 4 - .../app/controllers/application_controller.rb | 3 + .../app/controllers/consumer_controller.rb | 1 + .../app/helpers/application_helper.rb | 1 - examples/rails_openid/app/mailers/.gitkeep | 0 examples/rails_openid/app/models/.gitkeep | 0 .../consumer/{index.rhtml => index.html.erb} | 0 .../layouts/{server.rhtml => server.html.erb} | 6 +- .../login/{index.rhtml => index.html.erb} | 0 .../server/{decide.rhtml => decide.html.erb} | 1 + examples/rails_openid/config.ru | 4 + examples/rails_openid/config/application.rb | 62 + examples/rails_openid/config/boot.rb | 21 +- examples/rails_openid/config/database.yml | 79 +- examples/rails_openid/config/environment.rb | 57 +- .../config/environments/development.rb | 46 +- .../config/environments/production.rb | 74 +- .../rails_openid/config/environments/test.rb | 48 +- .../initializers/backtrace_silencers.rb | 7 + .../config/initializers/inflections.rb | 15 + .../config/initializers/mime_types.rb | 5 + .../config/initializers/rails_root.rb | 1 + .../config/initializers/secret_token.rb | 7 + .../config/initializers/session_store.rb | 8 + .../config/initializers/wrap_parameters.rb | 14 + examples/rails_openid/config/locales/en.yml | 5 + examples/rails_openid/config/routes.rb | 83 +- examples/rails_openid/db/development.sqlite3 | 0 examples/rails_openid/db/seeds.rb | 7 + examples/rails_openid/doc/README_FOR_APP | 2 +- examples/rails_openid/lib/assets/.gitkeep | 0 examples/rails_openid/lib/tasks/.gitkeep | 0 examples/rails_openid/log/.gitkeep | 0 examples/rails_openid/log/development.log | 2052 ++++++++ examples/rails_openid/public/.htaccess | 40 - examples/rails_openid/public/404.html | 28 +- examples/rails_openid/public/422.html | 26 + examples/rails_openid/public/500.html | 27 +- .../public/javascripts/application.js | 2 + .../public/javascripts/controls.js | 959 ++-- .../public/javascripts/dragdrop.js | 761 ++- .../public/javascripts/effects.js | 1252 +++-- .../public/javascripts/prototype.js | 4305 +++++++++++++---- examples/rails_openid/public/robots.txt | 6 +- examples/rails_openid/script/about | 3 - examples/rails_openid/script/breakpointer | 3 - examples/rails_openid/script/console | 3 - examples/rails_openid/script/destroy | 3 - examples/rails_openid/script/generate | 3 - .../script/performance/benchmarker | 3 - .../rails_openid/script/performance/profiler | 3 - examples/rails_openid/script/plugin | 3 - examples/rails_openid/script/process/reaper | 3 - examples/rails_openid/script/process/spawner | 3 - examples/rails_openid/script/process/spinner | 3 - examples/rails_openid/script/rails | 6 + examples/rails_openid/script/runner | 3 - examples/rails_openid/script/server | 3 - examples/rails_openid/test/fixtures/.gitkeep | 0 .../rails_openid/test/functional/.gitkeep | 0 .../rails_openid/test/integration/.gitkeep | 0 .../test/performance/browsing_test.rb | 12 + examples/rails_openid/test/test_helper.rb | 29 +- examples/rails_openid/test/unit/.gitkeep | 0 70 files changed, 8192 insertions(+), 2254 deletions(-) create mode 100644 examples/rails_openid/Gemfile create mode 100644 examples/rails_openid/README.rdoc create mode 100644 examples/rails_openid/app/assets/images/rails.png create mode 100644 examples/rails_openid/app/assets/javascripts/application.js create mode 100644 examples/rails_openid/app/assets/stylesheets/application.css delete mode 100644 examples/rails_openid/app/controllers/application.rb create mode 100644 examples/rails_openid/app/controllers/application_controller.rb create mode 100644 examples/rails_openid/app/mailers/.gitkeep create mode 100644 examples/rails_openid/app/models/.gitkeep rename examples/rails_openid/app/views/consumer/{index.rhtml => index.html.erb} (100%) rename examples/rails_openid/app/views/layouts/{server.rhtml => server.html.erb} (93%) rename examples/rails_openid/app/views/login/{index.rhtml => index.html.erb} (100%) rename examples/rails_openid/app/views/server/{decide.rhtml => decide.html.erb} (88%) create mode 100644 examples/rails_openid/config.ru create mode 100644 examples/rails_openid/config/application.rb create mode 100644 examples/rails_openid/config/initializers/backtrace_silencers.rb create mode 100644 examples/rails_openid/config/initializers/inflections.rb create mode 100644 examples/rails_openid/config/initializers/mime_types.rb create mode 100644 examples/rails_openid/config/initializers/rails_root.rb create mode 100644 examples/rails_openid/config/initializers/secret_token.rb create mode 100644 examples/rails_openid/config/initializers/session_store.rb create mode 100644 examples/rails_openid/config/initializers/wrap_parameters.rb create mode 100644 examples/rails_openid/config/locales/en.yml create mode 100644 examples/rails_openid/db/development.sqlite3 create mode 100644 examples/rails_openid/db/seeds.rb create mode 100644 examples/rails_openid/lib/assets/.gitkeep create mode 100644 examples/rails_openid/lib/tasks/.gitkeep create mode 100644 examples/rails_openid/log/.gitkeep create mode 100644 examples/rails_openid/log/development.log delete mode 100644 examples/rails_openid/public/.htaccess create mode 100644 examples/rails_openid/public/422.html create mode 100644 examples/rails_openid/public/javascripts/application.js delete mode 100755 examples/rails_openid/script/about delete mode 100755 examples/rails_openid/script/breakpointer delete mode 100755 examples/rails_openid/script/console delete mode 100755 examples/rails_openid/script/destroy delete mode 100755 examples/rails_openid/script/generate delete mode 100755 examples/rails_openid/script/performance/benchmarker delete mode 100755 examples/rails_openid/script/performance/profiler delete mode 100755 examples/rails_openid/script/plugin delete mode 100755 examples/rails_openid/script/process/reaper delete mode 100755 examples/rails_openid/script/process/spawner delete mode 100755 examples/rails_openid/script/process/spinner create mode 100755 examples/rails_openid/script/rails delete mode 100755 examples/rails_openid/script/runner delete mode 100755 examples/rails_openid/script/server create mode 100644 examples/rails_openid/test/fixtures/.gitkeep create mode 100644 examples/rails_openid/test/functional/.gitkeep create mode 100644 examples/rails_openid/test/integration/.gitkeep create mode 100644 examples/rails_openid/test/performance/browsing_test.rb create mode 100644 examples/rails_openid/test/unit/.gitkeep diff --git a/examples/rails_openid/Gemfile b/examples/rails_openid/Gemfile new file mode 100644 index 00000000..0e331d37 --- /dev/null +++ b/examples/rails_openid/Gemfile @@ -0,0 +1,41 @@ +source 'https://rubygems.org/' + +gem 'rails', '3.2.13' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' + +gem 'sqlite3' + +gem 'json' + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + # gem 'therubyracer', :platforms => :ruby + + gem 'uglifier', '>= 1.0.3' +end + +gem 'jquery-rails' + +# To use ActiveModel has_secure_password +# gem 'bcrypt-ruby', '~> 3.0.0' + +# To use Jbuilder templates for JSON +# gem 'jbuilder' + +# Use unicorn as the app server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + +# To use debugger +# gem 'ruby-debug' + +gem 'ruby-openid' diff --git a/examples/rails_openid/README.rdoc b/examples/rails_openid/README.rdoc new file mode 100644 index 00000000..3e1c15c8 --- /dev/null +++ b/examples/rails_openid/README.rdoc @@ -0,0 +1,261 @@ +== Welcome to Rails + +Rails is a web-application framework that includes everything needed to create +database-backed web applications according to the Model-View-Control pattern. + +This pattern splits the view (also called the presentation) into "dumb" +templates that are primarily responsible for inserting pre-built data in between +HTML tags. The model contains the "smart" domain objects (such as Account, +Product, Person, Post) that holds all the business logic and knows how to +persist themselves to a database. The controller handles the incoming requests +(such as Save New Account, Update Product, Show Post) by manipulating the model +and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting Started + +1. At the command prompt, create a new Rails application: + rails new myapp (where myapp is the application name) + +2. Change directory to myapp and start the web server: + cd myapp; rails server (run with --help for options) + +3. Go to http://localhost:3000/ and you'll see: + "Welcome aboard: You're riding Ruby on Rails!" + +4. Follow the guidelines to start developing your application. You can find +the following resources handy: + +* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html +* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ + + +== Debugging Rails + +Sometimes your application goes wrong. Fortunately there are a lot of tools that +will help you debug it and get it back on the rails. + +First area to check is the application log files. Have "tail -f" commands +running on the server.log and development.log. Rails will automatically display +debugging and runtime information to these files. Debugging info will also be +shown in the browser on requests from 127.0.0.1. + +You can also log your own messages directly into the log file from your code +using the Ruby logger class from inside your controllers. Example: + + class WeblogController < ActionController::Base + def destroy + @weblog = Weblog.find(params[:id]) + @weblog.destroy + logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") + end + end + +The result will be a message in your log file along the lines of: + + Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! + +More information on how to use the logger is at http://www.ruby-doc.org/core/ + +Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are +several books available online as well: + +* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) +* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) + +These two books will bring you up to speed on the Ruby language and also on +programming in general. + + +== Debugger + +Debugger support is available through the debugger command when you start your +Mongrel or WEBrick server with --debugger. This means that you can break out of +execution at any point in the code, investigate and change the model, and then, +resume execution! You need to install ruby-debug to run the server in debugging +mode. With gems, use sudo gem install ruby-debug. Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.all + debugger + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the server window. Here you can do things like: + + >> @posts.inspect + => "[#nil, "body"=>nil, "id"=>"1"}>, + #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" + >> @posts.first.title = "hello from a debugger" + => "hello from a debugger" + +...and even better, you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you can enter "cont". + + +== Console + +The console is a Ruby shell, which allows you to interact with your +application's domain model. Here you'll have all parts of the application +configured, just like it is when the application is running. You can inspect +domain models, change values, and save to the database. Starting the script +without arguments will launch it in the development environment. + +To start the console, run rails console from the application +directory. + +Options: + +* Passing the -s, --sandbox argument will rollback any modifications + made to the database. +* Passing an environment name as an argument will load the corresponding + environment. Example: rails console production. + +To reload your controllers and models after launching the console run +reload! + +More information about irb can be found at: +link:http://www.rubycentral.org/pickaxe/irb.html + + +== dbconsole + +You can go to the command line of your database directly through rails +dbconsole. You would be connected to the database with the credentials +defined in database.yml. Starting the script without arguments will connect you +to the development database. Passing an argument will connect you to a different +database, like rails dbconsole production. Currently works for MySQL, +PostgreSQL and SQLite 3. + +== Description of Contents + +The default directory structure of a generated Ruby on Rails application: + + |-- app + | |-- assets + | | |-- images + | | |-- javascripts + | | `-- stylesheets + | |-- controllers + | |-- helpers + | |-- mailers + | |-- models + | `-- views + | `-- layouts + |-- config + | |-- environments + | |-- initializers + | `-- locales + |-- db + |-- doc + |-- lib + | |-- assets + | `-- tasks + |-- log + |-- public + |-- script + |-- test + | |-- fixtures + | |-- functional + | |-- integration + | |-- performance + | `-- unit + |-- tmp + | `-- cache + | `-- assets + `-- vendor + |-- assets + | |-- javascripts + | `-- stylesheets + `-- plugins + +app + Holds all the code that's specific to this particular application. + +app/assets + Contains subdirectories for images, stylesheets, and JavaScript files. + +app/controllers + Holds controllers that should be named like weblogs_controller.rb for + automated URL mapping. All controllers should descend from + ApplicationController which itself descends from ActionController::Base. + +app/models + Holds models that should be named like post.rb. Models descend from + ActiveRecord::Base by default. + +app/views + Holds the template files for the view that should be named like + weblogs/index.html.erb for the WeblogsController#index action. All views use + eRuby syntax by default. + +app/views/layouts + Holds the template files for layouts to be used with views. This models the + common header/footer method of wrapping views. In your views, define a layout + using the layout :default and create a file named default.html.erb. + Inside default.html.erb, call <% yield %> to render the view using this + layout. + +app/helpers + Holds view helpers that should be named like weblogs_helper.rb. These are + generated for you automatically when using generators for controllers. + Helpers can be used to wrap functionality for your views into methods. + +config + Configuration files for the Rails environment, the routing map, the database, + and other dependencies. + +db + Contains the database schema in schema.rb. db/migrate contains all the + sequence of Migrations for your schema. + +doc + This directory is where your application documentation will be stored when + generated using rake doc:app + +lib + Application specific libraries. Basically, any kind of custom code that + doesn't belong under controllers, models, or helpers. This directory is in + the load path. + +public + The directory available for the web server. Also contains the dispatchers and the + default HTML files. This should be set as the DOCUMENT_ROOT of your web + server. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. When using the rails generate + command, template test files will be generated for you and placed in this + directory. + +vendor + External libraries that the application depends on. Also includes the plugins + subdirectory. If the app has frozen rails, those gems also go here, under + vendor/rails/. This directory is in the load path. diff --git a/examples/rails_openid/Rakefile b/examples/rails_openid/Rakefile index cffd19f0..7e99b477 100644 --- a/examples/rails_openid/Rakefile +++ b/examples/rails_openid/Rakefile @@ -1,10 +1,7 @@ +#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake. +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require(File.join(File.dirname(__FILE__), 'config', 'boot')) +require File.expand_path('../config/application', __FILE__) -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' - -require 'tasks/rails' \ No newline at end of file +RailsOpenid::Application.load_tasks diff --git a/examples/rails_openid/app/assets/images/rails.png b/examples/rails_openid/app/assets/images/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..d5edc04e65f555e3ba4dcdaad39dc352e75b575e GIT binary patch literal 6646 zcmVpVcQya!6@Dsmj@#jv7C*qh zIhOJ6_K0n?*d`*T7TDuW-}m`9Kz3~>+7`DUkbAraU%yi+R{N~~XA2B%zt-4=tLimUer9!2M~N{G5bftFij_O&)a zsHnOppFIzebQ`RA0$!yUM-lg#*o@_O2wf422iLnM6cU(ktYU8#;*G!QGhIy9+ZfzKjLuZo%@a z-i@9A`X%J{^;2q&ZHY3C(B%gqCPW!8{9C0PMcNZccefK){s|V5-xxtHQc@uf>XqhD z7#N^siWqetgq29aX>G^olMf=bbRF6@Y(}zYxw6o!9WBdG1unP}<(V;zKlcR2p86fq zYjaqB^;Ycq>Wy@5T1xOzG3tucG3e%nPvajaN{CrFbnzv^9&K3$NrDm*eQe4`BGQ2bI;dFEwyt>hK%X!L6)82aOZp zsrGcJ#7PoX7)s|~t6is?FfX*7vWdREi58tiY4S)t6u*|kv?J)d_$r+CH#eZ?Ef+I_ z(eVlX8dh~4QP?o*E`_MgaNFIKj*rtN(0Raj3ECjSXcWfd#27NYs&~?t`QZFT}!Zaf=ldZIhi}LhQlqLo+o5(Pvui&{7PD__^53f9j>HW`Q z_V8X5j~$|GP9qXu0C#!@RX2}lXD35@3N5{BkUi%jtaPQ*H6OX2zIz4QPuqmTv3`vG{zc>l3t0B9E75h< z8&twGh%dp7WPNI+tRl%#gf2}Epg8st+~O4GjtwJsXfN;EjAmyr6z5dnaFU(;IV~QK zW62fogF~zA``(Q>_SmD!izc6Y4zq*97|NAPHp1j5X7Op2%;GLYm>^HEMyObo6s7l) zE3n|aOHi5~B84!}b^b*-aL2E)>OEJX_tJ~t<#VJ?bT?lDwyDB&5SZ$_1aUhmAY}#* zs@V1I+c5md9%R-o#_DUfqVtRk>59{+Opd5Yu%dAU#VQW}^m}x-30ftBx#527{^pI4 z6l2C6C7QBG$~NLYb3rVdLD#Z{+SleOp`(Lg5J}`kxdTHe(nV5BdpLrD=l|)e$gEqA zwI6vuX-PFCtcDIH>bGY2dwq&^tf+&R?)nY-@7_j%4CMRAF}C9w%p86W<2!aSY$p+k zrkFtG=cGo38RnrG28;?PNk%7a@faaXq&MS*&?1Z`7Ojw7(#>}ZG4nMAs3VXxfdW>i zY4VX02c5;f7jDPY_7@Oa)CHH}cH<3y#}_!nng^W+h1e-RL*YFYOteC@h?BtJZ+?sE zy)P5^8Mregx{nQaw1NY-|3>{Z)|0`?zc?G2-acYiSU`tj#sSGfm7k86ZQ0SQgPevcklHxM9<~4yW zR796sisf1|!#{Z=e^)0;_8iUhL8g(;j$l=02FTPZ(dZV@s#aQ`DHkLM6=YsbE4iQ!b#*374l0Jw5;jD%J;vQayq=nD8-kHI~f9Ux|32SJUM`> zGp2UGK*4t?cRKi!2he`zI#j0f${I#f-jeT?u_C7S4WsA0)ryi-1L0(@%pa^&g5x=e z=KW9+Nn(=)1T&S8g_ug%dgk*~l2O-$r9#zEGBdQsweO%t*6F4c8JC36JtTizCyy+E4h%G(+ z5>y$%0txMuQ$e~wjFgN(xrAndHQo`Za+K*?gUVDTBV&Ap^}|{w#CIq{DRe}+l@(Ec zCCV6f_?dY_{+f{}6XGn!pL_up?}@>KijT^$w#Lb6iHW&^8RP~g6y=vZBXx~B9nI^i zGexaPjcd(%)zGw!DG_dDwh-7x6+ST#R^${iz_M$uM!da8SxgB_;Z0G%Y*HpvLjKw; zX=ir7i1O$-T|*TBoH$dlW+TLf5j5sep^DlDtkox;Kg{Q%EXWedJq@J@%VAcK)j3y1 zShM!CS#qax;D@RND%2t3W6kv+#Ky0F9<3YKDbV^XJ=^$s(Vtza8V72YY)577nnldI zHMA0PUo!F3j(ubV*CM@PiK<^|RM2(DuCbG7`W}Rg(xdYC>C~ z;1KJGLN&$cRxSZunjXcntykmpFJ7;dk>shY(DdK&3K_JDJ6R%D`e~6Qv67@Rwu+q9 z*|NG{r}4F8f{Dfzt0+cZMd$fvlX3Q`dzM46@r?ISxr;9gBTG2rmfiGOD*#c*3f)cc zF+PFZobY$-^}J8 z%n=h4;x2}cP!@SiVd!v;^Wwo0(N??-ygDr7gG^NKxDjSo{5T{?$|Qo5;8V!~D6O;F*I zuY!gd@+2j_8Rn=UWDa#*4E2auWoGYDddMW7t0=yuC(xLWky?vLimM~!$3fgu!dR>p z?L?!8z>6v$|MsLb&dU?ob)Zd!B)!a*Z2eTE7 zKCzP&e}XO>CT%=o(v+WUY`Az*`9inbTG& z_9_*oQKw;sc8{ipoBC`S4Tb7a%tUE)1fE+~ib$;|(`|4QbXc2>VzFi%1nX%ti;^s3~NIL0R}!!a{0A zyCRp0F7Y&vcP&3`&Dzv5!&#h}F2R-h&QhIfq*ts&qO13{_CP}1*sLz!hI9VoTSzTu zok5pV0+~jrGymE~{TgbS#nN5+*rF7ij)cnSLQw0Ltc70zmk|O!O(kM<3zw-sUvkx~ z2`y+{xAwKSa-0}n7{$I@Zop7CWy%_xIeN1e-7&OjQ6vZZPbZ^3_ z(~=;ZSP98S2oB#35b1~_x`2gWiPdIVddEf`AD9<@c_s)TM;3J$T_l?pr{<7PTgdiy zBc5IGx)g~n=s+Z$RzYCmv8PlJu%gkh^;%mTGMc)UwRINVD~K;`Rl!5@hhGg;y>5qj zq|u-Yf0q_~Y+Mbivkkfa0nAOzB1acnytogsj_m7FB(-FjihMek#GAU4M!iXCgdK8a zjoKm?*|iz7;dHm4$^hh(`Ufl>yb>$hjIA-;>{>C}G0Di%bGvUsJkfLAV|xq32c>RqJqTBJ3Dx zYC;*Dt|S$b6)aCJFnK(Eey$M1DpVV~_MIhwK> zygo(jWC|_IRw|456`roEyXtkNLWNAt-4N1qyN$I@DvBzt;e|?g<*HK1%~cq|^u*}C zmMrwh>{QAq?Ar~4l^DqT%SQ)w)FA(#7#u+N;>E975rYML>)LgE`2<7nN=C1pC{IkV zVw}_&v6j&S?QVh*)wF3#XmE@0($^BVl1969csLKUBNer{suVd!a~B!0MxWY?=(GD6 zy$G&ERFR#i6G4=2F?R4}Mz3B?3tnpoX3)qFF2sh9-Jn*e%9F>i{WG7$_~XyOO2!+@ z6k+38KyD@-0=uee54D0!Z1@B^ilj~StchdOn(*qvg~s5QJpWGc!6U^Aj!xt-HZn_V zS%|fyQ5YS@EP2lBIodXCLjG_+a)%En+7jzngk@J>6D~^xbxKkvf-R0-c%mX+o{?&j zZZ%RxFeav8Y0gkwtdtrwUb-i0Egd2C=ADu%w5VV-hNJvl)GZ?M;y$!?b=S+wKRK7Q zcOjPT!p<*#8m;TsBih=@Xc&c)?Vy`Ys>IvK@|1%N+M6J-^RCRaZcPP2eQh9DEGZr+ z?8B~wF14mk4Xkuen{wY^CWwS1PI<8gikY*)3?RSo5l8es4*J z43k_BIwc}of=6Pfs%xIxlMDGOJN zvl!a>G)52XMqA%fbgkZi%)%bN*ZzZw2!rn4@+J)2eK#kWuEW{)W~-`y1vhA5-7p%R z&f5N!a9f8cK1Xa=O}=9{wg%}Ur^+8Y(!UCeqw>%wj@|bYHD-bZO~mk3L$9_^MmF3G zvCiK^e@q6G?tHkM8%GqsBMZaB20W$UEt_5r~jc#WlR>Bv{6W>A=!#InoY zLOd04@Rz?*7PpW8u|+}bt`?+Z(GsX{Br4A2$ZZ(26Degmr9`O=t2KgHTL*==R3xcP z&Y(J7hC@6_x8zVz!CX3l4Xtss6i7r#E6kXMNN1~>9KTRzewfp))ij%)SBBl0fZdYP zd!zzQD5u8yk-u|41|Rqz7_tCFUMThZJVj)yQf6^Cwtn|Ew6cm5J|u1Bq>MWX-AfB&NE;C z62@=-0le`E6-CurMKjoIy)BuUmhMGJb}pPx!@GLWMT+wH2R?wA=MEy)o57~feFp8P zY@YXAyt4<1FD<|iw{FGQu~GEI<4C64)V*QiVk+VzOV^9GWf4ir#oYgHJz!wq>iZV#_6@_{)&lum)4x z_Of*CLVQ7wdT#XT-(h0qH%mcIF7yzMIvvTN3bPceK>PpJi(=3Nny zbSn}p$dGKQUlX&-t~RR)#F7I<8NCD^yke(vdf#4^aAh}M-{tS9-&^tC4`KU_pToXy z+|K8sx}a)Kh{h{;*V1#hs1xB%(?j>)g~`Wv(9F)f=Qn)(daVB7hZtcp^#LrEr1T1J zZSJ*lVyVVjhy)mkex9Whn=EinKDHe@KlfQI-Fl7M?-c~HnW0;C;+MbUY8?FToy;A+ zs&Nc7VZ=Of+e!G6s#+S5WBU)kgQq_I1@!uH74GJ-+O|%0HXm9Mqlvp|j%0`T>fr9^ zK;qo>XdwZW<>%tTA+<(1^6(>=-2N;hRgBnjvEjN;VbKMbFg--WrGy|XESoH1p|M4` z86(gC^vB4qScASZ&cdpT{~QDN-jC|GJ(RYoW1VW4!SSn- zhQds9&RBKn6M&GVK_Aayt(Hekbnw=tr>f z^o@v9_*iQO1*zeOrts9Q-$pc@!StS&kz$cF`s@pM`rmJXTP&h5G)A74!0e%ZJbl}( zssI|_!%~_hZFypv*S^JE5N&Kvmx7KiG<|fGMO=WrH+@Yhuj+KwiS#l4>@%2nl zS)mDikfmokO4q2A)hRVZBq2-5q&XC>%HOLkOYxZ66(s86?=0s4z5xbiOV)}L-&6b)h6(~CIaR#JNw~46+WBiU7IhB zq!NuR4!TsYnyBg>@G=Ib*cMq^k<}AMpCeYEf&dzfiGI-wOQ7hb+nA zkN7_){y&c3xC0 AQ~&?~ literal 0 HcmV?d00001 diff --git a/examples/rails_openid/app/assets/javascripts/application.js b/examples/rails_openid/app/assets/javascripts/application.js new file mode 100644 index 00000000..9097d830 --- /dev/null +++ b/examples/rails_openid/app/assets/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +// GO AFTER THE REQUIRES BELOW. +// +//= require jquery +//= require jquery_ujs +//= require_tree . diff --git a/examples/rails_openid/app/assets/stylesheets/application.css b/examples/rails_openid/app/assets/stylesheets/application.css new file mode 100644 index 00000000..3192ec89 --- /dev/null +++ b/examples/rails_openid/app/assets/stylesheets/application.css @@ -0,0 +1,13 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require_tree . + */ diff --git a/examples/rails_openid/app/controllers/application.rb b/examples/rails_openid/app/controllers/application.rb deleted file mode 100644 index 537de40d..00000000 --- a/examples/rails_openid/app/controllers/application.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Filters added to this controller will be run for all controllers in the application. -# Likewise, all the methods added will be available for all controllers. -class ApplicationController < ActionController::Base -end \ No newline at end of file diff --git a/examples/rails_openid/app/controllers/application_controller.rb b/examples/rails_openid/app/controllers/application_controller.rb new file mode 100644 index 00000000..e8065d95 --- /dev/null +++ b/examples/rails_openid/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +class ApplicationController < ActionController::Base + protect_from_forgery +end diff --git a/examples/rails_openid/app/controllers/consumer_controller.rb b/examples/rails_openid/app/controllers/consumer_controller.rb index a42b2724..89c3a273 100644 --- a/examples/rails_openid/app/controllers/consumer_controller.rb +++ b/examples/rails_openid/app/controllers/consumer_controller.rb @@ -59,6 +59,7 @@ def complete # FIXME - url_for some action is not necessarily the current URL. current_url = url_for(:action => 'complete', :only_path => false) parameters = params.reject{|k,v|request.path_parameters[k]} + parameters.reject!{|k,v|%w{action controller}.include? k.to_s} oidresp = consumer.complete(parameters, current_url) case oidresp.status when OpenID::Consumer::FAILURE diff --git a/examples/rails_openid/app/helpers/application_helper.rb b/examples/rails_openid/app/helpers/application_helper.rb index 22a7940e..de6be794 100644 --- a/examples/rails_openid/app/helpers/application_helper.rb +++ b/examples/rails_openid/app/helpers/application_helper.rb @@ -1,3 +1,2 @@ -# Methods added to this helper will be available to all templates in the application. module ApplicationHelper end diff --git a/examples/rails_openid/app/mailers/.gitkeep b/examples/rails_openid/app/mailers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/rails_openid/app/models/.gitkeep b/examples/rails_openid/app/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/rails_openid/app/views/consumer/index.rhtml b/examples/rails_openid/app/views/consumer/index.html.erb similarity index 100% rename from examples/rails_openid/app/views/consumer/index.rhtml rename to examples/rails_openid/app/views/consumer/index.html.erb diff --git a/examples/rails_openid/app/views/layouts/server.rhtml b/examples/rails_openid/app/views/layouts/server.html.erb similarity index 93% rename from examples/rails_openid/app/views/layouts/server.rhtml rename to examples/rails_openid/app/views/layouts/server.html.erb index 3dd5d786..bcfef512 100644 --- a/examples/rails_openid/app/views/layouts/server.rhtml +++ b/examples/rails_openid/app/views/layouts/server.html.erb @@ -1,5 +1,7 @@ - OpenID Server Example + OpenID Server Example + <%#= csrf_meta_tags %> + + + -

File not found

-

Change this error message for pages not found in public/404.html

+ +
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
- \ No newline at end of file + diff --git a/examples/rails_openid/public/422.html b/examples/rails_openid/public/422.html new file mode 100644 index 00000000..83660ab1 --- /dev/null +++ b/examples/rails_openid/public/422.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (422) + + + + + +
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+ + diff --git a/examples/rails_openid/public/500.html b/examples/rails_openid/public/500.html index a1001a00..f3648a0d 100644 --- a/examples/rails_openid/public/500.html +++ b/examples/rails_openid/public/500.html @@ -1,8 +1,25 @@ - + + + We're sorry, but something went wrong (500) + + + -

Application error (Apache)

-

Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html

+ +
+

We're sorry, but something went wrong.

+
- \ No newline at end of file + diff --git a/examples/rails_openid/public/javascripts/application.js b/examples/rails_openid/public/javascripts/application.js new file mode 100644 index 00000000..fe457769 --- /dev/null +++ b/examples/rails_openid/public/javascripts/application.js @@ -0,0 +1,2 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults diff --git a/examples/rails_openid/public/javascripts/controls.js b/examples/rails_openid/public/javascripts/controls.js index 9742b691..ca29aefd 100644 --- a/examples/rails_openid/public/javascripts/controls.js +++ b/examples/rails_openid/public/javascripts/controls.js @@ -1,21 +1,22 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) -// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava // Rob Wills -// -// See scriptaculous.js for full license. +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ -// Autocompleter.Base handles all the autocompletion functionality +// Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This // includes drawing the autocompletion menu, observing keyboard // and mouse events, and similar. // -// Specific autocompleters need to provide, at the very least, +// Specific autocompleters need to provide, at the very least, // a getUpdatedChoices function that will be invoked every time -// the text inside the monitored textbox changes. This method +// the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by // invoking this.getToken(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized @@ -29,62 +30,71 @@ // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with // a token array, e.g. { tokens: [',', '\n'] } which -// enables autocompletion on multiple tokens. This is most -// useful when one of the tokens is \n (a newline), as it +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. -var Autocompleter = {} -Autocompleter.Base = function() {}; -Autocompleter.Base.prototype = { +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { }; +Autocompleter.Base = Class.create({ baseInitialize: function(element, update, options) { - this.element = $(element); - this.update = $(update); - this.hasFocus = false; - this.changed = false; - this.active = false; - this.index = 0; + element = $(element); + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; this.entryCount = 0; + this.oldElementValue = this.element.value; - if (this.setOptions) + if(this.setOptions) this.setOptions(options); else - this.options = options || {}; + this.options = options || { }; this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; this.options.minChars = this.options.minChars || 1; - this.options.onShow = this.options.onShow || - function(element, update){ - if(!update.style.position || update.style.position=='absolute') { - update.style.position = 'absolute'; - Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); - } - Effect.Appear(update,{duration:0.15}); - }; - this.options.onHide = this.options.onHide || - function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; - if (typeof(this.options.tokens) == 'string') + if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); this.observer = null; - + this.element.setAttribute('autocomplete','off'); Element.hide(this.update); - Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); - Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); - if(!this.iefix && - (navigator.appVersion.indexOf('MSIE')>0) && - (navigator.userAgent.indexOf('Opera')<0) && + if(!this.iefix && + (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { - new Insertion.After(this.update, + new Insertion.After(this.update, ''); @@ -92,9 +102,9 @@ Autocompleter.Base.prototype = { } if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); }, - + fixIEOverlapping: function() { - Position.clone(this.update, this.iefix); + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); this.iefix.style.zIndex = 1; this.update.style.zIndex = 2; Element.show(this.iefix); @@ -132,58 +142,63 @@ Autocompleter.Base.prototype = { case Event.KEY_UP: this.markPrevious(); this.render(); - if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + Event.stop(event); return; case Event.KEY_DOWN: this.markNext(); this.render(); - if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + Event.stop(event); return; } - else - if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) - return; + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; this.changed = true; this.hasFocus = true; if(this.observer) clearTimeout(this.observer); - this.observer = + this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + onHover: function(event) { var element = Event.findElement(event, 'LI'); - if(this.index != element.autocompleteIndex) + if(this.index != element.autocompleteIndex) { this.index = element.autocompleteIndex; this.render(); } Event.stop(event); }, - + onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; this.selectEntry(); this.hide(); }, - + onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); this.hasFocus = false; - this.active = false; - }, - + this.active = false; + }, + render: function() { if(this.entryCount > 0) { for (var i = 0; i < this.entryCount; i++) - this.index==i ? - Element.addClassName(this.getEntry(i),"selected") : + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : Element.removeClassName(this.getEntry(i),"selected"); - - if(this.hasFocus) { + if(this.hasFocus) { this.show(); this.active = true; } @@ -192,25 +207,27 @@ Autocompleter.Base.prototype = { this.hide(); } }, - + markPrevious: function() { - if(this.index > 0) this.index-- + if(this.index > 0) this.index--; else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); }, - + markNext: function() { - if(this.index < this.entryCount-1) this.index++ + if(this.index < this.entryCount-1) this.index++; else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); }, - + getEntry: function(index) { return this.update.firstChild.childNodes[index]; }, - + getCurrentEntry: function() { return this.getEntry(this.index); }, - + selectEntry: function() { this.active = false; this.updateElement(this.getCurrentEntry()); @@ -221,20 +238,26 @@ Autocompleter.Base.prototype = { this.options.updateElement(selectedElement); return; } - - var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); - var lastTokenPos = this.findLastToken(); - if (lastTokenPos != -1) { - var newValue = this.element.value.substr(0, lastTokenPos + 1); - var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); if (whitespace) newValue += whitespace[0]; - this.element.value = newValue + value; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); } else { this.element.value = value; } + this.oldElementValue = this.element.value; this.element.focus(); - + if (this.options.afterUpdateElement) this.options.afterUpdateElement(this.element, selectedElement); }, @@ -243,24 +266,29 @@ Autocompleter.Base.prototype = { if(!this.changed && this.hasFocus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); - Element.cleanWhitespace(this.update.firstChild); + Element.cleanWhitespace(this.update.down()); - if(this.update.firstChild && this.update.firstChild.childNodes) { - this.entryCount = - this.update.firstChild.childNodes.length; + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; for (var i = 0; i < this.entryCount; i++) { var entry = this.getEntry(i); entry.autocompleteIndex = i; this.addObservers(entry); } - } else { + } else { this.entryCount = 0; } this.stopIndicator(); - this.index = 0; - this.render(); + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } } }, @@ -270,42 +298,51 @@ Autocompleter.Base.prototype = { }, onObserverEvent: function() { - this.changed = false; + this.changed = false; + this.tokenBounds = null; if(this.getToken().length>=this.options.minChars) { - this.startIndicator(); this.getUpdatedChoices(); } else { this.active = false; this.hide(); } + this.oldElementValue = this.element.value; }, getToken: function() { - var tokenPos = this.findLastToken(); - if (tokenPos != -1) - var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); - else - var ret = this.element.value; - - return /\n/.test(ret) ? '' : ret; - }, - - findLastToken: function() { - var lastTokenPos = -1; - - for (var i=0; i lastTokenPos) - lastTokenPos = thisTokenPos; + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; } - return lastTokenPos; + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); } -} +}); -Ajax.Autocompleter = Class.create(); -Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { initialize: function(element, update, url, options) { - this.baseInitialize(element, update, options); + this.baseInitialize(element, update, options); this.options.asynchronous = true; this.options.onComplete = this.onComplete.bind(this); this.options.defaultParams = this.options.parameters || null; @@ -313,13 +350,15 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro }, getUpdatedChoices: function() { - entry = encodeURIComponent(this.options.paramName) + '=' + + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + encodeURIComponent(this.getToken()); this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; - if(this.options.defaultParams) + if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; new Ajax.Request(this.url, this.options); @@ -328,7 +367,6 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro onComplete: function(request) { this.updateChoices(request.responseText); } - }); // The local array autocompleter. Used when you'd prefer to @@ -344,7 +382,7 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro // - choices - How many autocompletion choices to offer // // - partialSearch - If false, the autocompleter will match entered -// text only at the beginning of strings in the +// text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to @@ -361,13 +399,12 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro // - ignoreCase - Whether to ignore case when autocompleting. // Defaults to true. // -// It's possible to pass in a custom function as the 'selector' +// It's possible to pass in a custom function as the 'selector' // option, if you prefer to write your own autocompletion logic. // In that case, the other options above will not apply unless // you support them. -Autocompleter.Local = Class.create(); -Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { +Autocompleter.Local = Class.create(Autocompleter.Base, { initialize: function(element, update, array, options) { this.baseInitialize(element, update, options); this.options.array = array; @@ -390,20 +427,20 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { var entry = instance.getToken(); var count = 0; - for (var i = 0; i < instance.options.array.length && - ret.length < instance.options.choices ; i++) { + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { var elem = instance.options.array[i]; - var foundPos = instance.options.ignoreCase ? - elem.toLowerCase().indexOf(entry.toLowerCase()) : + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); while (foundPos != -1) { - if (foundPos == 0 && elem.length != entry.length) { - ret.push("
  • " + elem.substr(0, entry.length) + "" + + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + elem.substr(entry.length) + "
  • "); break; - } else if (entry.length >= instance.options.partialChars && + } else if (entry.length >= instance.options.partialChars && instance.options.partialSearch && foundPos != -1) { if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { partial.push("
  • " + elem.substr(0, foundPos) + "" + @@ -413,23 +450,22 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { } } - foundPos = instance.options.ignoreCase ? - elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : elem.indexOf(entry, foundPos + 1); } } if (partial.length) - ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); return "
      " + ret.join('') + "
    "; } - }, options || {}); + }, options || { }); } }); -// AJAX in-place editor -// -// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve (April 2007). // Use this if you notice weird scrolling problems on some browsers, // the DOM might be a bit confused when this gets called so do this @@ -438,303 +474,480 @@ Field.scrollFreeActivate = function(field) { setTimeout(function() { Field.activate(field); }, 1); -} +}; -Ajax.InPlaceEditor = Class.create(); -Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; -Ajax.InPlaceEditor.prototype = { +Ajax.InPlaceEditor = Class.create({ initialize: function(element, url, options) { this.url = url; - this.element = $(element); - - this.options = Object.extend({ - okText: "ok", - cancelText: "cancel", - savingText: "Saving...", - clickToEditText: "Click to edit", - okText: "ok", - rows: 1, - onComplete: function(transport, element) { - new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); - }, - onFailure: function(transport) { - alert("Error communicating with the server: " + transport.responseText.stripTags()); - }, - callback: function(form) { - return Form.serialize(form); - }, - handleLineBreaks: true, - loadingText: 'Loading...', - savingClassName: 'inplaceeditor-saving', - loadingClassName: 'inplaceeditor-loading', - formClassName: 'inplaceeditor-form', - highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, - highlightendcolor: "#FFFFFF", - externalControl: null, - ajaxOptions: {} - }, options || {}); - - if(!this.options.formId && this.element.id) { - this.options.formId = this.element.id + "-inplaceeditor"; - if ($(this.options.formId)) { - // there's already a form with that name, don't specify an id - this.options.formId = null; - } + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; } - - if (this.options.externalControl) { + if (this.options.externalControl) this.options.externalControl = $(this.options.externalControl); - } - - this.originalBackground = Element.getStyle(this.element, 'background-color'); - if (!this.originalBackground) { - this.originalBackground = "transparent"; - } - + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; this.element.title = this.options.clickToEditText; - - this.onclickListener = this.enterEditMode.bindAsEventListener(this); - this.mouseoverListener = this.enterHover.bindAsEventListener(this); - this.mouseoutListener = this.leaveHover.bindAsEventListener(this); - Event.observe(this.element, 'click', this.onclickListener); - Event.observe(this.element, 'mouseover', this.mouseoverListener); - Event.observe(this.element, 'mouseout', this.mouseoutListener); - if (this.options.externalControl) { - Event.observe(this.options.externalControl, 'click', this.onclickListener); - Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); - Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; } }, - enterEditMode: function(evt) { - if (this.saving) return; - if (this.editing) return; - this.editing = true; - this.onEnterEditMode(); - if (this.options.externalControl) { - Element.hide(this.options.externalControl); - } - Element.hide(this.element); - this.createForm(); - this.element.parentNode.insertBefore(this.form, this.element); - Field.scrollFreeActivate(this.editField); - // stop the event to avoid a page refresh in Safari - if (evt) { - Event.stop(evt); + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; } - return false; + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); }, createForm: function() { - this.form = document.createElement("form"); - this.form.id = this.options.formId; - Element.addClassName(this.form, this.options.formClassName) - this.form.onsubmit = this.onSubmit.bind(this); - + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; this.createEditField(); - - if (this.options.textarea) { - var br = document.createElement("br"); - this.form.appendChild(br); - } - - okButton = document.createElement("input"); - okButton.type = "submit"; - okButton.value = this.options.okText; - this.form.appendChild(okButton); - - cancelLink = document.createElement("a"); - cancelLink.href = "#"; - cancelLink.appendChild(document.createTextNode(this.options.cancelText)); - cancelLink.onclick = this.onclickCancel.bind(this); - this.form.appendChild(cancelLink); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); }, - hasHTMLLineBreaks: function(string) { - if (!this.options.handleLineBreaks) return false; - return string.match(/
    /i); + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); }, - convertHTMLLineBreaks: function(string) { - return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + getText: function() { + return this.element.innerHTML.unescapeHTML(); }, - createEditField: function() { - var text; - if(this.options.loadTextURL) { - text = this.options.loadingText; - } else { - text = this.getText(); + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; } - - if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { - this.options.textarea = false; - var textField = document.createElement("input"); - textField.type = "text"; - textField.name = "value"; - textField.value = text; - textField.style.backgroundColor = this.options.highlightcolor; - var size = this.options.size || this.options.cols || 0; - if (size != 0) textField.size = size; - this.editField = textField; + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); } else { - this.options.textarea = true; - var textArea = document.createElement("textarea"); - textArea.name = "value"; - textArea.value = this.convertHTMLLineBreaks(text); - textArea.rows = this.options.rows; - textArea.cols = this.options.cols || 40; - this.editField = textArea; - } - - if(this.options.loadTextURL) { - this.loadExternalText(); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); } - this.form.appendChild(this.editField); + if (e) Event.stop(e); }, - getText: function() { - return this.element.innerHTML; + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); }, loadExternalText: function() { - Element.addClassName(this.form, this.options.loadingClassName); - this.editField.disabled = true; - new Ajax.Request( - this.options.loadTextURL, - Object.extend({ - asynchronous: true, - onComplete: this.onLoadedExternalText.bind(this) - }, this.options.ajaxOptions) - ); - }, - onLoadedExternalText: function(transport) { - Element.removeClassName(this.form, this.options.loadingClassName); - this.editField.disabled = false; - this.editField.value = transport.responseText.stripTags(); - }, - onclickCancel: function() { - this.onComplete(); - this.leaveEditMode(); - return false; - }, - onFailure: function(transport) { - this.options.onFailure(transport); - if (this.oldInnerHTML) { - this.element.innerHTML = this.oldInnerHTML; - this.oldInnerHTML = null; - } - return false; - }, - onSubmit: function() { - // onLoading resets these so we need to save them away for the Ajax call - var form = this.form; - var value = this.editField.value; - - // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... - // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... - // to be displayed indefinitely - this.onLoading(); - - new Ajax.Updater( - { - success: this.element, - // don't update on failure (this could be an option) - failure: null - }, - this.url, - Object.extend({ - parameters: this.options.callback(form, value), - onComplete: this.onComplete.bind(this), - onFailure: this.onFailure.bind(this) - }, this.options.ajaxOptions) - ); - // stop the event to avoid a page refresh in Safari - if (arguments.length > 1) { - Event.stop(arguments[0]); - } - return false; - }, - onLoading: function() { - this.saving = true; + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; this.removeForm(); this.leaveHover(); this.showSaving(); }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, showSaving: function() { - this.oldInnerHTML = this.element.innerHTML; + this._oldInnerHTML = this.element.innerHTML; this.element.innerHTML = this.options.savingText; - Element.addClassName(this.element, this.options.savingClassName); - this.element.style.backgroundColor = this.originalBackground; - Element.show(this.element); + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); }, - removeForm: function() { - if(this.form) { - if (this.form.parentNode) Element.remove(this.form); - this.form = null; + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); } }, - enterHover: function() { - if (this.saving) return; - this.element.style.backgroundColor = this.options.highlightcolor; - if (this.effect) { - this.effect.cancel(); - } - Element.addClassName(this.element, this.options.hoverClassName) + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); }, - leaveHover: function() { - if (this.options.backgroundColor) { - this.element.style.backgroundColor = this.oldBackground; - } - Element.removeClassName(this.element, this.options.hoverClassName) - if (this.saving) return; - this.effect = new Effect.Highlight(this.element, { - startcolor: this.options.highlightcolor, - endcolor: this.options.highlightendcolor, - restorecolor: this.originalBackground + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw('Server returned an invalid collection representation.'); + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure }); + new Ajax.Request(this.options.loadCollectionURL, options); }, - leaveEditMode: function() { - Element.removeClassName(this.element, this.options.savingClassName); - this.removeForm(); - this.leaveHover(); - this.element.style.backgroundColor = this.originalBackground; - Element.show(this.element); - if (this.options.externalControl) { - Element.show(this.options.externalControl); + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; } - this.editing = false; - this.saving = false; - this.oldInnerHTML = null; - this.onLeaveEditMode(); + tempOption.update((text || '').stripScripts().stripTags()); }, - onComplete: function(transport) { - this.leaveEditMode(); - this.options.onComplete.bind(this)(transport, this.element); + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); }, - onEnterEditMode: function() {}, - onLeaveEditMode: function() {}, - dispose: function() { - if (this.oldInnerHTML) { - this.element.innerHTML = this.oldInnerHTML; - } - this.leaveEditMode(); - Event.stopObserving(this.element, 'click', this.onclickListener); - Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); - Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); - if (this.options.externalControl) { - Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); - Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); - Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' }; -// Delayed observer, like Form.Element.Observer, +// Delayed observer, like Form.Element.Observer, // but waits for delay after last key input // Ideal for live-search fields -Form.Element.DelayedObserver = Class.create(); -Form.Element.DelayedObserver.prototype = { +Form.Element.DelayedObserver = Class.create({ initialize: function(element, delay, callback) { this.delay = delay || 0.5; this.element = $(element); this.callback = callback; this.timer = null; - this.lastValue = $F(this.element); + this.lastValue = $F(this.element); Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); }, delayedListener: function(event) { @@ -747,4 +960,4 @@ Form.Element.DelayedObserver.prototype = { this.timer = null; this.callback(this.element, $F(this.element)); } -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/examples/rails_openid/public/javascripts/dragdrop.js b/examples/rails_openid/public/javascripts/dragdrop.js index 92d1f731..07229f98 100644 --- a/examples/rails_openid/public/javascripts/dragdrop.js +++ b/examples/rails_openid/public/javascripts/dragdrop.js @@ -1,8 +1,11 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// -// See scriptaculous.js for full license. +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ -/*--------------------------------------------------------------------------*/ +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { drops: [], @@ -15,21 +18,21 @@ var Droppables = { element = $(element); var options = Object.extend({ greedy: true, - hoverclass: null - }, arguments[1] || {}); + hoverclass: null, + tree: false + }, arguments[1] || { }); // cache containers if(options.containment) { options._containers = []; var containment = options.containment; - if((typeof containment == 'object') && - (containment.constructor == Array)) { + if(Object.isArray(containment)) { containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); } } - + if(options.accept) options.accept = [options.accept].flatten(); Element.makePositioned(element); // fix IE @@ -38,9 +41,24 @@ var Droppables = { this.drops.push(options); }, + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + isContained: function(element, drop) { - var parentNode = element.parentNode; - return drop._containers.detect(function(c) { return parentNode == c }); + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); }, isAffected: function(point, element, drop) { @@ -49,7 +67,7 @@ var Droppables = { ((!drop._containers) || this.isContained(element, drop)) && ((!drop.accept) || - (Element.classNames(element).detect( + (Element.classNames(element).detect( function(v) { return drop.accept.include(v) } ) )) && Position.within(drop.element, point[0], point[1]) ); }, @@ -68,18 +86,24 @@ var Droppables = { show: function(point, element) { if(!this.drops.length) return; - - if(this.last_active) this.deactivate(this.last_active); + var drop, affected = []; + this.drops.each( function(drop) { - if(Droppables.isAffected(point, element, drop)) { - if(drop.onHover) - drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); - if(drop.greedy) { - Droppables.activate(drop); - throw $break; - } - } + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } }, fire: function(event, element) { @@ -87,33 +111,35 @@ var Droppables = { Position.prepare(); if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) - if (this.last_active.onDrop) + if (this.last_active.onDrop) { this.last_active.onDrop(element, this.last_active.element, event); + return true; + } }, reset: function() { if(this.last_active) this.deactivate(this.last_active); } -} +}; var Draggables = { drags: [], observers: [], - + register: function(draggable) { if(this.drags.length == 0) { this.eventMouseUp = this.endDrag.bindAsEventListener(this); this.eventMouseMove = this.updateDrag.bindAsEventListener(this); this.eventKeypress = this.keyPress.bindAsEventListener(this); - + Event.observe(document, "mouseup", this.eventMouseUp); Event.observe(document, "mousemove", this.eventMouseMove); Event.observe(document, "keypress", this.eventKeypress); } this.drags.push(draggable); }, - + unregister: function(draggable) { this.drags = this.drags.reject(function(d) { return d==draggable }); if(this.drags.length == 0) { @@ -122,16 +148,24 @@ var Draggables = { Event.stopObserving(document, "keypress", this.eventKeypress); } }, - + activate: function(draggable) { - window.focus(); // allows keypress events if window isn't currently focused, fails for Safari - this.activeDraggable = draggable; + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } }, - - deactivate: function(draggbale) { + + deactivate: function() { this.activeDraggable = null; }, - + updateDrag: function(event) { if(!this.activeDraggable) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; @@ -139,37 +173,44 @@ var Draggables = { // the same coordinates, prevent needless redrawing (moz bug?) if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); }, - + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } if(!this.activeDraggable) return; this._lastPointer = null; this.activeDraggable.endDrag(event); + this.activeDraggable = null; }, - + keyPress: function(event) { if(this.activeDraggable) this.activeDraggable.keyPress(event); }, - + addObserver: function(observer) { this.observers.push(observer); this._cacheObserverCallbacks(); }, - + removeObserver: function(element) { // element instead of observer fixes mem leaks this.observers = this.observers.reject( function(o) { return o.element==element }); this._cacheObserverCallbacks(); }, - + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' if(this[eventName+'Count'] > 0) this.observers.each( function(o) { if(o[eventName]) o[eventName](eventName, draggable, event); }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); }, - + _cacheObserverCallbacks: function() { ['onStart','onEnd','onDrag'].each( function(eventName) { Draggables[eventName+'Count'] = Draggables.observers.select( @@ -177,134 +218,214 @@ var Draggables = { ).length; }); } -} +}; /*--------------------------------------------------------------------------*/ -var Draggable = Class.create(); -Draggable.prototype = { +var Draggable = Class.create({ initialize: function(element) { - var options = Object.extend({ + var defaults = { handle: false, - starteffect: function(element) { - new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); - }, reverteffect: function(element, top_offset, left_offset) { var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; - element._revert = new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); }, - endeffect: function(element) { - new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); }, zindex: 1000, revert: false, - snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } - }, arguments[1] || {}); + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); this.element = $(element); - - if(options.handle && (typeof options.handle == 'string')) - this.handle = Element.childrenWithClassName(this.element, options.handle)[0]; + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = this.element; - Element.makePositioned(this.element); // fix IE + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE - this.delta = this.currentDelta(); this.options = options; - this.dragging = false; + this.dragging = false; this.eventMouseDown = this.initDrag.bindAsEventListener(this); Event.observe(this.handle, "mousedown", this.eventMouseDown); - + Draggables.register(this); }, - + destroy: function() { Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); Draggables.unregister(this); }, - + currentDelta: function() { return([ - parseInt(this.element.style.left || '0'), - parseInt(this.element.style.top || '0')]); + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); }, - + initDrag: function(event) { - if(Event.isLeftClick(event)) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); - if(src.tagName && ( - src.tagName=='INPUT' || - src.tagName=='SELECT' || - src.tagName=='BUTTON' || - src.tagName=='TEXTAREA')) return; - - if(this.element._revert) { - this.element._revert.cancel(); - this.element._revert = null; - } - + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pos = Position.cumulativeOffset(this.element); this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); - + Draggables.activate(this); Event.stop(event); } }, - + startDrag: function(event) { this.dragging = true; - + if(!this.delta) + this.delta = this.currentDelta(); + if(this.options.zindex) { this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); this.element.style.zIndex = this.options.zindex; } - + if(this.options.ghosting) { this._clone = this.element.cloneNode(true); - Position.absolutize(this.element); + this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this._originallyAbsolute) + Position.absolutize(this.element); this.element.parentNode.insertBefore(this._clone, this.element); } - + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + Draggables.notify('onStart', this, event); + if(this.options.starteffect) this.options.starteffect(this.element); }, - + updateDrag: function(event, pointer) { if(!this.dragging) this.startDrag(event); - Position.prepare(); - Droppables.show(pointer, this.element); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + Draggables.notify('onDrag', this, event); + this.draw(pointer); if(this.options.change) this.options.change(this); - + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + // fix AppleWebKit rendering - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + Event.stop(event); }, - + finishDrag: function(event, success) { this.dragging = false; + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + if(this.options.ghosting) { - Position.relativize(this.element); + if (!this._originallyAbsolute) + Position.relativize(this.element); + delete this._originallyAbsolute; Element.remove(this._clone); this._clone = null; } - if(success) Droppables.fire(event, this.element); + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; - if(revert && typeof revert == 'function') revert = revert(this.element); - + if(revert && Object.isFunction(revert)) revert = revert(this.element); + var d = this.currentDelta(); if(revert && this.options.reverteffect) { - this.options.reverteffect(this.element, - d[1]-this.delta[1], d[0]-this.delta[0]); + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); } else { this.delta = d; } @@ -312,111 +433,223 @@ Draggable.prototype = { if(this.options.zindex) this.element.style.zIndex = this.originalZ; - if(this.options.endeffect) + if(this.options.endeffect) this.options.endeffect(this.element); Draggables.deactivate(this); Droppables.reset(); }, - + keyPress: function(event) { - if(!event.keyCode==Event.KEY_ESC) return; + if(event.keyCode!=Event.KEY_ESC) return; this.finishDrag(event, false); Event.stop(event); }, - + endDrag: function(event) { if(!this.dragging) return; + this.stopScrolling(); this.finishDrag(event, true); Event.stop(event); }, - + draw: function(point) { var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + var d = this.currentDelta(); pos[0] -= d[0]; pos[1] -= d[1]; - - var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this)); - + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + if(this.options.snap) { - if(typeof this.options.snap == 'function') { - p = this.options.snap(p[0],p[1]); + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); } else { - if(this.options.snap instanceof Array) { + if(Object.isArray(this.options.snap)) { p = p.map( function(v, i) { - return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); } else { p = p.map( function(v) { - return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + return (v/this.options.snap).round()*this.options.snap }.bind(this)); } }} - + var style = this.element.style; if((!this.options.constraint) || (this.options.constraint=='horizontal')) style.left = p[0] + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight; + } + } + return { top: T, left: L, width: W, height: H }; } -} +}); + +Draggable._dragging = { }; /*--------------------------------------------------------------------------*/ -var SortableObserver = Class.create(); -SortableObserver.prototype = { +var SortableObserver = Class.create({ initialize: function(element, observer) { this.element = $(element); this.observer = observer; this.lastValue = Sortable.serialize(this.element); }, - + onStart: function() { this.lastValue = Sortable.serialize(this.element); }, - + onEnd: function() { Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } -} +}); var Sortable = { - sortables: new Array(), - - options: function(element){ - element = $(element); - return this.sortables.detect(function(s) { return s.element == element }); + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; }, - + destroy: function(element){ element = $(element); - this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + var s = Sortable.sortables[element.id]; + + if(s) { Draggables.removeObserver(s.element); s.droppables.each(function(d){ Droppables.remove(d) }); s.draggables.invoke('destroy'); - }); - this.sortables = this.sortables.reject(function(s) { return s.element == element }); + + delete Sortable.sortables[s.element.id]; + } }, - + create: function(element) { element = $(element); - var options = Object.extend({ + var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' dropOnEmpty: false, - tree: false, // fixme: unimplemented + tree: false, + treeTag: 'ul', overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, + delay: 0, hoverclass: null, ghosting: false, - format: null, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction - }, arguments[1] || {}); + }, arguments[1] || { }); // clear any old sortable with same element this.destroy(element); @@ -424,6 +657,11 @@ var Sortable = { // build options for the draggables var options_for_draggable = { revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, ghosting: options.ghosting, constraint: options.constraint, handle: options.handle }; @@ -445,42 +683,54 @@ var Sortable = { if(options.zindex) options_for_draggable.zindex = options.zindex; - // build options for the droppables + // build options for the droppables var options_for_droppable = { overlap: options.overlap, containment: options.containment, + tree: options.tree, hoverclass: options.hoverclass, - onHover: Sortable.onHover, - greedy: !options.dropOnEmpty - } + onHover: Sortable.onHover + }; + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + }; // fix for gecko engine - Element.cleanWhitespace(element); + Element.cleanWhitespace(element); options.draggables = []; options.droppables = []; - // make it so - // drop on empty handling - if(options.dropOnEmpty) { - Droppables.add(element, - {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); options.droppables.push(element); } - (this.findElements(element, options) || []).each( function(e) { - // handles are per-draggable - var handle = options.handle ? - Element.childrenWithClassName(e, options.handle)[0] : e; + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); - options.droppables.push(e); + if(options.tree) e.treeNode = element; + options.droppables.push(e); }); + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + // keep reference - this.sortables.push(options); + this.sortables[element.id] = options; // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); @@ -489,29 +739,27 @@ var Sortable = { // return all suitable-for-sortable elements in a guaranteed order findElements: function(element, options) { - if(!element.hasChildNodes()) return null; - var elements = []; - $A(element.childNodes).each( function(e) { - if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() && - (!options.only || (Element.hasClassName(e, options.only)))) - elements.push(e); - if(options.tree) { - var grandchildren = this.findElements(e, options); - if(grandchildren) elements.push(grandchildren); - } - }); + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, - return (elements.length>0 ? elements.flatten() : null); + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); }, onHover: function(element, dropon, overlap) { - if(overlap>0.5) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { Sortable.mark(dropon, 'before'); if(dropon.previousSibling != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, dropon); - if(dropon.parentNode!=oldParentNode) + if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } @@ -522,63 +770,204 @@ var Sortable = { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, nextElement); - if(dropon.parentNode!=oldParentNode) + if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } }, - onEmptyHover: function(element, dropon) { - if(element.parentNode!=dropon) { - var oldParentNode = element.parentNode; - dropon.appendChild(element); + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + Sortable.options(oldParentNode).onChange(element); - Sortable.options(dropon).onChange(element); + droponOptions.onChange(element); } }, unmark: function() { - if(Sortable._marker) Element.hide(Sortable._marker); + if(Sortable._marker) Sortable._marker.hide(); }, mark: function(dropon, position) { // mark on ghosting only var sortable = Sortable.options(dropon.parentNode); - if(sortable && !sortable.ghosting) return; + if(sortable && !sortable.ghosting) return; if(!Sortable._marker) { - Sortable._marker = $('dropmarker') || document.createElement('DIV'); - Element.hide(Sortable._marker); - Element.addClassName(Sortable._marker, 'dropmarker'); - Sortable._marker.style.position = 'absolute'; + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); - } + } var offsets = Position.cumulativeOffset(dropon); - Sortable._marker.style.left = offsets[0] + 'px'; - Sortable._marker.style.top = offsets[1] + 'px'; - + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + if(position=='after') - if(sortable.overlap == 'horizontal') - Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); else - Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; - - Element.show(Sortable._marker); + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); }, - serialize: function(element) { + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + }; + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child); + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ - tag: sortableOptions.tag, + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, only: sortableOptions.only, name: element.id, - format: sortableOptions.format || /^[^_]*_(.*)$/ - }, arguments[1] || {}); + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + }; + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + return $(this.findElements(element, options) || []).map( function(item) { - return (encodeURIComponent(options.name) + "[]=" + - encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : '')); - }).join("&"); + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } } -} \ No newline at end of file +}; + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +}; + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +}; + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +}; \ No newline at end of file diff --git a/examples/rails_openid/public/javascripts/effects.js b/examples/rails_openid/public/javascripts/effects.js index 414398ce..5a639d2d 100644 --- a/examples/rails_openid/public/javascripts/effects.js +++ b/examples/rails_openid/public/javascripts/effects.js @@ -1,110 +1,120 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) // Martin Bialasinki -// -// See scriptaculous.js for full license. - -/* ------------- element ext -------------- */ - -// converts rgb() and #xxx to #xxxxxx format, -// returns self (or first argument) if not convertable -String.prototype.parseColor = function() { - var color = '#'; - if(this.slice(0,4) == 'rgb(') { - var cols = this.slice(4,this.length-1).split(','); - var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); - } else { - if(this.slice(0,1) == '#') { - if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); - if(this.length==7) color = this.toLowerCase(); - } - } - return(color.length==7 ? color : (arguments[0] || this)); -} - -Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { - var children = $(element).childNodes; - var text = ''; - var classtest = new RegExp('^([^ ]+ )*' + ignoreclass+ '( [^ ]+)*$','i'); - - for (var i = 0; i < children.length; i++) { - if(children[i].nodeType==3) { - text+=children[i].nodeValue; - } else { - if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) - text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); - } - } - - return text; -} +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ -Element.setStyle = function(element, style) { - element = $(element); - for(k in style) element.style[k.camelize()] = style[k]; -} +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; -Element.setContentZoom = function(element, percent) { - Element.setStyle(element, {fontSize: (percent/100) + 'em'}); - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); -} +/*--------------------------------------------------------------------------*/ -Element.getOpacity = function(element){ - var opacity; - if (opacity = Element.getStyle(element, 'opacity')) - return parseFloat(opacity); - if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) - if(opacity[1]) return parseFloat(opacity[1]) / 100; - return 1.0; -} +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; -Element.setOpacity = function(element, value){ - element= $(element); - if (value == 1){ - Element.setStyle(element, { opacity: - (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? - 0.999999 : null }); - if(/MSIE/.test(navigator.userAgent)) - Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); - } else { - if(value < 0.00001) value = 0; - Element.setStyle(element, {opacity: value}); - if(/MSIE/.test(navigator.userAgent)) - Element.setStyle(element, - { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + - 'alpha(opacity='+value*100+')' }); - } -} - -Element.getInlineOpacity = function(element){ - return $(element).style.opacity || ''; -} +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; -Element.childrenWithClassName = function(element, className) { - return $A($(element).getElementsByTagName('*')).select( - function(c) { return Element.hasClassName(c, className) }); -} +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; -Array.prototype.call = function() { - var args = arguments; - this.each(function(f){ f.apply(this, args) }); -} +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; /*--------------------------------------------------------------------------*/ var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + .5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; + }, + pulse: function(pos, pulses) { + return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, tagifyText: function(element) { var tagifyStyle = 'position:relative'; - if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + element = $(element); $A(element.childNodes).each( function(child) { - if(child.nodeType==3) { + if (child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( - Builder.node('span',{style: tagifyStyle}, - character == ' ' ? String.fromCharCode(160) : character), + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), child); }); Element.remove(child); @@ -113,176 +123,194 @@ var Effect = { }, multiple: function(element, effect) { var elements; - if(((typeof element == 'object') || - (typeof element == 'function')) && + if (((typeof element == 'object') || + Object.isFunction(element)) && (element.length)) elements = element; else elements = $(element).childNodes; - + var options = Object.extend({ speed: 0.1, delay: 0.0 - }, arguments[2] || {}); + }, arguments[2] || { }); var masterDelay = options.delay; $A(elements).each( function(element, index) { new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); } }; -var Effect2 = Effect; // deprecated - -/* ------------- transitions ------------- */ - -Effect.Transitions = {} - -Effect.Transitions.linear = function(pos) { - return pos; -} -Effect.Transitions.sinoidal = function(pos) { - return (-Math.cos(pos*Math.PI)/2) + 0.5; -} -Effect.Transitions.reverse = function(pos) { - return 1-pos; -} -Effect.Transitions.flicker = function(pos) { - return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; -} -Effect.Transitions.wobble = function(pos) { - return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; -} -Effect.Transitions.pulse = function(pos) { - return (Math.floor(pos*10) % 2 == 0 ? - (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); -} -Effect.Transitions.none = function(pos) { - return 0; -} -Effect.Transitions.full = function(pos) { - return 1; -} +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; /* ------------- core effects ------------- */ -Effect.Queue = { - effects: [], +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, _each: function(iterator) { this.effects._each(iterator); }, - interval: null, add: function(effect) { var timestamp = new Date().getTime(); - - switch(effect.options.queue) { + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { case 'front': - // move unstarted effects after this effect + // move unstarted effects after this effect this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { e.startOn += effect.finishOn; e.finishOn += effect.finishOn; }); break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; case 'end': // start effect after last queued effect has finished timestamp = this.effects.pluck('finishOn').max() || timestamp; break; } - + effect.startOn += timestamp; effect.finishOn += timestamp; - this.effects.push(effect); - if(!this.interval) - this.interval = setInterval(this.loop.bind(this), 40); + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); - if(this.effects.length == 0) { + if (this.effects.length == 0) { clearInterval(this.interval); this.interval = null; } }, loop: function() { var timePos = new Date().getTime(); - this.effects.invoke('loop', timePos); + for(var i=0, len=this.effects.length;i= this.startOn) { - if(timePos >= this.finishOn) { + if (timePos >= this.startOn) { + if (timePos >= this.finishOn) { this.render(1.0); this.cancel(); this.event('beforeFinish'); - if(this.finish) this.finish(); + if (this.finish) this.finish(); this.event('afterFinish'); - return; + return; } - var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); - var frame = Math.round(pos * this.options.fps * this.options.duration); - if(frame > this.currentFrame) { + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, - render: function(pos) { - if(this.state == 'idle') { - this.state = 'running'; - this.event('beforeSetup'); - if(this.setup) this.setup(); - this.event('afterSetup'); - } - if(this.state == 'running') { - if(this.options.transition) pos = this.options.transition(pos); - pos *= (this.options.to-this.options.from); - pos += this.options.from; - this.position = pos; - this.event('beforeUpdate'); - if(this.update) this.update(pos); - this.event('afterUpdate'); - } - }, cancel: function() { - if(!this.options.sync) Effect.Queue.remove(this); + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { - if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); - if(this.options[eventName]) this.options[eventName](this); + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); }, inspect: function() { - return '#'; + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#'; } -} +}); -Effect.Parallel = Class.create(); -Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { +Effect.Parallel = Class.create(Effect.Base, { initialize: function(effects) { this.effects = effects || []; this.start(arguments[1]); @@ -295,423 +323,458 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { effect.render(1.0); effect.cancel(); effect.event('beforeFinish'); - if(effect.finish) effect.finish(position); + if (effect.finish) effect.finish(position); effect.event('afterFinish'); }); } }); -Effect.Opacity = Class.create(); -Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' - if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) - Element.setStyle(this.element, {zoom: 1}); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); var options = Object.extend({ - from: Element.getOpacity(this.element) || 0.0, + from: this.element.getOpacity() || 0.0, to: 1.0 - }, arguments[1] || {}); + }, arguments[1] || { }); this.start(options); }, update: function(position) { - Element.setOpacity(this.element, position); + this.element.setOpacity(position); } }); -Effect.MoveBy = Class.create(); -Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { - initialize: function(element, toTop, toLeft) { - this.element = $(element); - this.toTop = toTop; - this.toLeft = toLeft; - this.start(arguments[3]); +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); }, setup: function() { - // Bug in Opera: Opera returns the "real" position of a static element or - // relative element that does not have top/left explicitly set. - // ==> Always set top and left for position relative elements in your stylesheets - // (to 0 if you do not need them) - Element.makePositioned(this.element); - this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); - this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } }, update: function(position) { - Element.setStyle(this.element, { - top: this.toTop * position + this.originalTop + 'px', - left: this.toLeft * position + this.originalLeft + 'px' + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' }); } }); -Effect.Scale = Class.create(); -Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { initialize: function(element, percent) { - this.element = $(element) + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, - scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleMode: 'box', // 'box' or 'contents' or { } with provided values scaleFrom: 100.0, scaleTo: percent - }, arguments[2] || {}); + }, arguments[2] || { }); this.start(options); }, setup: function() { this.restoreAfterFinish = this.options.restoreAfterFinish || false; - this.elementPositioning = Element.getStyle(this.element,'position'); - - this.originalStyle = {}; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; ['top','left','width','height','fontSize'].each( function(k) { this.originalStyle[k] = this.element.style[k]; }.bind(this)); - + this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; - - var fontSize = Element.getStyle(this.element,'font-size') || '100%'; - ['em','px','%'].each( function(fontSizeType) { - if(fontSize.indexOf(fontSizeType)>0) { + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { this.fontSize = parseFloat(fontSize); this.fontSizeType = fontSizeType; } }.bind(this)); - + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; - + this.dims = null; - if(this.options.scaleMode=='box') + if (this.options.scaleMode=='box') this.dims = [this.element.offsetHeight, this.element.offsetWidth]; - if(/^content/.test(this.options.scaleMode)) + if (/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; - if(!this.dims) + if (!this.dims) this.dims = [this.options.scaleMode.originalHeight, this.options.scaleMode.originalWidth]; }, update: function(position) { var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); - if(this.options.scaleContent && this.fontSize) - Element.setStyle(this.element, {fontSize: this.fontSize * currentScale + this.fontSizeType }); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { - if (this.restoreAfterFinish) Element.setStyle(this.element, this.originalStyle); + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { - var d = {}; - if(this.options.scaleX) d.width = width + 'px'; - if(this.options.scaleY) d.height = height + 'px'; - if(this.options.scaleFromCenter) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; - if(this.elementPositioning == 'absolute') { - if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; - if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; } else { - if(this.options.scaleY) d.top = -topd + 'px'; - if(this.options.scaleX) d.left = -leftd + 'px'; + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; } } - Element.setStyle(this.element, d); + this.element.setStyle(d); } }); -Effect.Highlight = Class.create(); -Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { +Effect.Highlight = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); - var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow - if(Element.getStyle(this.element, 'display')=='none') { this.cancel(); return; } + if (this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect - this.oldStyle = { - backgroundImage: Element.getStyle(this.element, 'background-image') }; - Element.setStyle(this.element, {backgroundImage: 'none'}); - if(!this.options.endcolor) - this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); - if(!this.options.restorecolor) - this.options.restorecolor = Element.getStyle(this.element, 'background-color'); + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); }, update: function(position) { - Element.setStyle(this.element,{backgroundColor: $R(0,2).inject('#',function(m,v,i){ - return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); }, finish: function() { - Element.setStyle(this.element, Object.extend(this.oldStyle, { + this.element.setStyle(Object.extend(this.oldStyle, { backgroundColor: this.options.restorecolor })); } }); -Effect.ScrollTo = Class.create(); -Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { - initialize: function(element) { - this.element = $(element); - this.start(arguments[1] || {}); - }, - setup: function() { - Position.prepare(); - var offsets = Position.cumulativeOffset(this.element); - if(this.options.offset) offsets[1] += this.options.offset; - var max = window.innerHeight ? - window.height - window.innerHeight : - document.body.scrollHeight - - (document.documentElement.clientHeight ? - document.documentElement.clientHeight : document.body.clientHeight); - this.scrollStart = Position.deltaY; - this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; - }, - update: function(position) { - Position.prepare(); - window.scrollTo(Position.deltaX, - this.scrollStart + (position*this.delta)); - } -}); +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()); } + ); +}; /* ------------- combination effects ------------- */ Effect.Fade = function(element) { - var oldOpacity = Element.getInlineOpacity(element); + element = $(element); + var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ - from: Element.getOpacity(element) || 1.0, - to: 0.0, - afterFinishInternal: function(effect) { with(Element) { - if(effect.options.to!=0) return; - hide(effect.element); - setStyle(effect.element, {opacity: oldOpacity}); }} - }, arguments[1] || {}); + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); return new Effect.Opacity(element,options); -} +}; Effect.Appear = function(element) { + element = $(element); var options = Object.extend({ - from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0), + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), to: 1.0, - beforeSetup: function(effect) { with(Element) { - setOpacity(effect.element, effect.options.from); - show(effect.element); }} - }, arguments[1] || {}); + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); return new Effect.Opacity(element,options); -} +}; Effect.Puff = function(element) { element = $(element); - var oldStyle = { opacity: Element.getInlineOpacity(element), position: Element.getStyle(element, 'position') }; + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; return new Effect.Parallel( - [ new Effect.Scale(element, 200, - { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), - new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], - Object.extend({ duration: 1.0, - beforeSetupInternal: function(effect) { with(Element) { - setStyle(effect.effects[0].element, {position: 'absolute'}); }}, - afterFinishInternal: function(effect) { with(Element) { - hide(effect.effects[0].element); - setStyle(effect.effects[0].element, oldStyle); }} - }, arguments[1] || {}) + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) ); -} +}; Effect.BlindUp = function(element) { element = $(element); - Element.makeClipping(element); - return new Effect.Scale(element, 0, - Object.extend({ scaleContent: false, - scaleX: false, + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, restoreAfterFinish: true, - afterFinishInternal: function(effect) { with(Element) { - [hide, undoClipping].call(effect.element); }} - }, arguments[1] || {}) + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) ); -} +}; Effect.BlindDown = function(element) { element = $(element); - var oldHeight = Element.getStyle(element, 'height'); - var elementDimensions = Element.getDimensions(element); - return new Effect.Scale(element, 100, - Object.extend({ scaleContent: false, - scaleX: false, - scaleFrom: 0, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { with(Element) { - makeClipping(effect.element); - setStyle(effect.element, {height: '0px'}); - show(effect.element); - }}, - afterFinishInternal: function(effect) { with(Element) { - undoClipping(effect.element); - setStyle(effect.element, {height: oldHeight}); - }} - }, arguments[1] || {}) - ); -} + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; Effect.SwitchOff = function(element) { element = $(element); - var oldOpacity = Element.getInlineOpacity(element); - return new Effect.Appear(element, { + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ duration: 0.4, from: 0, transition: Effect.Transitions.flicker, afterFinishInternal: function(effect) { - new Effect.Scale(effect.element, 1, { + new Effect.Scale(effect.element, 1, { duration: 0.3, scaleFromCenter: true, scaleX: false, scaleContent: false, restoreAfterFinish: true, - beforeSetup: function(effect) { with(Element) { - [makePositioned,makeClipping].call(effect.element); - }}, - afterFinishInternal: function(effect) { with(Element) { - [hide,undoClipping,undoPositioned].call(effect.element); - setStyle(effect.element, {opacity: oldOpacity}); - }} - }) + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }); } - }); -} + }, arguments[1] || { })); +}; Effect.DropOut = function(element) { element = $(element); var oldStyle = { - top: Element.getStyle(element, 'top'), - left: Element.getStyle(element, 'left'), - opacity: Element.getInlineOpacity(element) }; + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; return new Effect.Parallel( - [ new Effect.MoveBy(element, 100, 0, { sync: true }), + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), new Effect.Opacity(element, { sync: true, to: 0.0 }) ], Object.extend( { duration: 0.5, - beforeSetup: function(effect) { with(Element) { - makePositioned(effect.effects[0].element); }}, - afterFinishInternal: function(effect) { with(Element) { - [hide, undoPositioned].call(effect.effects[0].element); - setStyle(effect.effects[0].element, oldStyle); }} - }, arguments[1] || {})); -} + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; Effect.Shake = function(element) { element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; var oldStyle = { - top: Element.getStyle(element, 'top'), - left: Element.getStyle(element, 'left') }; - return new Effect.MoveBy(element, 0, 20, - { duration: 0.05, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -20, - { duration: 0.05, afterFinishInternal: function(effect) { with(Element) { - undoPositioned(effect.element); - setStyle(effect.element, oldStyle); - }}}) }}) }}) }}) }}) }}); -} + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}); }}); }}); }}); }}); }}); +}; Effect.SlideDown = function(element) { - element = $(element); - Element.cleanWhitespace(element); + element = $(element).cleanWhitespace(); // SlideDown need to have the content of the element wrapped in a container element with fixed height! - var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom'); - var elementDimensions = Element.getDimensions(element); - return new Effect.Scale(element, 100, Object.extend({ - scaleContent: false, - scaleX: false, - scaleFrom: 0, + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, - afterSetup: function(effect) { with(Element) { - makePositioned(effect.element); - makePositioned(effect.element.firstChild); - if(window.opera) setStyle(effect.element, {top: ''}); - makeClipping(effect.element); - setStyle(effect.element, {height: '0px'}); - show(element); }}, - afterUpdateInternal: function(effect) { with(Element) { - setStyle(effect.element.firstChild, {bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); }}, - afterFinishInternal: function(effect) { with(Element) { - undoClipping(effect.element); - undoPositioned(effect.element.firstChild); - undoPositioned(effect.element); - setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }} - }, arguments[1] || {}) + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) ); -} - +}; + Effect.SlideUp = function(element) { - element = $(element); - Element.cleanWhitespace(element); - var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom'); - return new Effect.Scale(element, 0, - Object.extend({ scaleContent: false, - scaleX: false, + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, scaleMode: 'box', scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, - beforeStartInternal: function(effect) { with(Element) { - makePositioned(effect.element); - makePositioned(effect.element.firstChild); - if(window.opera) setStyle(effect.element, {top: ''}); - makeClipping(effect.element); - show(element); }}, - afterUpdateInternal: function(effect) { with(Element) { - setStyle(effect.element.firstChild, {bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); }}, - afterFinishInternal: function(effect) { with(Element) { - [hide, undoClipping].call(effect.element); - undoPositioned(effect.element.firstChild); - undoPositioned(effect.element); - setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }} - }, arguments[1] || {}) + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) ); -} +}; -// Bug in opera makes the TD containing this element expand for a instance after finish +// Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { - return new Effect.Scale(element, window.opera ? 1 : 0, - { restoreAfterFinish: true, - beforeSetup: function(effect) { with(Element) { - makeClipping(effect.element); }}, - afterFinishInternal: function(effect) { with(Element) { - hide(effect.element); - undoClipping(effect.element); }} + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } }); -} +}; Effect.Grow = function(element) { element = $(element); var options = Object.extend({ direction: 'center', - moveTransistion: Effect.Transitions.sinoidal, + moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.full - }, arguments[1] || {}); + }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, - opacity: Element.getInlineOpacity(element) }; + opacity: element.getInlineOpacity() }; - var dims = Element.getDimensions(element); + var dims = element.getDimensions(); var initialMoveX, initialMoveY; var moveX, moveY; - + switch (options.direction) { case 'top-left': - initialMoveX = initialMoveY = moveX = moveY = 0; + initialMoveX = initialMoveY = moveX = moveY = 0; break; case 'top-right': initialMoveX = dims.width; @@ -736,52 +799,52 @@ Effect.Grow = function(element) { moveY = -dims.height / 2; break; } - - return new Effect.MoveBy(element, initialMoveY, initialMoveX, { - duration: 0.01, - beforeSetup: function(effect) { with(Element) { - hide(effect.element); - makeClipping(effect.element); - makePositioned(effect.element); - }}, + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, afterFinishInternal: function(effect) { new Effect.Parallel( [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), - new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: options.moveTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), new Effect.Scale(effect.element, 100, { - scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) ], Object.extend({ - beforeSetup: function(effect) { with(Element) { - setStyle(effect.effects[0].element, {height: '0px'}); - show(effect.effects[0].element); }}, - afterFinishInternal: function(effect) { with(Element) { - [undoClipping, undoPositioned].call(effect.effects[0].element); - setStyle(effect.effects[0].element, oldStyle); }} + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } }, options) - ) + ); } }); -} +}; Effect.Shrink = function(element) { element = $(element); var options = Object.extend({ direction: 'center', - moveTransistion: Effect.Transitions.sinoidal, + moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.none - }, arguments[1] || {}); + }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, - opacity: Element.getInlineOpacity(element) }; + opacity: element.getInlineOpacity() }; - var dims = Element.getDimensions(element); + var dims = element.getDimensions(); var moveX, moveY; - + switch (options.direction) { case 'top-left': moveX = moveY = 0; @@ -798,38 +861,40 @@ Effect.Shrink = function(element) { moveX = dims.width; moveY = dims.height; break; - case 'center': + case 'center': moveX = dims.width / 2; moveY = dims.height / 2; break; } - + return new Effect.Parallel( [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), - new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: options.moveTransition }) - ], Object.extend({ - beforeStartInternal: function(effect) { with(Element) { - [makePositioned, makeClipping].call(effect.effects[0].element) }}, - afterFinishInternal: function(effect) { with(Element) { - [hide, undoClipping, undoPositioned].call(effect.effects[0].element); - setStyle(effect.effects[0].element, oldStyle); }} + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); -} +}; Effect.Pulsate = function(element) { element = $(element); - var options = arguments[1] || {}; - var oldOpacity = Element.getInlineOpacity(element); - var transition = options.transition || Effect.Transitions.sinoidal; - var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; - reverser.bind(transition); - return new Effect.Opacity(element, - Object.extend(Object.extend({ duration: 3.0, from: 0, - afterFinishInternal: function(effect) { Element.setStyle(effect.element, {opacity: oldOpacity}); } + var options = arguments[1] || { }, + oldOpacity = element.getInlineOpacity(), + transition = options.transition || Effect.Transitions.linear, + reverser = function(pos){ + return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); + }; + + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); -} +}; Effect.Fold = function(element) { element = $(element); @@ -838,17 +903,226 @@ Effect.Fold = function(element) { left: element.style.left, width: element.style.width, height: element.style.height }; - Element.makeClipping(element); - return new Effect.Scale(element, 5, Object.extend({ + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ scaleContent: false, scaleX: false, afterFinishInternal: function(effect) { - new Effect.Scale(element, 1, { - scaleContent: false, + new Effect.Scale(element, 1, { + scaleContent: false, scaleY: false, - afterFinishInternal: function(effect) { with(Element) { - [hide, undoClipping].call(effect.element); - setStyle(effect.element, oldStyle); - }} }); - }}, arguments[1] || {})); + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + }; + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ); + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ); + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '

    '; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { + results[property] = css[property]; + return results; + }); + if (!styles.opacity) styles.opacity = element.getOpacity(); + return styles; + }; } + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element); + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + }; + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/examples/rails_openid/public/javascripts/prototype.js b/examples/rails_openid/public/javascripts/prototype.js index e9ccd3c8..dfe8ab4e 100644 --- a/examples/rails_openid/public/javascripts/prototype.js +++ b/examples/rails_openid/public/javascripts/prototype.js @@ -1,102 +1,297 @@ -/* Prototype JavaScript framework, version 1.4.0 - * (c) 2005 Sam Stephenson - * - * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff - * against the source tree, available from the Prototype darcs repository. +/* Prototype JavaScript framework, version 1.6.0.3 + * (c) 2005-2008 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ * - * For details, see the Prototype web site: http://prototype.conio.net/ - * -/*--------------------------------------------------------------------------*/ + *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.4.0', - ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + Version: '1.6.0.3', - emptyFunction: function() {}, - K: function(x) {return x} -} + Browser: { + IE: !!(window.attachEvent && + navigator.userAgent.indexOf('Opera') === -1), + Opera: navigator.userAgent.indexOf('Opera') > -1, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && + navigator.userAgent.indexOf('KHTML') === -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div')['__proto__'] && + document.createElement('div')['__proto__'] !== + document.createElement('form')['__proto__'] + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ var Class = { create: function() { - return function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { this.initialize.apply(this, arguments); } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; } -} +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } +}; -var Abstract = new Object(); +var Abstract = { }; Object.extend = function(destination, source) { - for (property in source) { + for (var property in source) destination[property] = source[property]; - } return destination; -} +}; -Object.inspect = function(object) { - try { - if (object == undefined) return 'undefined'; - if (object == null) return 'null'; - return object.inspect ? object.inspect() : object.toString(); - } catch (e) { - if (e instanceof RangeError) return '...'; - throw e; - } -} +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, -Function.prototype.bind = function() { - var __method = this, args = $A(arguments), object = args.shift(); - return function() { - return __method.apply(object, args.concat($A(arguments))); - } -} + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return !!(object && object.nodeType == 1); + }, + + isArray: function(object) { + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; + }, + + isHash: function(object) { + return object instanceof Hash; + }, -Function.prototype.bindAsEventListener = function(object) { - var __method = this; - return function(event) { - return __method.call(object, event || window.event); + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; } -} +}); -Object.extend(Number.prototype, { - toColorPart: function() { - var digits = this.toString(16); - if (this < 16) return '0' + digits; - return digits; +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; }, - succ: function() { - return this + 1; + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } }, - times: function(iterator) { - $R(0, this, true).each(iterator); - return this; + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + defer: function() { + var args = [0.01].concat($A(arguments)); + return this.delay.apply(this, args); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; } }); +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + var Try = { these: function() { var returnValue; - for (var i = 0; i < arguments.length; i++) { + for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); break; - } catch (e) {} + } catch (e) { } } return returnValue; } -} +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; /*--------------------------------------------------------------------------*/ -var PeriodicalExecuter = Class.create(); -PeriodicalExecuter.prototype = { +var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; @@ -106,40 +301,87 @@ PeriodicalExecuter.prototype = { }, registerCallback: function() { - setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; }, onTimerEvent: function() { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; - this.callback(); + this.execute(); } finally { this.currentlyExecuting = false; } } } -} +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); -/*--------------------------------------------------------------------------*/ +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, -function $() { - var elements = new Array(); + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; - for (var i = 0; i < arguments.length; i++) { - var element = arguments[i]; - if (typeof element == 'string') - element = document.getElementById(element); + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, - if (arguments.length == 1) - return element; + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, - elements.push(element); - } + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, - return elements; -} -Object.extend(String.prototype, { stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, @@ -157,28 +399,40 @@ Object.extend(String.prototype, { }, evalScripts: function() { - return this.extractScripts().map(eval); + return this.extractScripts().map(function(script) { return eval(script) }); }, escapeHTML: function() { - var div = document.createElement('div'); - var text = document.createTextNode(this); - div.appendChild(text); - return div.innerHTML; + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; }, unescapeHTML: function() { - var div = document.createElement('div'); + var div = new Element('div'); div.innerHTML = this.stripTags(); - return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; }, - toQueryParams: function() { - var pairs = this.match(/^\??(.*)$/)[1].split('&'); - return pairs.inject({}, function(params, pairString) { - var pair = pairString.split('='); - params[pair[0]] = pair[1]; - return params; + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; }); }, @@ -186,78 +440,214 @@ Object.extend(String.prototype, { return this.split(''); }, + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + camelize: function() { - var oStringList = this.split('-'); - if (oStringList.length == 1) return oStringList[0]; + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; - var camelizedString = this.indexOf('-') == 0 - ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) - : oStringList[0]; + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; - for (var i = 1, len = oStringList.length; i < len; i++) { - var s = oStringList[i]; - camelizedString += s.charAt(0).toUpperCase() + s.substring(1); - } + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); - return camelizedString; + return camelized; }, - inspect: function() { - return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(//g,'>'); + }, + unescapeHTML: function() { + return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } }); +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + String.prototype.parseQuery = String.prototype.toQueryParams; -var $break = new Object(); -var $continue = new Object(); +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; var Enumerable = { - each: function(iterator) { + each: function(iterator, context) { var index = 0; try { this._each(function(value) { - try { - iterator(value, index++); - } catch (e) { - if (e != $continue) throw e; - } + iterator.call(context, value, index++); }); } catch (e) { if (e != $break) throw e; } + return this; }, - all: function(iterator) { + eachSlice: function(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator || Prototype.K; var result = true; this.each(function(value, index) { - result = result && !!(iterator || Prototype.K)(value, index); + result = result && !!iterator.call(context, value, index); if (!result) throw $break; }); return result; }, - any: function(iterator) { - var result = true; + any: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; this.each(function(value, index) { - if (result = !!(iterator || Prototype.K)(value, index)) + if (result = !!iterator.call(context, value, index)) throw $break; }); return result; }, - collect: function(iterator) { + collect: function(iterator, context) { + iterator = iterator || Prototype.K; var results = []; this.each(function(value, index) { - results.push(iterator(value, index)); + results.push(iterator.call(context, value, index)); }); return results; }, - detect: function (iterator) { + detect: function(iterator, context) { var result; this.each(function(value, index) { - if (iterator(value, index)) { + if (iterator.call(context, value, index)) { result = value; throw $break; } @@ -265,26 +655,33 @@ var Enumerable = { return result; }, - findAll: function(iterator) { + findAll: function(iterator, context) { var results = []; this.each(function(value, index) { - if (iterator(value, index)) + if (iterator.call(context, value, index)) results.push(value); }); return results; }, - grep: function(pattern, iterator) { + grep: function(filter, iterator, context) { + iterator = iterator || Prototype.K; var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + this.each(function(value, index) { - var stringValue = value.toString(); - if (stringValue.match(pattern)) - results.push((iterator || Prototype.K)(value, index)); - }) + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); return results; }, include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + var found = false; this.each(function(value) { if (value == object) { @@ -295,44 +692,55 @@ var Enumerable = { return found; }, - inject: function(memo, iterator) { + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { this.each(function(value, index) { - memo = iterator(memo, value, index); + memo = iterator.call(context, memo, value, index); }); return memo; }, invoke: function(method) { var args = $A(arguments).slice(1); - return this.collect(function(value) { + return this.map(function(value) { return value[method].apply(value, args); }); }, - max: function(iterator) { + max: function(iterator, context) { + iterator = iterator || Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); - if (value >= (result || value)) + value = iterator.call(context, value, index); + if (result == null || value >= result) result = value; }); return result; }, - min: function(iterator) { + min: function(iterator, context) { + iterator = iterator || Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); - if (value <= (result || value)) + value = iterator.call(context, value, index); + if (result == null || value < result) result = value; }); return result; }, - partition: function(iterator) { + partition: function(iterator, context) { + iterator = iterator || Prototype.K; var trues = [], falses = []; this.each(function(value, index) { - ((iterator || Prototype.K)(value, index) ? + (iterator.call(context, value, index) ? trues : falses).push(value); }); return [trues, falses]; @@ -340,24 +748,27 @@ var Enumerable = { pluck: function(property) { var results = []; - this.each(function(value, index) { + this.each(function(value) { results.push(value[property]); }); return results; }, - reject: function(iterator) { + reject: function(iterator, context) { var results = []; this.each(function(value, index) { - if (!iterator(value, index)) + if (!iterator.call(context, value, index)) results.push(value); }); return results; }, - sortBy: function(iterator) { - return this.collect(function(value, index) { - return {value: value, criteria: iterator(value, index)}; + sortBy: function(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; @@ -365,52 +776,71 @@ var Enumerable = { }, toArray: function() { - return this.collect(Prototype.K); + return this.map(); }, zip: function() { var iterator = Prototype.K, args = $A(arguments); - if (typeof args.last() == 'function') + if (Object.isFunction(args.last())) iterator = args.pop(); var collections = [this].concat(args).map($A); return this.map(function(value, index) { - iterator(value = collections.pluck(index)); - return value; + return iterator(collections.pluck(index)); }); }, + size: function() { + return this.toArray().length; + }, + inspect: function() { return '#'; } -} +}; Object.extend(Enumerable, { map: Enumerable.collect, find: Enumerable.detect, select: Enumerable.findAll, + filter: Enumerable.findAll, member: Enumerable.include, - entries: Enumerable.toArray + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any }); -var $A = Array.from = function(iterable) { +function $A(iterable) { if (!iterable) return []; - if (iterable.toArray) { - return iterable.toArray(); - } else { - var results = []; - for (var i = 0; i < iterable.length; i++) - results.push(iterable[i]); + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + $A = function(iterable) { + if (!iterable) return []; + // In Safari, only use the `toArray` method if it's not a NodeList. + // A NodeList is a function, has an function `item` property, and a numeric + // `length` property. Adapted from Google Doctype. + if (!(typeof iterable === 'function' && typeof iterable.length === + 'number' && typeof iterable.item === 'function') && iterable.toArray) + return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; return results; - } + }; } +Array.from = $A; + Object.extend(Array.prototype, Enumerable); -Array.prototype._reverse = Array.prototype.reverse; +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { - for (var i = 0; i < this.length; i++) + for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }, @@ -429,13 +859,13 @@ Object.extend(Array.prototype, { compact: function() { return this.select(function(value) { - return value != undefined || value != null; + return value != null; }); }, flatten: function() { return this.inject([], function(array, value) { - return array.concat(value.constructor == Array ? + return array.concat(Object.isArray(value) ? value.flatten() : [value]); }); }, @@ -447,78 +877,221 @@ Object.extend(Array.prototype, { }); }, - indexOf: function(object) { - for (var i = 0; i < this.length; i++) - if (this[i] == object) return i; - return -1; - }, - reverse: function(inline) { return (inline !== false ? this : this.toArray())._reverse(); }, - shift: function() { - var result = this[0]; - for (var i = 0; i < this.length - 1; i++) - this[i] = this[i + 1]; - this.length--; - return result; + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; }, inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; } }); -var Hash = { - _each: function(iterator) { - for (key in this) { - var value = this[key]; - if (typeof value == 'function') continue; - var pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); - } - }, +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; - keys: function() { - return this.pluck('key'); +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); }, - values: function() { - return this.pluck('value'); + succ: function() { + return this + 1; }, - merge: function(hash) { - return $H(hash).inject($H(this), function(mergedHash, pair) { - mergedHash[pair.key] = pair.value; - return mergedHash; - }); + times: function(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; }, - toQueryString: function() { - return this.map(function(pair) { - return pair.map(encodeURIComponent).join('='); - }).join('&'); + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; }, - inspect: function() { - return '#'; + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; } -} +}); +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); function $H(object) { - var hash = Object.extend({}, object || {}); - Object.extend(hash, Enumerable); - Object.extend(hash, Hash); - return hash; -} -ObjectRange = Class.create(); -Object.extend(ObjectRange.prototype, Enumerable); -Object.extend(ObjectRange.prototype, { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + // simulating poorly supported hasOwnProperty + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { initialize: function(start, end, exclusive) { this.start = start; this.end = end; @@ -527,10 +1100,10 @@ Object.extend(ObjectRange.prototype, { _each: function(iterator) { var value = this.start; - do { + while (this.include(value)) { iterator(value); value = value.succ(); - } while (this.include(value)); + } }, include: function(value) { @@ -544,19 +1117,19 @@ Object.extend(ObjectRange.prototype, { var $R = function(start, end, exclusive) { return new ObjectRange(start, end, exclusive); -} +}; var Ajax = { getTransport: function() { return Try.these( + function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, - function() {return new ActiveXObject('Microsoft.XMLHTTP')}, - function() {return new XMLHttpRequest()} + function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, activeRequestCount: 0 -} +}; Ajax.Responders = { responders: [], @@ -565,21 +1138,21 @@ Ajax.Responders = { this.responders._each(iterator); }, - register: function(responderToAdd) { - if (!this.include(responderToAdd)) - this.responders.push(responderToAdd); + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); }, - unregister: function(responderToRemove) { - this.responders = this.responders.without(responderToRemove); + unregister: function(responder) { + this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { - if (responder[callback] && typeof responder[callback] == 'function') { + if (Object.isFunction(responder[callback])) { try { responder[callback].apply(responder, [request, transport, json]); - } catch (e) {} + } catch (e) { } } }); } @@ -588,154 +1161,194 @@ Ajax.Responders = { Object.extend(Ajax.Responders, Enumerable); Ajax.Responders.register({ - onCreate: function() { - Ajax.activeRequestCount++; - }, - - onComplete: function() { - Ajax.activeRequestCount--; - } + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } }); -Ajax.Base = function() {}; -Ajax.Base.prototype = { - setOptions: function(options) { +Ajax.Base = Class.create({ + initialize: function(options) { this.options = { method: 'post', asynchronous: true, - parameters: '' - } - Object.extend(this.options, options || {}); - }, - - responseIsSuccess: function() { - return this.transport.status == undefined - || this.transport.status == 0 - || (this.transport.status >= 200 && this.transport.status < 300); - }, - - responseIsFailure: function() { - return !this.responseIsSuccess(); + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); } -} +}); -Ajax.Request = Class.create(); -Ajax.Request.Events = - ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, -Ajax.Request.prototype = Object.extend(new Ajax.Base(), { - initialize: function(url, options) { + initialize: function($super, url, options) { + $super(options); this.transport = Ajax.getTransport(); - this.setOptions(options); this.request(url); }, request: function(url) { - var parameters = this.options.parameters || ''; - if (parameters.length > 0) parameters += '&_='; + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); - try { - this.url = url; - if (this.options.method == 'get' && parameters.length > 0) - this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } - Ajax.Responders.dispatch('onCreate', this, this.transport); + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); - this.transport.open(this.options.method, this.url, + this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); - if (this.options.asynchronous) { - this.transport.onreadystatechange = this.onStateChange.bind(this); - setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); - } + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); - var body = this.options.postBody ? this.options.postBody : parameters; - this.transport.send(this.options.method == 'post' ? body : null); + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); - } catch (e) { + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { this.dispatchException(e); } }, - setRequestHeaders: function() { - var requestHeaders = - ['X-Requested-With', 'XMLHttpRequest', - 'X-Prototype-Version', Prototype.Version]; - - if (this.options.method == 'post') { - requestHeaders.push('Content-type', - 'application/x-www-form-urlencoded'); + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, - /* Force "Connection: close" for Mozilla browsers to work around - * a bug where XMLHttpReqeuest sends an incorrect Content-length - * header. See Mozilla Bugzilla #246651. + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. */ - if (this.transport.overrideMimeType) - requestHeaders.push('Connection', 'close'); + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; } - if (this.options.requestHeaders) - requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; - for (var i = 0; i < requestHeaders.length; i += 2) - this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); - }, - - onStateChange: function() { - var readyState = this.transport.readyState; - if (readyState != 1) - this.respondToReadyState(this.transport.readyState); - }, + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } - header: function(name) { - try { - return this.transport.getResponseHeader(name); - } catch (e) {} + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); }, - evalJSON: function() { - try { - return eval(this.header('X-JSON')); - } catch (e) {} + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); }, - evalResponse: function() { + getStatus: function() { try { - return eval(this.transport.responseText); - } catch (e) { - this.dispatchException(e); - } + return this.transport.status || 0; + } catch (e) { return 0 } }, respondToReadyState: function(readyState) { - var event = Ajax.Request.Events[readyState]; - var transport = this.transport, json = this.evalJSON(); + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); - if (event == 'Complete') { + if (state == 'Complete') { try { - (this.options['on' + this.transport.status] - || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(transport, json); + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); } catch (e) { this.dispatchException(e); } - if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } try { - (this.options['on' + event] || Prototype.emptyFunction)(transport, json); - Ajax.Responders.dispatch('on' + event, this, transport, json); + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); } catch (e) { this.dispatchException(e); } - /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ - if (event == 'Complete') + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } }, dispatchException: function(exception) { @@ -744,61 +1357,128 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { } }); -Ajax.Updater = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; -Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { - initialize: function(container, url, options) { - this.containers = { - success: container.success ? $(container.success) : $(container), - failure: container.failure ? $(container.failure) : - (container.success ? null : $(container)) +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); } - this.transport = Ajax.getTransport(); - this.setOptions(options); + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, - var onComplete = this.options.onComplete || Prototype.emptyFunction; - this.options.onComplete = (function(transport, object) { - this.updateContent(); - onComplete(transport, object); - }).bind(this); + status: 0, + statusText: '', - this.request(url); + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } }, - updateContent: function() { - var receiver = this.responseIsSuccess() ? - this.containers.success : this.containers.failure; - var response = this.transport.responseText; + getHeader: Ajax.Request.prototype.getHeader, - if (!this.options.evalScripts) - response = response.stripScripts(); + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, - if (receiver) { - if (this.options.insertion) { - new this.options.insertion(receiver, response); - } else { - Element.update(receiver, response); - } + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, - if (this.responseIsSuccess()) { - if (this.onComplete) - setTimeout(this.onComplete.bind(this), 10); + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); } } }); -Ajax.PeriodicalUpdater = Class.create(); -Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { - initialize: function(container, url, options) { - this.setOptions(options); +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); this.onComplete = this.options.onComplete; this.frequency = (this.options.frequency || 2); this.decay = (this.options.decay || 1); - this.updater = {}; + this.updater = { }; this.container = container; this.url = url; @@ -811,80 +1491,340 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { }, stop: function() { - this.updater.onComplete = undefined; + this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, - updateComplete: function(request) { + updateComplete: function(response) { if (this.options.decay) { - this.decay = (request.responseText == this.lastText ? + this.decay = (response.responseText == this.lastText ? this.decay * this.options.decay : 1); - this.lastText = request.responseText; + this.lastText = response.responseText; } - this.timer = setTimeout(this.onTimerEvent.bind(this), - this.decay * this.frequency * 1000); + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); }, onTimerEvent: function() { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); -document.getElementsByClassName = function(className, parentElement) { - var children = ($(parentElement) || document.body).getElementsByTagName('*'); - return $A(children).inject([], function(elements, child) { - if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) - elements.push(child); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); return elements; - }); + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; } /*--------------------------------------------------------------------------*/ -if (!window.Element) { - var Element = new Object(); +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); } -Object.extend(Element, { +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); + if (element) this.Element.prototype = element.prototype; +}).call(window); + +Element.cache = { }; + +Element.Methods = { visible: function(element) { return $(element).style.display != 'none'; }, - toggle: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - Element[Element.visible(element) ? 'hide' : 'show'](element); - } + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; }, - hide: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = 'none'; - } + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; }, - show: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = ''; - } + show: function(element) { + element = $(element); + element.style.display = ''; + return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); + return element; }, - update: function(element, html) { - $(element).innerHTML = html.stripScripts(); - setTimeout(function() {html.evalScripts()}, 10); + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; }, - getHeight: function(element) { + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).select("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { element = $(element); - return element.offsetHeight; + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? element.descendants()[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; }, classNames: function(element) { @@ -893,67 +1833,115 @@ Object.extend(Element, { hasClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).include(className); + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }, addClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).add(className); + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).remove(className); + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); }, // removes whitespace-only text node children cleanWhitespace: function(element) { element = $(element); - for (var i = 0; i < element.childNodes.length; i++) { - var node = element.childNodes[i]; + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) - Element.remove(node); + element.removeChild(node); + node = nextNode; } + return element; }, empty: function(element) { - return $(element).innerHTML.match(/^\s*$/); + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; }, scrollTo: function(element) { element = $(element); - var x = element.x ? element.x : element.offsetLeft, - y = element.y ? element.y : element.offsetTop; - window.scrollTo(x, y); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; }, getStyle: function(element, style) { element = $(element); - var value = element.style[style.camelize()]; - if (!value) { - if (document.defaultView && document.defaultView.getComputedStyle) { - var css = document.defaultView.getComputedStyle(element, null); - value = css ? css.getPropertyValue(style) : null; - } else if (element.currentStyle) { - value = element.currentStyle[style.camelize()]; - } + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, - if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) - if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, - return value == 'auto' ? null : value; + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; }, - setStyle: function(element, style) { + setOpacity: function(element, value) { element = $(element); - for (name in style) - element.style[name.camelize()] = style[name]; + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; }, getDimensions: function(element) { element = $(element); - if (Element.getStyle(element, 'display') != 'none') + var display = element.getStyle('display'); + if (display != 'none' && display != null) // Safari bug return {width: element.offsetWidth, height: element.offsetHeight}; // All *Width and *Height properties give 0 on elements with display none, @@ -961,12 +1949,13 @@ Object.extend(Element, { var els = element.style; var originalVisibility = els.visibility; var originalPosition = els.position; + var originalDisplay = els.display; els.visibility = 'hidden'; els.position = 'absolute'; - els.display = ''; + els.display = 'block'; var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; - els.display = 'none'; + els.display = originalDisplay; els.position = originalPosition; els.visibility = originalVisibility; return {width: originalWidth, height: originalHeight}; @@ -980,11 +1969,12 @@ Object.extend(Element, { element.style.position = 'relative'; // Opera returns the offset relative to the positioning context, when an // element is position relative but top and left have not been defined - if (window.opera) { + if (Prototype.Browser.Opera) { element.style.top = 0; element.style.left = 0; } } + return element; }, undoPositioned: function(element) { @@ -997,388 +1987,1744 @@ Object.extend(Element, { element.style.bottom = element.style.right = ''; } + return element; }, makeClipping: function(element) { element = $(element); - if (element._overflow) return; - element._overflow = element.style.overflow; - if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') element.style.overflow = 'hidden'; + return element; }, undoClipping: function(element) { element = $(element); - if (element._overflow) return; - element.style.overflow = element._overflow; - element._overflow = undefined; - } + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants }); -var Toggle = new Object(); -Toggle.display = Element.toggle; +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; -/*--------------------------------------------------------------------------*/ +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); -Abstract.Insertion = function(adjacency) { - this.adjacency = adjacency; + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); } -Abstract.Insertion.prototype = { - initialize: function(element, content) { - this.element = $(element); - this.content = content.stripScripts(); +else if (Prototype.Browser.IE) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + // IE throws an error if element is not in document + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); - if (this.adjacency && this.element.insertAdjacentHTML) { - try { - this.element.insertAdjacentHTML(this.adjacency, this.content); - } catch (e) { - if (this.element.tagName.toLowerCase() == 'tbody') { - this.insertContent(this.contentFromAnonymousTable()); - } else { - throw e; + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; } } - } else { - this.range = this.element.ownerDocument.createRange(); - if (this.initializeRange) this.initializeRange(); - this.insertContent([this.range.createContextualFragment(this.content)]); } + }; + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); - setTimeout(function() {content.evalScripts()}, 10); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if ('outerHTML' in document.createElement('div')) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['', '
    ', 1], + TBODY: ['', '
    ', 2], + TR: ['', '
    ', 3], + TD: ['
    ', '
    ', 4], + SELECT: ['', 1] + } +}; - contentFromAnonymousTable: function() { - var div = document.createElement('div'); - div.innerHTML = '' + this.content + '
    '; - return $A(div.childNodes[0].childNodes[0].childNodes); +(function() { + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div')['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div')['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; } -var Insertion = new Object(); +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; -Insertion.Before = Class.create(); -Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { - initializeRange: function() { - this.range.setStartBefore(this.element); - }, + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(), property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.parentNode.insertBefore(fragment, this.element); - }).bind(this)); +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); } -}); -Insertion.Top = Class.create(); -Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { - initializeRange: function() { - this.range.selectNodeContents(this.element); - this.range.collapse(true); - }, + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } - insertContent: function(fragments) { - fragments.reverse(false).each((function(fragment) { - this.element.insertBefore(fragment, this.element.firstChild); - }).bind(this)); + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); } -}); -Insertion.Bottom = Class.create(); -Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { - initializeRange: function() { - this.range.selectNodeContents(this.element); - this.range.collapse(this.element); - }, + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.appendChild(fragment); - }).bind(this)); + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } } -}); -Insertion.After = Class.create(); -Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { - initializeRange: function() { - this.range.setStartAfter(this.element); + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName)['__proto__']; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }, B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + if (B.WebKit && !document.evaluate) { + // Safari <3.0 needs self.innerWidth/Height + dimensions[d] = self['inner' + D]; + } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { + // Opera <9.5 needs document.body.clientWidth/Height + dimensions[d] = document.body['client' + D] + } else { + dimensions[d] = document.documentElement['client' + D]; + } + }); + return dimensions; }, - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.parentNode.insertBefore(fragment, - this.element.nextSibling); - }).bind(this)); + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } -}); +}; +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } -/*--------------------------------------------------------------------------*/ + }, -Element.ClassNames = Class.create(); -Element.ClassNames.prototype = { - initialize: function(element) { - this.element = $(element); + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + return true; }, - _each: function(iterator) { - this.element.className.split(/\s+/).select(function(name) { - return name.length > 0; - })._each(iterator); + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; }, - set: function(className) { - this.element.className = className; + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; }, - add: function(classNameToAdd) { - if (this.include(classNameToAdd)) return; - this.set(this.toArray().concat(classNameToAdd).join(' ')); + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; }, - remove: function(classNameToRemove) { - if (!this.include(classNameToRemove)) return; - this.set(this.select(function(className) { - return className != classNameToRemove; - }).join(' ')); + findElements: function(root) { + root = root || document; + var e = this.expression, results; + + switch (this.mode) { + case 'selectorsAPI': + // querySelectorAll queries document-wide, then filters to descendants + // of the context element. That's not what we want. + // Add an explicit context to the selector if necessary. + if (root !== document) { + var oldId = root.id, id = $(root).identify(); + e = "#" + id + " " + e; + } + + results = $A(root.querySelectorAll(e)).map(Element.extend); + root.id = oldId; + + return results; + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; }, toString: function() { - return this.toArray().join(' '); + return this.expression; + }, + + inspect: function() { + return "#"; } -} +}); -Object.extend(Element.ClassNames.prototype, Enumerable); -var Field = { - clear: function() { - for (var i = 0; i < arguments.length; i++) - $(arguments[i]).value = ''; +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } }, - focus: function(element) { - $(element).focus(); + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, + attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } }, - present: function() { - for (var i = 0; i < arguments.length; i++) - if ($(arguments[i]).value == '') return false; - return true; + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } }, - select: function(element) { - $(element).select(); + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } }, - activate: function(element) { - element = $(element); - element.focus(); - if (element.select) - element.select(); + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; } -} +}); -/*--------------------------------------------------------------------------*/ +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); +} +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} var Form = { - serialize: function(form) { - var elements = Form.getElements($(form)); - var queryComponents = new Array(); + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); - for (var i = 0; i < elements.length; i++) { - var queryComponent = Form.Element.serialize(elements[i]); - if (queryComponent) - queryComponents.push(queryComponent); - } + return options.hash ? data : Object.toQueryString(data); + } +}; - return queryComponents.join('&'); +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); }, getElements: function(form) { - form = $(form); - var elements = new Array(); - - for (tagName in Form.Element.Serializers) { - var tagElements = form.getElementsByTagName(tagName); - for (var j = 0; j < tagElements.length; j++) - elements.push(tagElements[j]); - } - return elements; + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); - if (!typeName && !name) - return inputs; + if (!typeName && !name) return $A(inputs).map(Element.extend); - var matchingInputs = new Array(); - for (var i = 0; i < inputs.length; i++) { + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; - if ((typeName && input.type != typeName) || - (name && input.name != name)) + if ((typeName && input.type != typeName) || (name && input.name != name)) continue; - matchingInputs.push(input); + matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - element.blur(); - element.disabled = 'true'; - } + form = $(form); + Form.getElements(form).invoke('disable'); + return form; }, enable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - element.disabled = ''; - } + form = $(form); + Form.getElements(form).invoke('enable'); + return form; }, findFirstElement: function(form) { - return Form.getElements(form).find(function(element) { - return element.type != 'hidden' && !element.disabled && - ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, focusFirstElement: function(form) { - Field.activate(Form.findFirstElement(form)); + form = $(form); + form.findFirstElement().activate(); + return form; }, - reset: function(form) { - $(form).reset(); + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); } -} +}; + +/*--------------------------------------------------------------------------*/ Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { element = $(element); var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); + return Form.Element.Serializers[method](element); + }, - if (parameter) { - var key = encodeURIComponent(parameter[0]); - if (key.length == 0) return; + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, - if (parameter[1].constructor != Array) - parameter[1] = [parameter[1]]; + clear: function(element) { + $(element).value = ''; + return element; + }, - return parameter[1].map(function(value) { - return key + '=' + encodeURIComponent(value); - }).join('&'); - } + present: function(element) { + return $(element).value != ''; }, - getValue: function(element) { + activate: function(element) { element = $(element); - var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, - if (parameter) - return parameter[1]; + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; } -} +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ Form.Element.Serializers = { - input: function(element) { + input: function(element, value) { switch (element.type.toLowerCase()) { - case 'submit': - case 'hidden': - case 'password': - case 'text': - return Form.Element.Serializers.textarea(element); case 'checkbox': case 'radio': - return Form.Element.Serializers.inputSelector(element); + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); } - return false; }, - inputSelector: function(element) { - if (element.checked) - return [element.name, element.value]; + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; }, - textarea: function(element) { - return [element.name, element.value]; + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; }, - select: function(element) { - return Form.Element.Serializers[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } }, selectOne: function(element) { - var value = '', opt, index = element.selectedIndex; - if (index >= 0) { - opt = element.options[index]; - value = opt.value; - if (!value && !('value' in opt)) - value = opt.text; - } - return [element.name, value]; + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { - var value = new Array(); - for (var i = 0; i < element.length; i++) { - var opt = element.options[i]; - if (opt.selected) { - var optValue = opt.value; - if (!optValue && !('value' in opt)) - optValue = opt.text; - value.push(optValue); - } - } - return [element.name, value]; - } -} + var values, length = element.length; + if (!length) return null; -/*--------------------------------------------------------------------------*/ + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, -var $F = Form.Element.getValue; + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; /*--------------------------------------------------------------------------*/ -Abstract.TimedObserver = function() {} -Abstract.TimedObserver.prototype = { - initialize: function(element, frequency, callback) { - this.frequency = frequency; +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); this.element = $(element); - this.callback = callback; - this.lastValue = this.getValue(); - this.registerCallback(); - }, - - registerCallback: function() { - setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, - onTimerEvent: function() { + execute: function() { var value = this.getValue(); - if (this.lastValue != value) { + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { this.callback(this.element, value); this.lastValue = value; } } -} +}); -Form.Element.Observer = Class.create(); -Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +Form.Element.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); -Form.Observer = Class.create(); -Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +Form.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.serialize(this.element); } @@ -1386,8 +3732,7 @@ Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { /*--------------------------------------------------------------------------*/ -Abstract.EventObserver = function() {} -Abstract.EventObserver.prototype = { +Abstract.EventObserver = Class.create({ initialize: function(element, callback) { this.element = $(element); this.callback = callback; @@ -1408,9 +3753,7 @@ Abstract.EventObserver.prototype = { }, registerFormCallbacks: function() { - var elements = Form.getElements(this.element); - for (var i = 0; i < elements.length; i++) - this.registerCallback(elements[i]); + Form.getElements(this.element).each(this.registerCallback, this); }, registerCallback: function(element) { @@ -1420,34 +3763,26 @@ Abstract.EventObserver.prototype = { case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; - case 'password': - case 'text': - case 'textarea': - case 'select-one': - case 'select-multiple': + default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } } } -} +}); -Form.Element.EventObserver = Class.create(); -Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); -Form.EventObserver = Class.create(); -Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +Form.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.serialize(this.element); } }); -if (!window.Event) { - var Event = new Object(); -} +if (!window.Event) var Event = { }; Object.extend(Event, { KEY_BACKSPACE: 8, @@ -1459,99 +3794,372 @@ Object.extend(Event, { KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); - element: function(event) { - return event.target || event.srcElement; - }, - - isLeftClick: function(event) { - return (((event.which) && (event.which == 1)) || - ((event.button) && (event.button == 1))); - }, - - pointerX: function(event) { - return event.pageX || (event.clientX + - (document.documentElement.scrollLeft || document.body.scrollLeft)); - }, +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; - pointerY: function(event) { - return event.pageY || (event.clientY + - (document.documentElement.scrollTop || document.body.scrollTop)); - }, + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } - stop: function(event) { - if (event.preventDefault) { + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + event = Event.extend(event); + + var node = event.target, + type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + // Firefox screws up the "click" event when moving between radio buttons + // via arrow keys. It also screws up the "load" and "error" events on images, + // reporting the document as the target instead of the original image. + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; + return Element.extend(node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0, scrollTop: 0 }; + return { + x: event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)), + y: event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); event.preventDefault(); event.stopPropagation(); - } else { - event.returnValue = false; - event.cancelBubble = true; + event.stopped = true; } - }, + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._prototypeEventID) return element._prototypeEventID[0]; + arguments.callee.id = arguments.callee.id || 1; + return element._prototypeEventID = [++arguments.callee.id]; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event); + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } - // find the first node with the given tagName, starting from the - // node the event was triggered on; traverses the DOM upwards - findElement: function(event, tagName) { - var element = Event.element(event); - while (element.parentNode && (!element.tagName || - (element.tagName.toUpperCase() != tagName.toUpperCase()))) - element = element.parentNode; - return element; - }, - observers: false, + // Internet Explorer needs to remove event handlers on page unload + // in order to avoid memory leaks. + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + // Safari has a dummy event handler on page unload so that it won't + // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" + // object when page is returned to via the back button using its bfcache. + if (Prototype.Browser.WebKit) { + window.addEventListener('unload', Prototype.emptyFunction, false); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } - _observeAndCache: function(element, name, observer, useCapture) { - if (!this.observers) this.observers = []; - if (element.addEventListener) { - this.observers.push([element, name, observer, useCapture]); - element.addEventListener(name, observer, useCapture); - } else if (element.attachEvent) { - this.observers.push([element, name, observer, useCapture]); - element.attachEvent('on' + name, observer); + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); } - }, + }; +})()); - unloadCache: function() { - if (!Event.observers) return; - for (var i = 0; i < Event.observers.length; i++) { - Event.stopObserving.apply(this, Event.observers[i]); - Event.observers[i][0] = null; +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + document.loaded = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); } - Event.observers = false; - }, - observe: function(element, name, observer, useCapture) { - var element = $(element); - useCapture = useCapture || false; + } else { + document.write("