git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2458 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/0.9.0
@@ -46,7 +46,7 @@ class AccountController < ApplicationController | |||
self.logged_user = nil | |||
else | |||
# Authenticate user | |||
if using_open_id? && Setting.openid? | |||
if Setting.openid? && using_open_id? | |||
open_id_authenticate(params[:openid_url]) | |||
else | |||
password_authentication |
@@ -140,6 +140,10 @@ class Setting < ActiveRecord::Base | |||
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort | |||
end | |||
def self.openid? | |||
Object.const_defined?(:OpenID) && self['openid'].to_s == '1' | |||
end | |||
# Checks if settings have changed since the values were read | |||
# and clears the cache hash if it's the case | |||
# Called once per request |
@@ -19,7 +19,7 @@ | |||
<%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %><%= hidden_field_tag 'settings[lost_password]', 0 %></p> | |||
<p><label><%= l(:setting_openid) %></label> | |||
<%= check_box_tag 'settings[openid]', 1, Setting.openid? %><%= hidden_field_tag 'settings[openid]', 0 %></p> | |||
<%= check_box_tag 'settings[openid]', 1, Setting.openid?, :disabled => !Object.const_defined?(:OpenID) %><%= hidden_field_tag 'settings[openid]', 0 %></p> | |||
</div> | |||
<div style="float:right;"> |
@@ -64,6 +64,8 @@ class AccountControllerTest < Test::Unit::TestCase | |||
:content => /Invalid user or password/ | |||
end | |||
if Object.const_defined?(:OpenID) | |||
def test_login_with_openid_for_existing_user | |||
Setting.self_registration = '3' | |||
Setting.openid = '1' | |||
@@ -134,6 +136,11 @@ class AccountControllerTest < Test::Unit::TestCase | |||
assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url] | |||
end | |||
else | |||
puts "Skipping openid tests." | |||
end | |||
def test_autologin | |||
Setting.autologin = "7" | |||
Token.delete_all |
@@ -1,34 +1,34 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
class UserTest < Test::Unit::TestCase | |||
fixtures :users, :members, :projects | |||
def setup | |||
@admin = User.find(1) | |||
@jsmith = User.find(2) | |||
@dlopper = User.find(3) | |||
end | |||
def test_truth | |||
assert_kind_of User, @jsmith | |||
end | |||
# redMine - project management software | |||
# Copyright (C) 2006 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
class UserTest < Test::Unit::TestCase | |||
fixtures :users, :members, :projects | |||
def setup | |||
@admin = User.find(1) | |||
@jsmith = User.find(2) | |||
@dlopper = User.find(3) | |||
end | |||
def test_truth | |||
assert_kind_of User, @jsmith | |||
end | |||
def test_create | |||
user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") | |||
@@ -47,160 +47,166 @@ class UserTest < Test::Unit::TestCase | |||
user.password, user.password_confirmation = "password", "password" | |||
assert user.save | |||
end | |||
def test_mail_uniqueness_should_not_be_case_sensitive | |||
u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") | |||
u.login = 'newuser1' | |||
u.password, u.password_confirmation = "password", "password" | |||
assert u.save | |||
u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo") | |||
u.login = 'newuser2' | |||
u.password, u.password_confirmation = "password", "password" | |||
assert !u.save | |||
assert_equal 'activerecord_error_taken', u.errors.on(:mail) | |||
end | |||
def test_update | |||
assert_equal "admin", @admin.login | |||
@admin.login = "john" | |||
assert @admin.save, @admin.errors.full_messages.join("; ") | |||
@admin.reload | |||
assert_equal "john", @admin.login | |||
end | |||
def test_destroy | |||
User.find(2).destroy | |||
assert_nil User.find_by_id(2) | |||
assert Member.find_all_by_user_id(2).empty? | |||
end | |||
def test_validate | |||
@admin.login = "" | |||
assert !@admin.save | |||
assert_equal 1, @admin.errors.count | |||
end | |||
def test_password | |||
user = User.try_to_login("admin", "admin") | |||
assert_kind_of User, user | |||
assert_equal "admin", user.login | |||
user.password = "hello" | |||
assert user.save | |||
user = User.try_to_login("admin", "hello") | |||
assert_kind_of User, user | |||
assert_equal "admin", user.login | |||
assert_equal User.hash_password("hello"), user.hashed_password | |||
end | |||
def test_name_format | |||
assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname) | |||
Setting.user_format = :firstname_lastname | |||
assert_equal 'John Smith', @jsmith.reload.name | |||
Setting.user_format = :username | |||
assert_equal 'jsmith', @jsmith.reload.name | |||
end | |||
def test_lock | |||
user = User.try_to_login("jsmith", "jsmith") | |||
assert_equal @jsmith, user | |||
@jsmith.status = User::STATUS_LOCKED | |||
assert @jsmith.save | |||
user = User.try_to_login("jsmith", "jsmith") | |||
assert_equal nil, user | |||
end | |||
def test_create_anonymous | |||
AnonymousUser.delete_all | |||
anon = User.anonymous | |||
assert !anon.new_record? | |||
assert_kind_of AnonymousUser, anon | |||
end | |||
def test_rss_key | |||
assert_nil @jsmith.rss_token | |||
key = @jsmith.rss_key | |||
assert_equal 40, key.length | |||
@jsmith.reload | |||
assert_equal key, @jsmith.rss_key | |||
end | |||
def test_role_for_project | |||
# user with a role | |||
role = @jsmith.role_for_project(Project.find(1)) | |||
assert_kind_of Role, role | |||
assert_equal "Manager", role.name | |||
# user with no role | |||
assert !@dlopper.role_for_project(Project.find(2)).member? | |||
end | |||
def test_mail_notification_all | |||
@jsmith.mail_notification = true | |||
@jsmith.notified_project_ids = [] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert @jsmith.projects.first.recipients.include?(@jsmith.mail) | |||
end | |||
def test_mail_notification_selected | |||
@jsmith.mail_notification = false | |||
@jsmith.notified_project_ids = [1] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert Project.find(1).recipients.include?(@jsmith.mail) | |||
end | |||
def test_mail_notification_none | |||
@jsmith.mail_notification = false | |||
@jsmith.notified_project_ids = [] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert !@jsmith.projects.first.recipients.include?(@jsmith.mail) | |||
end | |||
def test_comments_sorting_preference | |||
assert !@jsmith.wants_comments_in_reverse_order? | |||
@jsmith.pref.comments_sorting = 'asc' | |||
assert !@jsmith.wants_comments_in_reverse_order? | |||
@jsmith.pref.comments_sorting = 'desc' | |||
assert @jsmith.wants_comments_in_reverse_order? | |||
end | |||
def test_find_by_mail_should_be_case_insensitive | |||
u = User.find_by_mail('JSmith@somenet.foo') | |||
assert_not_nil u | |||
assert_equal 'jsmith@somenet.foo', u.mail | |||
end | |||
def test_random_password | |||
u = User.new | |||
u.random_password | |||
assert !u.password.blank? | |||
assert !u.password_confirmation.blank? | |||
end | |||
def test_setting_identity_url | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'http://example.com/' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
def test_setting_identity_url_without_trailing_slash | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'http://example.com' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
def test_setting_identity_url_without_protocol | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'example.com' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
end | |||
end | |||
def test_mail_uniqueness_should_not_be_case_sensitive | |||
u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") | |||
u.login = 'newuser1' | |||
u.password, u.password_confirmation = "password", "password" | |||
assert u.save | |||
u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo") | |||
u.login = 'newuser2' | |||
u.password, u.password_confirmation = "password", "password" | |||
assert !u.save | |||
assert_equal 'activerecord_error_taken', u.errors.on(:mail) | |||
end | |||
def test_update | |||
assert_equal "admin", @admin.login | |||
@admin.login = "john" | |||
assert @admin.save, @admin.errors.full_messages.join("; ") | |||
@admin.reload | |||
assert_equal "john", @admin.login | |||
end | |||
def test_destroy | |||
User.find(2).destroy | |||
assert_nil User.find_by_id(2) | |||
assert Member.find_all_by_user_id(2).empty? | |||
end | |||
def test_validate | |||
@admin.login = "" | |||
assert !@admin.save | |||
assert_equal 1, @admin.errors.count | |||
end | |||
def test_password | |||
user = User.try_to_login("admin", "admin") | |||
assert_kind_of User, user | |||
assert_equal "admin", user.login | |||
user.password = "hello" | |||
assert user.save | |||
user = User.try_to_login("admin", "hello") | |||
assert_kind_of User, user | |||
assert_equal "admin", user.login | |||
assert_equal User.hash_password("hello"), user.hashed_password | |||
end | |||
def test_name_format | |||
assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname) | |||
Setting.user_format = :firstname_lastname | |||
assert_equal 'John Smith', @jsmith.reload.name | |||
Setting.user_format = :username | |||
assert_equal 'jsmith', @jsmith.reload.name | |||
end | |||
def test_lock | |||
user = User.try_to_login("jsmith", "jsmith") | |||
assert_equal @jsmith, user | |||
@jsmith.status = User::STATUS_LOCKED | |||
assert @jsmith.save | |||
user = User.try_to_login("jsmith", "jsmith") | |||
assert_equal nil, user | |||
end | |||
def test_create_anonymous | |||
AnonymousUser.delete_all | |||
anon = User.anonymous | |||
assert !anon.new_record? | |||
assert_kind_of AnonymousUser, anon | |||
end | |||
def test_rss_key | |||
assert_nil @jsmith.rss_token | |||
key = @jsmith.rss_key | |||
assert_equal 40, key.length | |||
@jsmith.reload | |||
assert_equal key, @jsmith.rss_key | |||
end | |||
def test_role_for_project | |||
# user with a role | |||
role = @jsmith.role_for_project(Project.find(1)) | |||
assert_kind_of Role, role | |||
assert_equal "Manager", role.name | |||
# user with no role | |||
assert !@dlopper.role_for_project(Project.find(2)).member? | |||
end | |||
def test_mail_notification_all | |||
@jsmith.mail_notification = true | |||
@jsmith.notified_project_ids = [] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert @jsmith.projects.first.recipients.include?(@jsmith.mail) | |||
end | |||
def test_mail_notification_selected | |||
@jsmith.mail_notification = false | |||
@jsmith.notified_project_ids = [1] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert Project.find(1).recipients.include?(@jsmith.mail) | |||
end | |||
def test_mail_notification_none | |||
@jsmith.mail_notification = false | |||
@jsmith.notified_project_ids = [] | |||
@jsmith.save | |||
@jsmith.reload | |||
assert !@jsmith.projects.first.recipients.include?(@jsmith.mail) | |||
end | |||
def test_comments_sorting_preference | |||
assert !@jsmith.wants_comments_in_reverse_order? | |||
@jsmith.pref.comments_sorting = 'asc' | |||
assert !@jsmith.wants_comments_in_reverse_order? | |||
@jsmith.pref.comments_sorting = 'desc' | |||
assert @jsmith.wants_comments_in_reverse_order? | |||
end | |||
def test_find_by_mail_should_be_case_insensitive | |||
u = User.find_by_mail('JSmith@somenet.foo') | |||
assert_not_nil u | |||
assert_equal 'jsmith@somenet.foo', u.mail | |||
end | |||
def test_random_password | |||
u = User.new | |||
u.random_password | |||
assert !u.password.blank? | |||
assert !u.password_confirmation.blank? | |||
end | |||
if Object.const_defined?(:OpenID) | |||
def test_setting_identity_url | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'http://example.com/' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
def test_setting_identity_url_without_trailing_slash | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'http://example.com' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
def test_setting_identity_url_without_protocol | |||
normalized_open_id_url = 'http://example.com/' | |||
u = User.new( :identity_url => 'example.com' ) | |||
assert_equal normalized_open_id_url, u.identity_url | |||
end | |||
else | |||
puts "Skipping openid tests." | |||
end | |||
end |
@@ -1,290 +0,0 @@ | |||
--- !ruby/object:Gem::Specification | |||
name: ruby-openid | |||
version: !ruby/object:Gem::Version | |||
version: 2.1.4 | |||
platform: ruby | |||
authors: | |||
- JanRain, Inc | |||
autorequire: openid | |||
bindir: bin | |||
cert_chain: | |||
date: 2008-12-19 00:00:00 -08:00 | |||
default_executable: | |||
dependencies: [] | |||
description: | |||
email: openid@janrain.com | |||
executables: [] | |||
extensions: [] | |||
extra_rdoc_files: | |||
- README | |||
- INSTALL | |||
- LICENSE | |||
- UPGRADE | |||
files: | |||
- examples/README | |||
- examples/active_record_openid_store | |||
- examples/rails_openid | |||
- examples/discover | |||
- examples/active_record_openid_store/lib | |||
- examples/active_record_openid_store/test | |||
- examples/active_record_openid_store/init.rb | |||
- examples/active_record_openid_store/README | |||
- examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb | |||
- examples/active_record_openid_store/XXX_upgrade_open_id_store.rb | |||
- examples/active_record_openid_store/lib/association.rb | |||
- examples/active_record_openid_store/lib/nonce.rb | |||
- examples/active_record_openid_store/lib/open_id_setting.rb | |||
- examples/active_record_openid_store/lib/openid_ar_store.rb | |||
- examples/active_record_openid_store/test/store_test.rb | |||
- examples/rails_openid/app | |||
- examples/rails_openid/components | |||
- examples/rails_openid/config | |||
- examples/rails_openid/db | |||
- examples/rails_openid/doc | |||
- examples/rails_openid/lib | |||
- examples/rails_openid/log | |||
- examples/rails_openid/public | |||
- examples/rails_openid/script | |||
- examples/rails_openid/test | |||
- examples/rails_openid/vendor | |||
- examples/rails_openid/Rakefile | |||
- examples/rails_openid/README | |||
- examples/rails_openid/app/controllers | |||
- examples/rails_openid/app/helpers | |||
- examples/rails_openid/app/models | |||
- examples/rails_openid/app/views | |||
- examples/rails_openid/app/controllers/application.rb | |||
- examples/rails_openid/app/controllers/login_controller.rb | |||
- examples/rails_openid/app/controllers/server_controller.rb | |||
- examples/rails_openid/app/controllers/consumer_controller.rb | |||
- examples/rails_openid/app/helpers/application_helper.rb | |||
- examples/rails_openid/app/helpers/login_helper.rb | |||
- examples/rails_openid/app/helpers/server_helper.rb | |||
- examples/rails_openid/app/views/layouts | |||
- examples/rails_openid/app/views/login | |||
- examples/rails_openid/app/views/server | |||
- examples/rails_openid/app/views/consumer | |||
- examples/rails_openid/app/views/layouts/server.rhtml | |||
- examples/rails_openid/app/views/login/index.rhtml | |||
- examples/rails_openid/app/views/server/decide.rhtml | |||
- examples/rails_openid/app/views/consumer/index.rhtml | |||
- examples/rails_openid/config/environments | |||
- examples/rails_openid/config/database.yml | |||
- examples/rails_openid/config/boot.rb | |||
- examples/rails_openid/config/environment.rb | |||
- examples/rails_openid/config/routes.rb | |||
- examples/rails_openid/config/environments/development.rb | |||
- examples/rails_openid/config/environments/production.rb | |||
- examples/rails_openid/config/environments/test.rb | |||
- examples/rails_openid/doc/README_FOR_APP | |||
- examples/rails_openid/lib/tasks | |||
- examples/rails_openid/public/images | |||
- examples/rails_openid/public/javascripts | |||
- examples/rails_openid/public/stylesheets | |||
- examples/rails_openid/public/dispatch.cgi | |||
- examples/rails_openid/public/404.html | |||
- examples/rails_openid/public/500.html | |||
- examples/rails_openid/public/dispatch.fcgi | |||
- examples/rails_openid/public/dispatch.rb | |||
- examples/rails_openid/public/favicon.ico | |||
- examples/rails_openid/public/robots.txt | |||
- examples/rails_openid/public/images/openid_login_bg.gif | |||
- examples/rails_openid/public/javascripts/controls.js | |||
- examples/rails_openid/public/javascripts/dragdrop.js | |||
- examples/rails_openid/public/javascripts/effects.js | |||
- examples/rails_openid/public/javascripts/prototype.js | |||
- examples/rails_openid/script/performance | |||
- examples/rails_openid/script/process | |||
- examples/rails_openid/script/console | |||
- examples/rails_openid/script/about | |||
- examples/rails_openid/script/breakpointer | |||
- examples/rails_openid/script/destroy | |||
- examples/rails_openid/script/generate | |||
- examples/rails_openid/script/plugin | |||
- examples/rails_openid/script/runner | |||
- examples/rails_openid/script/server | |||
- examples/rails_openid/script/performance/benchmarker | |||
- examples/rails_openid/script/performance/profiler | |||
- examples/rails_openid/script/process/spawner | |||
- examples/rails_openid/script/process/reaper | |||
- examples/rails_openid/script/process/spinner | |||
- examples/rails_openid/test/fixtures | |||
- examples/rails_openid/test/functional | |||
- examples/rails_openid/test/mocks | |||
- examples/rails_openid/test/unit | |||
- examples/rails_openid/test/test_helper.rb | |||
- examples/rails_openid/test/functional/login_controller_test.rb | |||
- examples/rails_openid/test/functional/server_controller_test.rb | |||
- examples/rails_openid/test/mocks/development | |||
- examples/rails_openid/test/mocks/test | |||
- lib/openid | |||
- lib/hmac | |||
- lib/openid.rb | |||
- lib/openid/cryptutil.rb | |||
- lib/openid/extras.rb | |||
- lib/openid/urinorm.rb | |||
- lib/openid/util.rb | |||
- lib/openid/trustroot.rb | |||
- lib/openid/message.rb | |||
- lib/openid/yadis | |||
- lib/openid/consumer | |||
- lib/openid/fetchers.rb | |||
- lib/openid/dh.rb | |||
- lib/openid/kvform.rb | |||
- lib/openid/association.rb | |||
- lib/openid/store | |||
- lib/openid/kvpost.rb | |||
- lib/openid/extensions | |||
- lib/openid/protocolerror.rb | |||
- lib/openid/server.rb | |||
- lib/openid/extension.rb | |||
- lib/openid/consumer.rb | |||
- lib/openid/yadis/htmltokenizer.rb | |||
- lib/openid/yadis/parsehtml.rb | |||
- lib/openid/yadis/filters.rb | |||
- lib/openid/yadis/xrds.rb | |||
- lib/openid/yadis/accept.rb | |||
- lib/openid/yadis/constants.rb | |||
- lib/openid/yadis/discovery.rb | |||
- lib/openid/yadis/xri.rb | |||
- lib/openid/yadis/xrires.rb | |||
- lib/openid/yadis/services.rb | |||
- lib/openid/consumer/html_parse.rb | |||
- lib/openid/consumer/idres.rb | |||
- lib/openid/consumer/associationmanager.rb | |||
- lib/openid/consumer/discovery.rb | |||
- lib/openid/consumer/discovery_manager.rb | |||
- lib/openid/consumer/checkid_request.rb | |||
- lib/openid/consumer/responses.rb | |||
- lib/openid/store/filesystem.rb | |||
- lib/openid/store/interface.rb | |||
- lib/openid/store/nonce.rb | |||
- lib/openid/store/memory.rb | |||
- lib/openid/extensions/sreg.rb | |||
- lib/openid/extensions/ax.rb | |||
- lib/openid/extensions/pape.rb | |||
- lib/hmac/hmac.rb | |||
- lib/hmac/sha1.rb | |||
- lib/hmac/sha2.rb | |||
- test/data | |||
- test/test_association.rb | |||
- test/test_urinorm.rb | |||
- test/testutil.rb | |||
- test/test_util.rb | |||
- test/test_message.rb | |||
- test/test_cryptutil.rb | |||
- test/test_extras.rb | |||
- test/util.rb | |||
- test/test_trustroot.rb | |||
- test/test_parsehtml.rb | |||
- test/test_fetchers.rb | |||
- test/test_dh.rb | |||
- test/test_kvform.rb | |||
- test/test_openid_yadis.rb | |||
- test/test_linkparse.rb | |||
- test/test_stores.rb | |||
- test/test_filters.rb | |||
- test/test_xrds.rb | |||
- test/test_nonce.rb | |||
- test/test_accept.rb | |||
- test/test_kvpost.rb | |||
- test/test_associationmanager.rb | |||
- test/discoverdata.rb | |||
- test/test_server.rb | |||
- test/test_yadis_discovery.rb | |||
- test/test_sreg.rb | |||
- test/test_idres.rb | |||
- test/test_ax.rb | |||
- test/test_xri.rb | |||
- test/test_xrires.rb | |||
- test/test_discover.rb | |||
- test/test_consumer.rb | |||
- test/test_pape.rb | |||
- test/test_checkid_request.rb | |||
- test/test_discovery_manager.rb | |||
- test/test_responses.rb | |||
- test/test_extension.rb | |||
- test/data/test_xrds | |||
- test/data/urinorm.txt | |||
- test/data/n2b64 | |||
- test/data/trustroot.txt | |||
- test/data/dh.txt | |||
- test/data/test1-parsehtml.txt | |||
- test/data/linkparse.txt | |||
- test/data/accept.txt | |||
- test/data/test_discover | |||
- test/data/example-xrds.xml | |||
- test/data/test1-discover.txt | |||
- test/data/test_xrds/ref.xrds | |||
- test/data/test_xrds/README | |||
- test/data/test_xrds/delegated-20060809-r1.xrds | |||
- test/data/test_xrds/delegated-20060809-r2.xrds | |||
- test/data/test_xrds/delegated-20060809.xrds | |||
- test/data/test_xrds/no-xrd.xml | |||
- test/data/test_xrds/not-xrds.xml | |||
- test/data/test_xrds/prefixsometimes.xrds | |||
- test/data/test_xrds/sometimesprefix.xrds | |||
- test/data/test_xrds/spoof1.xrds | |||
- test/data/test_xrds/spoof2.xrds | |||
- test/data/test_xrds/spoof3.xrds | |||
- test/data/test_xrds/status222.xrds | |||
- test/data/test_xrds/valid-populated-xrds.xml | |||
- test/data/test_xrds/=j3h.2007.11.14.xrds | |||
- test/data/test_xrds/subsegments.xrds | |||
- test/data/test_discover/openid2_xrds.xml | |||
- test/data/test_discover/openid.html | |||
- test/data/test_discover/openid2.html | |||
- test/data/test_discover/openid2_xrds_no_local_id.xml | |||
- test/data/test_discover/openid_1_and_2.html | |||
- test/data/test_discover/openid_1_and_2_xrds.xml | |||
- test/data/test_discover/openid_and_yadis.html | |||
- test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml | |||
- test/data/test_discover/openid_no_delegate.html | |||
- test/data/test_discover/yadis_0entries.xml | |||
- test/data/test_discover/yadis_2_bad_local_id.xml | |||
- test/data/test_discover/yadis_2entries_delegate.xml | |||
- test/data/test_discover/yadis_2entries_idp.xml | |||
- test/data/test_discover/yadis_another_delegate.xml | |||
- test/data/test_discover/yadis_idp.xml | |||
- test/data/test_discover/yadis_idp_delegate.xml | |||
- test/data/test_discover/yadis_no_delegate.xml | |||
- test/data/test_discover/malformed_meta_tag.html | |||
- NOTICE | |||
- CHANGELOG | |||
- README | |||
- INSTALL | |||
- LICENSE | |||
- UPGRADE | |||
- admin/runtests.rb | |||
has_rdoc: true | |||
homepage: http://openidenabled.com/ruby-openid/ | |||
post_install_message: | |||
rdoc_options: | |||
- --main | |||
- README | |||
require_paths: | |||
- lib | |||
required_ruby_version: !ruby/object:Gem::Requirement | |||
requirements: | |||
- - ">" | |||
- !ruby/object:Gem::Version | |||
version: 0.0.0 | |||
version: | |||
required_rubygems_version: !ruby/object:Gem::Requirement | |||
requirements: | |||
- - ">=" | |||
- !ruby/object:Gem::Version | |||
version: "0" | |||
version: | |||
requirements: [] | |||
rubyforge_project: | |||
rubygems_version: 1.3.1 | |||
signing_key: | |||
specification_version: 1 | |||
summary: A library for consuming and serving OpenID identities. | |||
test_files: | |||
- admin/runtests.rb |
@@ -1,11 +0,0 @@ | |||
Fri Dec 19 11:50:10 PST 2008 cygnus@janrain.com | |||
tagged 2.1.4 | |||
Fri Dec 19 11:48:25 PST 2008 cygnus@janrain.com | |||
* Version: 2.1.4 | |||
Fri Dec 19 11:42:47 PST 2008 cygnus@janrain.com | |||
* Normalize XRIs when doing discovery in accordance with the OpenID 2 spec | |||
Tue Dec 16 13:14:07 PST 2008 cygnus@janrain.com | |||
tagged 2.1.3 |
@@ -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. |
@@ -1,210 +0,0 @@ | |||
The code in lib/hmac/ is Copyright 2001 by Daiki Ueno, and distributed under | |||
the terms of the Ruby license. See http://www.ruby-lang.org/en/LICENSE.txt | |||
lib/openid/yadis/htmltokenizer.rb is Copyright 2004 by Ben Giddings and | |||
distributed under the terms of the Ruby license. | |||
The remainder of this package is Copyright 2006-2008 by JanRain, Inc. and | |||
distributed under the terms of license below: | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS | |||
APPENDIX: How to apply the Apache License to your work. | |||
To apply the Apache License to your work, attach the following | |||
boilerplate notice, with the fields enclosed by brackets "[]" | |||
replaced with your own identifying information. (Don't include | |||
the brackets!) The text should be enclosed in the appropriate | |||
comment syntax for the file format. We also recommend that a | |||
file or class name and description of purpose be included on the | |||
same "printed page" as the copyright notice for easier | |||
identification within third-party archives. | |||
Copyright [yyyy] [name of copyright owner] | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at | |||
http://www.apache.org/licenses/LICENSE-2.0 | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@@ -1,2 +0,0 @@ | |||
This product includes software developed by JanRain, | |||
available from http://openidenabled.com/ |
@@ -1,82 +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://openidenabled.com/ruby-openid/ | |||
See also: | |||
http://openid.net/ | |||
http://openidenabled.com/ | |||
==Community | |||
Discussion regarding the Ruby OpenID library and other JanRain OpenID | |||
libraries takes place on the the OpenID mailing list on | |||
openidenabled.com. | |||
http://lists.openidenabled.com/mailman/listinfo/dev | |||
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. |
@@ -1,127 +0,0 @@ | |||
= Upgrading from the OpenID 1.x series library | |||
== 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 | |||
=== 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 | |||
The format has changed, and you should remove your old store directory. | |||
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. | |||
Also, note that the stores now can be garbage collected with the method | |||
store.cleanup | |||
=== 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 | |||
Data regarding the OpenID server once lived in | |||
openid_request.service | |||
The corresponding object in the 2.0 lib can be retrieved with | |||
openid_request.endpoint | |||
Getting the unverified identifier: Where you once had | |||
openid_request.identity_url | |||
you will now want | |||
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 | |||
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' | |||
before calling consumer.begin. | |||
=== Requesting Simple Registration Data | |||
You'll need to require the code for the extension | |||
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) | |||
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. | |||
=== 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) | |||
The response still uses the status codes, but they are now namespaced | |||
slightly differently, for example OpenID::Consumer::SUCCESS | |||
In the case of failure, the error message is now found in | |||
openid_response.message | |||
The identifier to display to the user can be found in | |||
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. | |||
== 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 | |||
constructor: | |||
@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. | |||
-- | |||
Dag Arneson, JanRain Inc. | |||
Please direct questions to openid@janrain.com |
@@ -1,36 +0,0 @@ | |||
#!/usr/bin/ruby | |||
require "logger" | |||
require "stringio" | |||
require "pathname" | |||
require 'test/unit/collector/dir' | |||
require 'test/unit/ui/console/testrunner' | |||
def main | |||
old_verbose = $VERBOSE | |||
$VERBOSE = true | |||
tests_dir = Pathname.new(__FILE__).dirname.dirname.join('test') | |||
# Collect tests from everything named test_*.rb. | |||
c = Test::Unit::Collector::Dir.new | |||
if c.respond_to?(:base=) | |||
# In order to supress warnings from ruby 1.8.6 about accessing | |||
# undefined member | |||
c.base = tests_dir | |||
suite = c.collect | |||
else | |||
# Because base is not defined in ruby < 1.8.6 | |||
suite = c.collect(tests_dir) | |||
end | |||
result = Test::Unit::UI::Console::TestRunner.run(suite) | |||
result.passed? | |||
ensure | |||
$VERBOSE = old_verbose | |||
end | |||
exit(main) |
@@ -1,32 +0,0 @@ | |||
This directory contains several examples that demonstrate use of the | |||
OpenID library. Make sure you have properly installed the library | |||
before running the examples. These examples are a great place to | |||
start in integrating OpenID into your application. | |||
==Rails example | |||
The rails_openid contains a fully functional OpenID server and relying | |||
party, and acts as a starting point for implementing your own | |||
production rails server. You'll need the latest version of Ruby on | |||
Rails installed, and then: | |||
cd rails_openid | |||
./script/server | |||
Open a web browser to http://localhost:3000/ and follow the instructions. | |||
The relevant code to work from when writing your Rails OpenID Relying | |||
Party is: | |||
rails_openid/app/controllers/consumer_controller.rb | |||
If you are working on an OpenID provider, check out | |||
rails_openid/app/controllers/server_controller.rb | |||
Since the library and examples are Apache-licensed, don't be shy about | |||
copy-and-paste. | |||
==Rails ActiveRecord OpenIDStore plugin | |||
For various reasons you may want or need to deploy your ruby openid | |||
consumer/server using an SQL based store. The active_record_openid_store | |||
is a plugin that makes using an SQL based store simple. Follow the | |||
README inside the plugin's dir for usage. |
@@ -1,58 +0,0 @@ | |||
=Active Record OpenID Store Plugin | |||
A store is required by an OpenID server and optionally by the consumer | |||
to store associations, nonces, and auth key information across | |||
requests and processes. If rails is distributed across several | |||
machines, they must must all have access to the same OpenID store | |||
data, so the FilesystemStore won't do. | |||
This directory contains a plugin for connecting your | |||
OpenID enabled rails app to an ActiveRecord based OpenID store. | |||
==Install | |||
1) Copy this directory and all it's contents into your | |||
RAILS_ROOT/vendor/plugins directory. You structure should look like | |||
this: | |||
RAILS_ROOT/vendor/plugins/active_record_openid_store/ | |||
2) Copy the migration, XXX_add_open_id_store_to_db.rb to your | |||
RAILS_ROOT/db/migrate directory. Rename the XXX portion of the | |||
file to next sequential migration number. | |||
3) Run the migration: | |||
rake migrate | |||
4) Change your app to use the ActiveRecordOpenIDStore: | |||
store = ActiveRecordOpenIDStore.new | |||
consumer = OpenID::Consumer.new(session, store) | |||
5) That's it! All your OpenID state will now be stored in the database. | |||
==Upgrade | |||
If you are upgrading from the 1.x ActiveRecord store, replace your old | |||
RAILS_ROOT/vendor/plugins/active_record_openid_store/ directory with | |||
the new one and run the migration XXX_upgrade_open_id_store.rb. | |||
==What about garbage collection? | |||
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' | |||
task :gc_openid_store => :environment do | |||
ActiveRecordOpenIDStore.new.cleanup | |||
end | |||
Run it by typing: | |||
rake gc_openid_store | |||
==Questions? | |||
Contact Dag Arneson: dag at janrain dot com |
@@ -1,24 +0,0 @@ | |||
# Use this migration to create the tables for the ActiveRecord store | |||
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 "handle", :string, :null => false | |||
t.column "secret", :binary, :null => false | |||
t.column "issued", :integer, :null => false | |||
t.column "lifetime", :integer, :null => false | |||
t.column "assoc_type", :string, :null => false | |||
end | |||
create_table "open_id_nonces", :force => true do |t| | |||
t.column :server_url, :string, :null => false | |||
t.column :timestamp, :integer, :null => false | |||
t.column :salt, :string, :null => false | |||
end | |||
end | |||
def self.down | |||
drop_table "open_id_associations" | |||
drop_table "open_id_nonces" | |||
end | |||
end |
@@ -1,26 +0,0 @@ | |||
# Use this migration to upgrade the old 1.1 ActiveRecord store schema | |||
# to the new 2.0 schema. | |||
class UpgradeOpenIdStore < ActiveRecord::Migration | |||
def self.up | |||
drop_table "open_id_settings" | |||
drop_table "open_id_nonces" | |||
create_table "open_id_nonces", :force => true do |t| | |||
t.column :server_url, :string, :null => false | |||
t.column :timestamp, :integer, :null => false | |||
t.column :salt, :string, :null => false | |||
end | |||
end | |||
def self.down | |||
drop_table "open_id_nonces" | |||
create_table "open_id_nonces", :force => true do |t| | |||
t.column "nonce", :string | |||
t.column "created", :integer | |||
end | |||
create_table "open_id_settings", :force => true do |t| | |||
t.column "setting", :string | |||
t.column "value", :binary | |||
end | |||
end | |||
end |
@@ -1,8 +0,0 @@ | |||
# might using the ruby-openid gem | |||
begin | |||
require 'rubygems' | |||
rescue LoadError | |||
nil | |||
end | |||
require 'openid' | |||
require 'openid_ar_store' |
@@ -1,10 +0,0 @@ | |||
require 'openid/association' | |||
require 'time' | |||
class Association < ActiveRecord::Base | |||
set_table_name 'open_id_associations' | |||
def from_record | |||
OpenID::Association.new(handle, secret, Time.at(issued), lifetime, assoc_type) | |||
end | |||
end | |||
@@ -1,3 +0,0 @@ | |||
class Nonce < ActiveRecord::Base | |||
set_table_name 'open_id_nonces' | |||
end |
@@ -1,4 +0,0 @@ | |||
class OpenIdSetting < ActiveRecord::Base | |||
validates_uniqueness_of :setting | |||
end |
@@ -1,57 +0,0 @@ | |||
require 'association' | |||
require 'nonce' | |||
require 'openid/store/interface' | |||
# not in OpenID module to avoid namespace conflict | |||
class ActiveRecordStore < OpenID::Store::Interface | |||
def store_association(server_url, assoc) | |||
remove_association(server_url, assoc.handle) | |||
Association.create!(:server_url => server_url, | |||
:handle => assoc.handle, | |||
:secret => assoc.secret, | |||
:issued => assoc.issued.to_i, | |||
:lifetime => assoc.lifetime, | |||
:assoc_type => assoc.assoc_type) | |||
end | |||
def get_association(server_url, handle=nil) | |||
assocs = if handle.blank? | |||
Association.find_all_by_server_url(server_url) | |||
else | |||
Association.find_all_by_server_url_and_handle(server_url, handle) | |||
end | |||
assocs.reverse.each do |assoc| | |||
a = assoc.from_record | |||
if a.expires_in == 0 | |||
assoc.destroy | |||
else | |||
return a | |||
end | |||
end if assocs.any? | |||
return nil | |||
end | |||
def remove_association(server_url, handle) | |||
Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0 | |||
end | |||
def use_nonce(server_url, timestamp, salt) | |||
return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt) | |||
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew | |||
Nonce.create!(:server_url => server_url, :timestamp => timestamp, :salt => salt) | |||
return true | |||
end | |||
def cleanup_nonces | |||
now = Time.now.to_i | |||
Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew]) | |||
end | |||
def cleanup_associations | |||
now = Time.now.to_i | |||
Association.delete_all(['issued + lifetime > ?',now]) | |||
end | |||
end |
@@ -1,212 +0,0 @@ | |||
$:.unshift(File.dirname(__FILE__) + '/../lib') | |||
require 'test/unit' | |||
RAILS_ENV = "test" | |||
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb')) | |||
module StoreTestCase | |||
@@allowed_handle = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' | |||
@@allowed_nonce = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | |||
def _gen_nonce | |||
OpenID::CryptUtil.random_string(8, @@allowed_nonce) | |||
end | |||
def _gen_handle(n) | |||
OpenID::CryptUtil.random_string(n, @@allowed_handle) | |||
end | |||
def _gen_secret(n, chars=nil) | |||
OpenID::CryptUtil.random_string(n, chars) | |||
end | |||
def _gen_assoc(issued, lifetime=600) | |||
secret = _gen_secret(20) | |||
handle = _gen_handle(128) | |||
OpenID::Association.new(handle, secret, Time.now + issued, lifetime, | |||
'HMAC-SHA1') | |||
end | |||
def _check_retrieve(url, handle=nil, expected=nil) | |||
ret_assoc = @store.get_association(url, handle) | |||
if expected.nil? | |||
assert_nil(ret_assoc) | |||
else | |||
assert_equal(expected, ret_assoc) | |||
assert_equal(expected.handle, ret_assoc.handle) | |||
assert_equal(expected.secret, ret_assoc.secret) | |||
end | |||
end | |||
def _check_remove(url, handle, expected) | |||
present = @store.remove_association(url, handle) | |||
assert_equal(expected, present) | |||
end | |||
def test_store | |||
server_url = "http://www.myopenid.com/openid" | |||
assoc = _gen_assoc(issued=0) | |||
# Make sure that a missing association returns no result | |||
_check_retrieve(server_url) | |||
# Check that after storage, getting returns the same result | |||
@store.store_association(server_url, assoc) | |||
_check_retrieve(server_url, nil, assoc) | |||
# more than once | |||
_check_retrieve(server_url, nil, assoc) | |||
# Storing more than once has no ill effect | |||
@store.store_association(server_url, assoc) | |||
_check_retrieve(server_url, nil, assoc) | |||
# Removing an association that does not exist returns not present | |||
_check_remove(server_url, assoc.handle + 'x', false) | |||
# Removing an association that does not exist returns not present | |||
_check_remove(server_url + 'x', assoc.handle, false) | |||
# Removing an association that is present returns present | |||
_check_remove(server_url, assoc.handle, true) | |||
# but not present on subsequent calls | |||
_check_remove(server_url, assoc.handle, false) | |||
# Put assoc back in the store | |||
@store.store_association(server_url, assoc) | |||
# More recent and expires after assoc | |||
assoc2 = _gen_assoc(issued=1) | |||
@store.store_association(server_url, assoc2) | |||
# After storing an association with a different handle, but the | |||
# same server_url, the handle with the later expiration is returned. | |||
_check_retrieve(server_url, nil, assoc2) | |||
# We can still retrieve the older association | |||
_check_retrieve(server_url, assoc.handle, assoc) | |||
# Plus we can retrieve the association with the later expiration | |||
# explicitly | |||
_check_retrieve(server_url, assoc2.handle, assoc2) | |||
# More recent, and expires earlier than assoc2 or assoc. Make sure | |||
# that we're picking the one with the latest issued date and not | |||
# taking into account the expiration. | |||
assoc3 = _gen_assoc(issued=2, lifetime=100) | |||
@store.store_association(server_url, assoc3) | |||
_check_retrieve(server_url, nil, assoc3) | |||
_check_retrieve(server_url, assoc.handle, assoc) | |||
_check_retrieve(server_url, assoc2.handle, assoc2) | |||
_check_retrieve(server_url, assoc3.handle, assoc3) | |||
_check_remove(server_url, assoc2.handle, true) | |||
_check_retrieve(server_url, nil, assoc3) | |||
_check_retrieve(server_url, assoc.handle, assoc) | |||
_check_retrieve(server_url, assoc2.handle, nil) | |||
_check_retrieve(server_url, assoc3.handle, assoc3) | |||
_check_remove(server_url, assoc2.handle, false) | |||
_check_remove(server_url, assoc3.handle, true) | |||
_check_retrieve(server_url, nil, assoc) | |||
_check_retrieve(server_url, assoc.handle, assoc) | |||
_check_retrieve(server_url, assoc2.handle, nil) | |||
_check_retrieve(server_url, assoc3.handle, nil) | |||
_check_remove(server_url, assoc2.handle, false) | |||
_check_remove(server_url, assoc.handle, true) | |||
_check_remove(server_url, assoc3.handle, false) | |||
_check_retrieve(server_url, nil, nil) | |||
_check_retrieve(server_url, assoc.handle, nil) | |||
_check_retrieve(server_url, assoc2.handle, nil) | |||
_check_retrieve(server_url, assoc3.handle, nil) | |||
_check_remove(server_url, assoc2.handle, false) | |||
_check_remove(server_url, assoc.handle, false) | |||
_check_remove(server_url, assoc3.handle, false) | |||
assocValid1 = _gen_assoc(-3600, 7200) | |||
assocValid2 = _gen_assoc(-5) | |||
assocExpired1 = _gen_assoc(-7200, 3600) | |||
assocExpired2 = _gen_assoc(-7200, 3600) | |||
@store.cleanup_associations | |||
@store.store_association(server_url + '1', assocValid1) | |||
@store.store_association(server_url + '1', assocExpired1) | |||
@store.store_association(server_url + '2', assocExpired2) | |||
@store.store_association(server_url + '3', assocValid2) | |||
cleaned = @store.cleanup_associations() | |||
assert_equal(2, cleaned, "cleaned up associations") | |||
end | |||
def _check_use_nonce(nonce, expected, server_url, msg='') | |||
stamp, salt = OpenID::Nonce::split_nonce(nonce) | |||
actual = @store.use_nonce(server_url, stamp, salt) | |||
assert_equal(expected, actual, msg) | |||
end | |||
def test_nonce | |||
server_url = "http://www.myopenid.com/openid" | |||
[server_url, ''].each{|url| | |||
nonce1 = OpenID::Nonce::mk_nonce | |||
_check_use_nonce(nonce1, true, url, "#{url}: nonce allowed by default") | |||
_check_use_nonce(nonce1, false, url, "#{url}: nonce not allowed twice") | |||
_check_use_nonce(nonce1, false, url, "#{url}: nonce not allowed third time") | |||
# old nonces shouldn't pass | |||
old_nonce = OpenID::Nonce::mk_nonce(3600) | |||
_check_use_nonce(old_nonce, false, url, "Old nonce #{old_nonce.inspect} passed") | |||
} | |||
now = Time.now.to_i | |||
old_nonce1 = OpenID::Nonce::mk_nonce(now - 20000) | |||
old_nonce2 = OpenID::Nonce::mk_nonce(now - 10000) | |||
recent_nonce = OpenID::Nonce::mk_nonce(now - 600) | |||
orig_skew = OpenID::Nonce.skew | |||
OpenID::Nonce.skew = 0 | |||
count = @store.cleanup_nonces | |||
OpenID::Nonce.skew = 1000000 | |||
ts, salt = OpenID::Nonce::split_nonce(old_nonce1) | |||
assert(@store.use_nonce(server_url, ts, salt), "oldnonce1") | |||
ts, salt = OpenID::Nonce::split_nonce(old_nonce2) | |||
assert(@store.use_nonce(server_url, ts, salt), "oldnonce2") | |||
ts, salt = OpenID::Nonce::split_nonce(recent_nonce) | |||
assert(@store.use_nonce(server_url, ts, salt), "recent_nonce") | |||
OpenID::Nonce.skew = 1000 | |||
cleaned = @store.cleanup_nonces | |||
assert_equal(2, cleaned, "Cleaned #{cleaned} nonces") | |||
OpenID::Nonce.skew = 100000 | |||
ts, salt = OpenID::Nonce::split_nonce(old_nonce1) | |||
assert(@store.use_nonce(server_url, ts, salt), "oldnonce1 after cleanup") | |||
ts, salt = OpenID::Nonce::split_nonce(old_nonce2) | |||
assert(@store.use_nonce(server_url, ts, salt), "oldnonce2 after cleanup") | |||
ts, salt = OpenID::Nonce::split_nonce(recent_nonce) | |||
assert(!@store.use_nonce(server_url, ts, salt), "recent_nonce after cleanup") | |||
OpenID::Nonce.skew = orig_skew | |||
end | |||
end | |||
class TestARStore < Test::Unit::TestCase | |||
include StoreTestCase | |||
def setup | |||
@store = ActiveRecordStore.new | |||
end | |||
end | |||
@@ -1,49 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require "openid/consumer/discovery" | |||
require 'openid/fetchers' | |||
OpenID::fetcher_use_env_http_proxy | |||
$names = [[:server_url, "Server URL "], | |||
[:local_id, "Local ID "], | |||
[:canonical_id, "Canonical ID"], | |||
] | |||
def show_services(user_input, normalized, services) | |||
puts " Claimed identifier: #{normalized}" | |||
if services.empty? | |||
puts " No OpenID services found" | |||
puts | |||
else | |||
puts " Discovered services:" | |||
n = 0 | |||
services.each do |service| | |||
n += 1 | |||
puts " #{n}." | |||
$names.each do |meth, name| | |||
val = service.send(meth) | |||
if val | |||
printf(" %s: %s\n", name, val) | |||
end | |||
end | |||
puts " Type URIs:" | |||
for type_uri in service.type_uris | |||
puts " * #{type_uri}" | |||
end | |||
puts | |||
end | |||
end | |||
end | |||
ARGV.each do |openid_identifier| | |||
puts "=" * 50 | |||
puts "Running discovery on #{openid_identifier}" | |||
begin | |||
normalized_identifier, services = OpenID.discover(openid_identifier) | |||
rescue OpenID::DiscoveryFailure => why | |||
puts "Discovery failed: #{why.message}" | |||
puts | |||
else | |||
show_services(openid_identifier, normalized_identifier, services) | |||
end | |||
end |
@@ -1,153 +0,0 @@ | |||
== Welcome to Rails | |||
Rails is a web-application and persistence framework that includes everything | |||
needed to create database-backed web-applications according to the | |||
Model-View-Control pattern of separation. 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. Run the WEBrick servlet: <tt>ruby script/server</tt> (run with --help for options) | |||
...or if you have lighttpd installed: <tt>ruby script/lighttpd</tt> (it's faster) | |||
2. Go to http://localhost:3000/ and get "Congratulations, you've put Ruby on Rails!" | |||
3. Follow the guidelines on the "Congratulations, you've put Ruby on Rails!" screen | |||
== Example for Apache conf | |||
<VirtualHost *:80> | |||
ServerName rails | |||
DocumentRoot /path/application/public/ | |||
ErrorLog /path/application/log/server.log | |||
<Directory /path/application/public/> | |||
Options ExecCGI FollowSymLinks | |||
AllowOverride all | |||
Allow from all | |||
Order allow,deny | |||
</Directory> | |||
</VirtualHost> | |||
NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI | |||
should be on and ".cgi" should respond. All requests from 127.0.0.1 go | |||
through CGI, so no Apache restart is necessary for changes. All other requests | |||
go through FCGI (or mod_ruby), which requires a restart to show changes. | |||
== Debugging Rails | |||
Have "tail -f" commands running on both the server.log, production.log, and | |||
test.log files. 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. | |||
== Breakpoints | |||
Breakpoint support is available through the script/breakpointer client. This | |||
means that you can break out of execution at any point in the code, investigate | |||
and change the model, AND then resume execution! Example: | |||
class WeblogController < ActionController::Base | |||
def index | |||
@posts = Post.find_all | |||
breakpoint "Breaking out from the list" | |||
end | |||
end | |||
So the controller will accept the action, run the first line, then present you | |||
with a IRB prompt in the breakpointer window. Here you can do things like: | |||
Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint' | |||
>> @posts.inspect | |||
=> "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>, | |||
#<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" | |||
>> @posts.first.title = "hello from a breakpoint" | |||
=> "hello from a breakpoint" | |||
...and even better is that you can examine how your runtime objects actually work: | |||
>> f = @posts.first | |||
=> #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}> | |||
>> f. | |||
Display all 152 possibilities? (y or n) | |||
Finally, when you're ready to resume execution, you press CTRL-D | |||
== Console | |||
You can interact with the domain model by starting the console through script/console. | |||
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. | |||
Passing an argument will specify a different environment, like <tt>console production</tt>. | |||
== Description of contents | |||
app | |||
Holds all the code that's specific to this particular application. | |||
app/controllers | |||
Holds controllers that should be named like weblog_controller.rb for | |||
automated URL mapping. All controllers should descend from | |||
ActionController::Base. | |||
app/models | |||
Holds models that should be named like post.rb. | |||
Most models will descend from ActiveRecord::Base. | |||
app/views | |||
Holds the template files for the view that should be named like | |||
weblog/index.rhtml for the WeblogController#index action. All views use eRuby | |||
syntax. This directory can also be used to keep stylesheets, images, and so on | |||
that can be symlinked to public. | |||
app/helpers | |||
Holds view helpers that should be named like weblog_helper.rb. | |||
config | |||
Configuration files for the Rails environment, the routing map, the database, and other dependencies. | |||
components | |||
Self-contained mini-applications that can bundle together controllers, models, and views. | |||
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. Contains subdirectories for images, stylesheets, | |||
and javascripts. Also contains the dispatchers and the default HTML files. | |||
script | |||
Helper scripts for automation and generation. | |||
test | |||
Unit and functional tests along with fixtures. | |||
vendor | |||
External libraries that the application depends on. Also includes the plugins subdirectory. | |||
This directory is in the load path. |
@@ -1,10 +0,0 @@ | |||
# 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. | |||
require(File.join(File.dirname(__FILE__), 'config', 'boot')) | |||
require 'rake' | |||
require 'rake/testtask' | |||
require 'rake/rdoctask' | |||
require 'tasks/rails' |
@@ -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 |
@@ -1,122 +0,0 @@ | |||
require 'pathname' | |||
require "openid" | |||
require 'openid/extensions/sreg' | |||
require 'openid/extensions/pape' | |||
require 'openid/store/filesystem' | |||
class ConsumerController < ApplicationController | |||
layout nil | |||
def index | |||
# render an openid form | |||
end | |||
def start | |||
begin | |||
identifier = params[:openid_identifier] | |||
if identifier.nil? | |||
flash[:error] = "Enter an OpenID identifier" | |||
redirect_to :action => 'index' | |||
return | |||
end | |||
oidreq = consumer.begin(identifier) | |||
rescue OpenID::OpenIDError => e | |||
flash[:error] = "Discovery failed for #{identifier}: #{e}" | |||
redirect_to :action => 'index' | |||
return | |||
end | |||
if params[:use_sreg] | |||
sregreq = OpenID::SReg::Request.new | |||
# required fields | |||
sregreq.request_fields(['email','nickname'], true) | |||
# optional fields | |||
sregreq.request_fields(['dob', 'fullname'], false) | |||
oidreq.add_extension(sregreq) | |||
oidreq.return_to_args['did_sreg'] = 'y' | |||
end | |||
if params[:use_pape] | |||
papereq = OpenID::PAPE::Request.new | |||
papereq.add_policy_uri(OpenID::PAPE::AUTH_PHISHING_RESISTANT) | |||
papereq.max_auth_age = 2*60*60 | |||
oidreq.add_extension(papereq) | |||
oidreq.return_to_args['did_pape'] = 'y' | |||
end | |||
if params[:force_post] | |||
oidreq.return_to_args['force_post']='x'*2048 | |||
end | |||
return_to = url_for :action => 'complete', :only_path => false | |||
realm = url_for :action => 'index', :only_path => false | |||
if oidreq.send_redirect?(realm, return_to, params[:immediate]) | |||
redirect_to oidreq.redirect_url(realm, return_to, params[:immediate]) | |||
else | |||
render :text => oidreq.html_markup(realm, return_to, params[:immediate], {'id' => 'openid_form'}) | |||
end | |||
end | |||
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]} | |||
oidresp = consumer.complete(parameters, current_url) | |||
case oidresp.status | |||
when OpenID::Consumer::FAILURE | |||
if oidresp.display_identifier | |||
flash[:error] = ("Verification of #{oidresp.display_identifier}"\ | |||
" failed: #{oidresp.message}") | |||
else | |||
flash[:error] = "Verification failed: #{oidresp.message}" | |||
end | |||
when OpenID::Consumer::SUCCESS | |||
flash[:success] = ("Verification of #{oidresp.display_identifier}"\ | |||
" succeeded.") | |||
if params[:did_sreg] | |||
sreg_resp = OpenID::SReg::Response.from_success_response(oidresp) | |||
sreg_message = "Simple Registration data was requested" | |||
if sreg_resp.empty? | |||
sreg_message << ", but none was returned." | |||
else | |||
sreg_message << ". The following data were sent:" | |||
sreg_resp.data.each {|k,v| | |||
sreg_message << "<br/><b>#{k}</b>: #{v}" | |||
} | |||
end | |||
flash[:sreg_results] = sreg_message | |||
end | |||
if params[:did_pape] | |||
pape_resp = OpenID::PAPE::Response.from_success_response(oidresp) | |||
pape_message = "A phishing resistant authentication method was requested" | |||
if pape_resp.auth_policies.member? OpenID::PAPE::AUTH_PHISHING_RESISTANT | |||
pape_message << ", and the server reported one." | |||
else | |||
pape_message << ", but the server did not report one." | |||
end | |||
if pape_resp.auth_time | |||
pape_message << "<br><b>Authentication time:</b> #{pape_resp.auth_time} seconds" | |||
end | |||
if pape_resp.nist_auth_level | |||
pape_message << "<br><b>NIST Auth Level:</b> #{pape_resp.nist_auth_level}" | |||
end | |||
flash[:pape_results] = pape_message | |||
end | |||
when OpenID::Consumer::SETUP_NEEDED | |||
flash[:alert] = "Immediate request failed - Setup Needed" | |||
when OpenID::Consumer::CANCEL | |||
flash[:alert] = "OpenID transaction cancelled." | |||
else | |||
end | |||
redirect_to :action => 'index' | |||
end | |||
private | |||
def consumer | |||
if @consumer.nil? | |||
dir = Pathname.new(RAILS_ROOT).join('db').join('cstore') | |||
store = OpenID::Store::Filesystem.new(dir) | |||
@consumer = OpenID::Consumer.new(session, store) | |||
end | |||
return @consumer | |||
end | |||
end |
@@ -1,45 +0,0 @@ | |||
# Controller for handling the login, logout process for "users" of our | |||
# little server. Users have no password. This is just an example. | |||
require 'openid' | |||
class LoginController < ApplicationController | |||
layout 'server' | |||
def base_url | |||
url_for(:controller => 'login', :action => nil, :only_path => false) | |||
end | |||
def index | |||
response.headers['X-XRDS-Location'] = url_for(:controller => "server", | |||
:action => "idp_xrds", | |||
:only_path => false) | |||
@base_url = base_url | |||
# just show the login page | |||
end | |||
def submit | |||
user = params[:username] | |||
# if we get a user, log them in by putting their username in | |||
# the session hash. | |||
unless user.nil? | |||
session[:username] = user unless user.nil? | |||
session[:approvals] = [] | |||
flash[:notice] = "Your OpenID URL is <b>#{base_url}user/#{user}</b><br/><br/>Proceed to step 2 below." | |||
else | |||
flash[:error] = "Sorry, couldn't log you in. Try again." | |||
end | |||
redirect_to :action => 'index' | |||
end | |||
def logout | |||
# delete the username from the session hash | |||
session[:username] = nil | |||
session[:approvals] = nil | |||
redirect_to :action => 'index' | |||
end | |||
end |
@@ -1,265 +0,0 @@ | |||
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 | |||
include ServerHelper | |||
include OpenID::Server | |||
layout nil | |||
def index | |||
begin | |||
oidreq = server.decode_request(params) | |||
rescue ProtocolError => e | |||
# invalid openid request, so just display a page with an error message | |||
render :text => e.to_s, :status => 500 | |||
return | |||
end | |||
# no openid.mode was given | |||
unless oidreq | |||
render :text => "This is an OpenID server endpoint." | |||
return | |||
end | |||
oidresp = nil | |||
if oidreq.kind_of?(CheckIDRequest) | |||
identity = oidreq.identity | |||
if oidreq.id_select | |||
if oidreq.immediate | |||
oidresp = oidreq.answer(false) | |||
elsif session[:username].nil? | |||
# The user hasn't logged in. | |||
show_decision_page(oidreq) | |||
return | |||
else | |||
# Else, set the identity to the one the user is using. | |||
identity = url_for_user | |||
end | |||
end | |||
if oidresp | |||
nil | |||
elsif self.is_authorized(identity, oidreq.trust_root) | |||
oidresp = oidreq.answer(true, nil, identity) | |||
# add the sreg response if requested | |||
add_sreg(oidreq, oidresp) | |||
# ditto pape | |||
add_pape(oidreq, oidresp) | |||
elsif oidreq.immediate | |||
server_url = url_for :action => 'index' | |||
oidresp = oidreq.answer(false, server_url) | |||
else | |||
show_decision_page(oidreq) | |||
return | |||
end | |||
else | |||
oidresp = server.handle_request(oidreq) | |||
end | |||
self.render_response(oidresp) | |||
end | |||
def show_decision_page(oidreq, message="Do you trust this site with your identity?") | |||
session[:last_oidreq] = oidreq | |||
@oidreq = oidreq | |||
if message | |||
flash[:notice] = message | |||
end | |||
render :template => 'server/decide', :layout => 'server' | |||
end | |||
def user_page | |||
# Yadis content-negotiation: we want to return the xrds if asked for. | |||
accept = request.env['HTTP_ACCEPT'] | |||
# This is not technically correct, and should eventually be updated | |||
# to do real Accept header parsing and logic. Though I expect it will work | |||
# 99% of the time. | |||
if accept and accept.include?('application/xrds+xml') | |||
user_xrds | |||
return | |||
end | |||
# content negotiation failed, so just render the user page | |||
xrds_url = url_for(:controller=>'user',:action=>params[:username])+'/xrds' | |||
identity_page = <<EOS | |||
<html><head> | |||
<meta http-equiv="X-XRDS-Location" content="#{xrds_url}" /> | |||
<link rel="openid.server" href="#{url_for :action => 'index'}" /> | |||
</head><body><p>OpenID identity page for #{params[:username]}</p> | |||
</body></html> | |||
EOS | |||
# Also add the Yadis location header, so that they don't have | |||
# to parse the html unless absolutely necessary. | |||
response.headers['X-XRDS-Location'] = xrds_url | |||
render :text => identity_page | |||
end | |||
def user_xrds | |||
types = [ | |||
OpenID::OPENID_2_0_TYPE, | |||
OpenID::OPENID_1_0_TYPE, | |||
OpenID::SREG_URI, | |||
] | |||
render_xrds(types) | |||
end | |||
def idp_xrds | |||
types = [ | |||
OpenID::OPENID_IDP_2_0_TYPE, | |||
] | |||
render_xrds(types) | |||
end | |||
def decision | |||
oidreq = session[:last_oidreq] | |||
session[:last_oidreq] = nil | |||
if params[:yes].nil? | |||
redirect_to oidreq.cancel_url | |||
return | |||
else | |||
id_to_send = params[:id_to_send] | |||
identity = oidreq.identity | |||
if oidreq.id_select | |||
if id_to_send and id_to_send != "" | |||
session[:username] = id_to_send | |||
session[:approvals] = [] | |||
identity = url_for_user | |||
else | |||
msg = "You must enter a username to in order to send " + | |||
"an identifier to the Relying Party." | |||
show_decision_page(oidreq, msg) | |||
return | |||
end | |||
end | |||
if session[:approvals] | |||
session[:approvals] << oidreq.trust_root | |||
else | |||
session[:approvals] = [oidreq.trust_root] | |||
end | |||
oidresp = oidreq.answer(true, nil, identity) | |||
add_sreg(oidreq, oidresp) | |||
add_pape(oidreq, oidresp) | |||
return self.render_response(oidresp) | |||
end | |||
end | |||
protected | |||
def server | |||
if @server.nil? | |||
server_url = url_for :action => 'index', :only_path => false | |||
dir = Pathname.new(RAILS_ROOT).join('db').join('openid-store') | |||
store = OpenID::Store::Filesystem.new(dir) | |||
@server = Server.new(store, server_url) | |||
end | |||
return @server | |||
end | |||
def approved(trust_root) | |||
return false if session[:approvals].nil? | |||
return session[:approvals].member?(trust_root) | |||
end | |||
def is_authorized(identity_url, trust_root) | |||
return (session[:username] and (identity_url == url_for_user) and self.approved(trust_root)) | |||
end | |||
def render_xrds(types) | |||
type_str = "" | |||
types.each { |uri| | |||
type_str += "<Type>#{uri}</Type>\n " | |||
} | |||
yadis = <<EOS | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<xrds:XRDS | |||
xmlns:xrds="xri://$xrds" | |||
xmlns="xri://$xrd*($v*2.0)"> | |||
<XRD> | |||
<Service priority="0"> | |||
#{type_str} | |||
<URI>#{url_for(:controller => 'server', :only_path => false)}</URI> | |||
</Service> | |||
</XRD> | |||
</xrds:XRDS> | |||
EOS | |||
response.headers['content-type'] = 'application/xrds+xml' | |||
render :text => yadis | |||
end | |||
def add_sreg(oidreq, oidresp) | |||
# check for Simple Registration arguments and respond | |||
sregreq = OpenID::SReg::Request.from_openid_request(oidreq) | |||
return if sregreq.nil? | |||
# In a real application, this data would be user-specific, | |||
# and the user should be asked for permission to release | |||
# it. | |||
sreg_data = { | |||
'nickname' => session[:username], | |||
'fullname' => 'Mayor McCheese', | |||
'email' => 'mayor@example.com' | |||
} | |||
sregresp = OpenID::SReg::Response.extract_response(sregreq, sreg_data) | |||
oidresp.add_extension(sregresp) | |||
end | |||
def add_pape(oidreq, oidresp) | |||
papereq = OpenID::PAPE::Request.from_openid_request(oidreq) | |||
return if papereq.nil? | |||
paperesp = OpenID::PAPE::Response.new | |||
paperesp.nist_auth_level = 0 # we don't even do auth at all! | |||
oidresp.add_extension(paperesp) | |||
end | |||
def render_response(oidresp) | |||
if oidresp.needs_signing | |||
signed_response = server.signatory.sign(oidresp) | |||
end | |||
web_response = server.encode_response(oidresp) | |||
case web_response.code | |||
when HTTP_OK | |||
render :text => web_response.body, :status => 200 | |||
when HTTP_REDIRECT | |||
redirect_to web_response.headers['location'] | |||
else | |||
render :text => web_response.body, :status => 400 | |||
end | |||
end | |||
end |
@@ -1,3 +0,0 @@ | |||
# Methods added to this helper will be available to all templates in the application. | |||
module ApplicationHelper | |||
end |
@@ -1,2 +0,0 @@ | |||
module LoginHelper | |||
end |
@@ -1,9 +0,0 @@ | |||
module ServerHelper | |||
def url_for_user | |||
url_for :controller => 'user', :action => session[:username] | |||
end | |||
end | |||
@@ -1,81 +0,0 @@ | |||
<html> | |||
<head> | |||
<title>Rails OpenID Example Relying Party</title> | |||
</head> | |||
<style type="text/css"> | |||
* { | |||
font-family: verdana,sans-serif; | |||
} | |||
body { | |||
width: 50em; | |||
margin: 1em; | |||
} | |||
div { | |||
padding: .5em; | |||
} | |||
.alert { | |||
border: 1px solid #e7dc2b; | |||
background: #fff888; | |||
} | |||
.error { | |||
border: 1px solid #ff0000; | |||
background: #ffaaaa; | |||
} | |||
.success { | |||
border: 1px solid #00ff00; | |||
background: #aaffaa; | |||
} | |||
#verify-form { | |||
border: 1px solid #777777; | |||
background: #dddddd; | |||
margin-top: 1em; | |||
padding-bottom: 0em; | |||
} | |||
input.openid { | |||
background: url( /images/openid_login_bg.gif ) no-repeat; | |||
background-position: 0 50%; | |||
background-color: #fff; | |||
padding-left: 18px; | |||
} | |||
</style> | |||
<body> | |||
<h1>Rails OpenID Example Relying Party</h1> | |||
<% if flash[:alert] %> | |||
<div class='alert'> | |||
<%= h(flash[:alert]) %> | |||
</div> | |||
<% end %> | |||
<% if flash[:error] %> | |||
<div class='error'> | |||
<%= h(flash[:error]) %> | |||
</div> | |||
<% end %> | |||
<% if flash[:success] %> | |||
<div class='success'> | |||
<%= h(flash[:success]) %> | |||
</div> | |||
<% end %> | |||
<% if flash[:sreg_results] %> | |||
<div class='alert'> | |||
<%= flash[:sreg_results] %> | |||
</div> | |||
<% end %> | |||
<% if flash[:pape_results] %> | |||
<div class='alert'> | |||
<%= flash[:pape_results] %> | |||
</div> | |||
<% end %> | |||
<div id="verify-form"> | |||
<form method="get" accept-charset="UTF-8" | |||
action='<%= url_for :action => 'start' %>'> | |||
Identifier: | |||
<input type="text" class="openid" name="openid_identifier" /> | |||
<input type="submit" value="Verify" /><br /> | |||
<input type="checkbox" name="immediate" id="immediate" /><label for="immediate">Use immediate mode</label><br/> | |||
<input type="checkbox" name="use_sreg" id="use_sreg" /><label for="use_sreg">Request registration data</label><br/> | |||
<input type="checkbox" name="use_pape" id="use_pape" /><label for="use_pape">Request phishing-resistent auth policy (PAPE)</label><br/> | |||
<input type="checkbox" name="force_post" id="force_post" /><label for="force_post">Force the transaction to use POST by adding 2K of extra data</label> | |||
</form> | |||
</div> | |||
</body> | |||
</html> |
@@ -1,68 +0,0 @@ | |||
<html> | |||
<head><title>OpenID Server Example</title></head> | |||
<style type="text/css"> | |||
* { | |||
font-family: verdana,sans-serif; | |||
} | |||
body { | |||
width: 50em; | |||
margin: 1em; | |||
} | |||
div { | |||
padding: .5em; | |||
} | |||
table { | |||
margin: none; | |||
padding: none; | |||
} | |||
.notice { | |||
border: 1px solid #60964f; | |||
background: #b3dca7; | |||
} | |||
.error { | |||
border: 1px solid #ff0000; | |||
background: #ffaaaa; | |||
} | |||
#login-form { | |||
border: 1px solid #777777; | |||
background: #dddddd; | |||
margin-top: 1em; | |||
padding-bottom: 0em; | |||
} | |||
table { | |||
padding: 1em; | |||
} | |||
li {margin-bottom: .5em;} | |||
span.openid:before { | |||
content: url(<%= @base_url %>images/openid_login_bg.gif) ; | |||
} | |||
span.openid { | |||
font-size: smaller; | |||
} | |||
</style> | |||
<body> | |||
<% if session[:username] %> | |||
<div style="float:right;"> | |||
Welcome, <%= session[:username] %> | <%= link_to('Log out', :controller => 'login', :action => 'logout') %><br /> | |||
<span class="openid"><%= @base_url %>user/<%= session[:username] %></span> | |||
</div> | |||
<% end %> | |||
<h3>Ruby OpenID Server Example</h3> | |||
<hr/> | |||
<% if flash[:notice] or flash[:error] %> | |||
<div class="<%= flash[:notice].nil? ? 'error' : 'notice' %>"> | |||
<%= flash[:error] or flash[:notice] %> | |||
</div> | |||
<% end %> | |||
<%= @content_for_layout %> | |||
</body> | |||
</html> |
@@ -1,56 +0,0 @@ | |||
<% if session[:username].nil? %> | |||
<div id="login-form"> | |||
<form method="get" action="<%= url_for :controller => 'login', :action => 'submit' %>"> | |||
Type a username: | |||
<input type="text" name="username" /> | |||
<input type="submit" value="Log In" /> | |||
</form> | |||
</div> | |||
<% end %> | |||
<p> Welcome to the Ruby OpenID example. This code is a starting point | |||
for developers wishing to implement an OpenID provider or relying | |||
party. We've used the <a href="http://rubyonrails.org/">Rails</a> | |||
platform to demonstrate, but the library code is not Rails specific.</p> | |||
<h2>To use the example provider</h2> | |||
<p> | |||
<ol> | |||
<li>Enter a username in the form above. You will be "Logged In" | |||
to the server, at which point you may authenticate using an OpenID | |||
consumer. Your OpenID URL will be displayed after you log | |||
in.<p>The server will automatically create an identity page for | |||
you at <%= @base_url %>user/<i>name</i></p></li> | |||
<li><p>Because WEBrick can only handle one thing at a time, you'll need to | |||
run another instance of the example on another port if you want to use | |||
a relying party to use with this example provider:</p> | |||
<blockquote> | |||
<code>script/server --port=3001</code> | |||
</blockquote> | |||
<p>(The RP needs to be able to access the provider, so unless you're | |||
running this example on a public IP, you can't use the live example | |||
at <a href="http://openidenabled.com/">openidenabled.com</a> on | |||
your local provider.)</p> | |||
</li> | |||
<li>Point your browser to this new instance and follow the directions | |||
below.</li> | |||
<!-- Fun fact: 'url_for :port => 3001' doesn't work very well. --> | |||
</ol> | |||
</p> | |||
<h2>To use the example relying party</h2> | |||
<p>Visit <a href="<%= url_for :controller => 'consumer' %>">/consumer</a> | |||
and enter your OpenID.</p> | |||
</p> | |||
@@ -1,26 +0,0 @@ | |||
<form method="post" action="<%= url_for :controller => 'server', :action => 'decision' %>"> | |||
<table> | |||
<tr><td>Site:</td><td><%= @oidreq.trust_root %></td></tr> | |||
<% if @oidreq.id_select %> | |||
<tr> | |||
<td colspan="2"> | |||
You entered the server identifier at the relying party. | |||
You'll need to send an identifier of your choosing. Enter a | |||
username below. | |||
</td> | |||
</tr> | |||
<tr> | |||
<td>Identity to send:</td> | |||
<td><input type="text" name="id_to_send" size="25" /></td> | |||
</tr> | |||
<% else %> | |||
<tr><td>Identity:</td><td><%= @oidreq.identity %></td></tr> | |||
<% end %> | |||
</table> | |||
<input type="submit" name="yes" value="yes" /> | |||
<input type="submit" name="no" value="no" /> | |||
</form> |
@@ -1,19 +0,0 @@ | |||
# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb | |||
unless defined?(RAILS_ROOT) | |||
root_path = File.join(File.dirname(__FILE__), '..') | |||
unless RUBY_PLATFORM =~ /mswin32/ | |||
require 'pathname' | |||
root_path = Pathname.new(root_path).cleanpath(true).to_s | |||
end | |||
RAILS_ROOT = root_path | |||
end | |||
if File.directory?("#{RAILS_ROOT}/vendor/rails") | |||
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" | |||
else | |||
require 'rubygems' | |||
require 'initializer' | |||
end | |||
Rails::Initializer.run(:set_load_path) |
@@ -1,54 +0,0 @@ | |||
# Be sure to restart your web server when you modify this file. | |||
# Uncomment below to force Rails into production mode when | |||
# you don't control web/app server and can't set it the proper way | |||
# ENV['RAILS_ENV'] ||= 'production' | |||
# Bootstrap the Rails environment, frameworks, and default configuration | |||
require File.join(File.dirname(__FILE__), 'boot') | |||
Rails::Initializer.run do |config| | |||
# Settings in config/environments/* take precedence those specified here | |||
# Skip frameworks you're not going to use | |||
# config.frameworks -= [ :action_web_service, :action_mailer ] | |||
# Add additional load paths for your own custom dirs | |||
# config.load_paths += %W( #{RAILS_ROOT}/extras ) | |||
# Force all environments to use the same logger level | |||
# (by default production uses :info, the others :debug) | |||
# config.log_level = :debug | |||
# Use the database for sessions instead of the file system | |||
# (create the session table with 'rake create_sessions_table') | |||
# config.action_controller.session_store = :active_record_store | |||
# Enable page/fragment caching by setting a file-based store | |||
# (remember to create the caching directory and make it readable to the application) | |||
# config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache" | |||
# Activate observers that should always be running | |||
# config.active_record.observers = :cacher, :garbage_collector | |||
# Make Active Record use UTC-base instead of local time | |||
# config.active_record.default_timezone = :utc | |||
# Use Active Record's schema dumper instead of SQL when creating the test database | |||
# (enables use of different database adapters for development and test environments) | |||
# config.active_record.schema_format = :ruby | |||
# See Rails::Configuration for more options | |||
end | |||
# Add new inflection rules using the following format | |||
# (all these examples are active by default): | |||
# Inflector.inflections do |inflect| | |||
# inflect.plural /^(ox)$/i, '\1en' | |||
# inflect.singular /^(ox)en/i, '\1' | |||
# inflect.irregular 'person', 'people' | |||
# inflect.uncountable %w( fish sheep ) | |||
# end | |||
# Include your application configuration below | |||
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_key] = '_session_id_2' |
@@ -1,19 +0,0 @@ | |||
# Settings specified here will take precedence over those in config/environment.rb | |||
# In the development environment your application's code is reloaded on | |||
# every request. This slows down response time but is perfect for development | |||
# since you don't have to restart the webserver when you make code changes. | |||
config.cache_classes = false | |||
# Log error messages when you accidentally call methods on nil. | |||
config.whiny_nils = true | |||
# Enable the breakpoint server that script/breakpointer connects to | |||
config.breakpoint_server = true | |||
# Show full error reports and disable caching | |||
config.action_controller.consider_all_requests_local = true | |||
config.action_controller.perform_caching = false | |||
# Don't care if the mailer can't send | |||
config.action_mailer.raise_delivery_errors = false |
@@ -1,19 +0,0 @@ | |||
# Settings specified here will take precedence over those in config/environment.rb | |||
# The production environment is meant for finished, "live" apps. | |||
# Code is not reloaded between requests | |||
config.cache_classes = true | |||
# Use a different logger for distributed setups | |||
# config.logger = SyslogLogger.new | |||
# Full error reports are disabled and caching is turned on | |||
config.action_controller.consider_all_requests_local = false | |||
config.action_controller.perform_caching = true | |||
# Enable serving of images, stylesheets, and javascripts from an asset server | |||
# config.action_controller.asset_host = "http://assets.example.com" | |||
# Disable delivery errors if you bad email addresses should just be ignored | |||
# config.action_mailer.raise_delivery_errors = false |
@@ -1,19 +0,0 @@ | |||
# Settings specified here will take precedence over those in config/environment.rb | |||
# The test environment is used exclusively to run your application's | |||
# test suite. You never need to work with it otherwise. Remember that | |||
# your test database is "scratch space" for the test suite and is wiped | |||
# and recreated between test runs. Don't rely on the data there! | |||
config.cache_classes = true | |||
# Log error messages when you accidentally call methods on nil. | |||
config.whiny_nils = true | |||
# Show full error reports and disable caching | |||
config.action_controller.consider_all_requests_local = true | |||
config.action_controller.perform_caching = false | |||
# Tell ActionMailer not to deliver emails to the real world. | |||
# The :test delivery method accumulates sent emails in the | |||
# ActionMailer::Base.deliveries array. | |||
config.action_mailer.delivery_method = :test |
@@ -1,24 +0,0 @@ | |||
ActionController::Routing::Routes.draw do |map| | |||
# Add your own custom routes here. | |||
# The priority is based upon order of creation: first created -> highest priority. | |||
# Here's a sample route: | |||
# map.connect 'products/:id', :controller => 'catalog', :action => 'view' | |||
# Keep in mind you can assign values other than :controller and :action | |||
# You can have the root of your site routed by hooking up '' | |||
# -- just remember to delete public/index.html. | |||
# map.connect '', :controller => "welcome" | |||
map.connect '', :controller => 'login' | |||
map.connect 'server/xrds', :controller => 'server', :action => 'idp_xrds' | |||
map.connect 'user/:username', :controller => 'server', :action => 'user_page' | |||
map.connect 'user/:username/xrds', :controller => 'server', :action => 'user_xrds' | |||
# Allow downloading Web Service WSDL as a file with an extension | |||
# instead of a file named 'wsdl' | |||
map.connect ':controller/service.wsdl', :action => 'wsdl' | |||
# Install the default route as the lowest priority. | |||
map.connect ':controller/:action/:id' | |||
end |
@@ -1,2 +0,0 @@ | |||
Use this README file to introduce your application and point to useful places in the API for learning more. | |||
Run "rake appdoc" to generate API documentation for your models and controllers. |
@@ -1,8 +0,0 @@ | |||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" | |||
"http://www.w3.org/TR/html4/loose.dtd"> | |||
<html> | |||
<body> | |||
<h1>File not found</h1> | |||
<p>Change this error message for pages not found in public/404.html</p> | |||
</body> | |||
</html> |
@@ -1,8 +0,0 @@ | |||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" | |||
"http://www.w3.org/TR/html4/loose.dtd"> | |||
<html> | |||
<body> | |||
<h1>Application error (Apache)</h1> | |||
<p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p> | |||
</body> | |||
</html> |
@@ -1,12 +0,0 @@ | |||
#!/usr/bin/ruby1.8 | |||
#!/usr/local/bin/ruby | |||
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) | |||
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: | |||
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired | |||
require "dispatcher" | |||
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) | |||
Dispatcher.dispatch |
@@ -1,26 +0,0 @@ | |||
#!/usr/bin/ruby1.8 | |||
#!/usr/local/bin/ruby | |||
# | |||
# You may specify the path to the FastCGI crash log (a log of unhandled | |||
# exceptions which forced the FastCGI instance to exit, great for debugging) | |||
# and the number of requests to process before running garbage collection. | |||
# | |||
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log | |||
# and the GC period is nil (turned off). A reasonable number of requests | |||
# could range from 10-100 depending on the memory footprint of your app. | |||
# | |||
# Example: | |||
# # Default log path, normal GC behavior. | |||
# RailsFCGIHandler.process! | |||
# | |||
# # Default log path, 50 requests between GC. | |||
# RailsFCGIHandler.process! nil, 50 | |||
# | |||
# # Custom log path, normal GC behavior. | |||
# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' | |||
# | |||
require File.dirname(__FILE__) + "/../config/environment" | |||
require 'fcgi_handler' | |||
RailsFCGIHandler.process! |
@@ -1,12 +0,0 @@ | |||
#!/usr/bin/ruby1.8 | |||
#!/usr/local/bin/ruby | |||
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) | |||
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: | |||
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired | |||
require "dispatcher" | |||
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) | |||
Dispatcher.dispatch |
@@ -1,750 +0,0 @@ | |||
// 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) | |||
// Contributors: | |||
// Richard Livsey | |||
// Rahul Bhargava | |||
// Rob Wills | |||
// | |||
// See scriptaculous.js for full license. | |||
// 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, | |||
// a getUpdatedChoices function that will be invoked every time | |||
// 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 | |||
// autocompletion. Specific auto-completion logic (AJAX, etc) | |||
// belongs in getUpdatedChoices. | |||
// | |||
// Tokenized incremental autocompletion is enabled automatically | |||
// when an autocompleter is instantiated with the 'tokens' option | |||
// in the options parameter, e.g.: | |||
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); | |||
// 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 | |||
// allows smart autocompletion after linebreaks. | |||
var Autocompleter = {} | |||
Autocompleter.Base = function() {}; | |||
Autocompleter.Base.prototype = { | |||
baseInitialize: function(element, update, options) { | |||
this.element = $(element); | |||
this.update = $(update); | |||
this.hasFocus = false; | |||
this.changed = false; | |||
this.active = false; | |||
this.index = 0; | |||
this.entryCount = 0; | |||
if (this.setOptions) | |||
this.setOptions(options); | |||
else | |||
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}) }; | |||
if (typeof(this.options.tokens) == 'string') | |||
this.options.tokens = new Array(this.options.tokens); | |||
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)); | |||
}, | |||
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) && | |||
(Element.getStyle(this.update, 'position')=='absolute')) { | |||
new Insertion.After(this.update, | |||
'<iframe id="' + this.update.id + '_iefix" '+ | |||
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + | |||
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); | |||
this.iefix = $(this.update.id+'_iefix'); | |||
} | |||
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); | |||
}, | |||
fixIEOverlapping: function() { | |||
Position.clone(this.update, this.iefix); | |||
this.iefix.style.zIndex = 1; | |||
this.update.style.zIndex = 2; | |||
Element.show(this.iefix); | |||
}, | |||
hide: function() { | |||
this.stopIndicator(); | |||
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); | |||
if(this.iefix) Element.hide(this.iefix); | |||
}, | |||
startIndicator: function() { | |||
if(this.options.indicator) Element.show(this.options.indicator); | |||
}, | |||
stopIndicator: function() { | |||
if(this.options.indicator) Element.hide(this.options.indicator); | |||
}, | |||
onKeyPress: function(event) { | |||
if(this.active) | |||
switch(event.keyCode) { | |||
case Event.KEY_TAB: | |||
case Event.KEY_RETURN: | |||
this.selectEntry(); | |||
Event.stop(event); | |||
case Event.KEY_ESC: | |||
this.hide(); | |||
this.active = false; | |||
Event.stop(event); | |||
return; | |||
case Event.KEY_LEFT: | |||
case Event.KEY_RIGHT: | |||
return; | |||
case Event.KEY_UP: | |||
this.markPrevious(); | |||
this.render(); | |||
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); | |||
return; | |||
case Event.KEY_DOWN: | |||
this.markNext(); | |||
this.render(); | |||
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); | |||
return; | |||
} | |||
else | |||
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) | |||
return; | |||
this.changed = true; | |||
this.hasFocus = true; | |||
if(this.observer) clearTimeout(this.observer); | |||
this.observer = | |||
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); | |||
}, | |||
onHover: function(event) { | |||
var element = Event.findElement(event, 'LI'); | |||
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; | |||
}, | |||
render: function() { | |||
if(this.entryCount > 0) { | |||
for (var i = 0; i < this.entryCount; i++) | |||
this.index==i ? | |||
Element.addClassName(this.getEntry(i),"selected") : | |||
Element.removeClassName(this.getEntry(i),"selected"); | |||
if(this.hasFocus) { | |||
this.show(); | |||
this.active = true; | |||
} | |||
} else { | |||
this.active = false; | |||
this.hide(); | |||
} | |||
}, | |||
markPrevious: function() { | |||
if(this.index > 0) this.index-- | |||
else this.index = this.entryCount-1; | |||
}, | |||
markNext: function() { | |||
if(this.index < this.entryCount-1) this.index++ | |||
else this.index = 0; | |||
}, | |||
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()); | |||
}, | |||
updateElement: function(selectedElement) { | |||
if (this.options.updateElement) { | |||
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+/); | |||
if (whitespace) | |||
newValue += whitespace[0]; | |||
this.element.value = newValue + value; | |||
} else { | |||
this.element.value = value; | |||
} | |||
this.element.focus(); | |||
if (this.options.afterUpdateElement) | |||
this.options.afterUpdateElement(this.element, selectedElement); | |||
}, | |||
updateChoices: function(choices) { | |||
if(!this.changed && this.hasFocus) { | |||
this.update.innerHTML = choices; | |||
Element.cleanWhitespace(this.update); | |||
Element.cleanWhitespace(this.update.firstChild); | |||
if(this.update.firstChild && this.update.firstChild.childNodes) { | |||
this.entryCount = | |||
this.update.firstChild.childNodes.length; | |||
for (var i = 0; i < this.entryCount; i++) { | |||
var entry = this.getEntry(i); | |||
entry.autocompleteIndex = i; | |||
this.addObservers(entry); | |||
} | |||
} else { | |||
this.entryCount = 0; | |||
} | |||
this.stopIndicator(); | |||
this.index = 0; | |||
this.render(); | |||
} | |||
}, | |||
addObservers: function(element) { | |||
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); | |||
Event.observe(element, "click", this.onClick.bindAsEventListener(this)); | |||
}, | |||
onObserverEvent: function() { | |||
this.changed = false; | |||
if(this.getToken().length>=this.options.minChars) { | |||
this.startIndicator(); | |||
this.getUpdatedChoices(); | |||
} else { | |||
this.active = false; | |||
this.hide(); | |||
} | |||
}, | |||
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<this.options.tokens.length; i++) { | |||
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); | |||
if (thisTokenPos > lastTokenPos) | |||
lastTokenPos = thisTokenPos; | |||
} | |||
return lastTokenPos; | |||
} | |||
} | |||
Ajax.Autocompleter = Class.create(); | |||
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { | |||
initialize: function(element, update, url, options) { | |||
this.baseInitialize(element, update, options); | |||
this.options.asynchronous = true; | |||
this.options.onComplete = this.onComplete.bind(this); | |||
this.options.defaultParams = this.options.parameters || null; | |||
this.url = url; | |||
}, | |||
getUpdatedChoices: function() { | |||
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) | |||
this.options.parameters += '&' + this.options.defaultParams; | |||
new Ajax.Request(this.url, this.options); | |||
}, | |||
onComplete: function(request) { | |||
this.updateChoices(request.responseText); | |||
} | |||
}); | |||
// The local array autocompleter. Used when you'd prefer to | |||
// inject an array of autocompletion options into the page, rather | |||
// than sending out Ajax queries, which can be quite slow sometimes. | |||
// | |||
// The constructor takes four parameters. The first two are, as usual, | |||
// the id of the monitored textbox, and id of the autocompletion menu. | |||
// The third is the array you want to autocomplete from, and the fourth | |||
// is the options block. | |||
// | |||
// Extra local autocompletion options: | |||
// - choices - How many autocompletion choices to offer | |||
// | |||
// - partialSearch - If false, the autocompleter will match entered | |||
// 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 | |||
// search anywhere in the string, additionally set | |||
// the option fullSearch to true (default: off). | |||
// | |||
// - fullSsearch - Search anywhere in autocomplete array strings. | |||
// | |||
// - partialChars - How many characters to enter before triggering | |||
// a partial match (unlike minChars, which defines | |||
// how many characters are required to do any match | |||
// at all). Defaults to 2. | |||
// | |||
// - ignoreCase - Whether to ignore case when autocompleting. | |||
// Defaults to true. | |||
// | |||
// 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(), { | |||
initialize: function(element, update, array, options) { | |||
this.baseInitialize(element, update, options); | |||
this.options.array = array; | |||
}, | |||
getUpdatedChoices: function() { | |||
this.updateChoices(this.options.selector(this)); | |||
}, | |||
setOptions: function(options) { | |||
this.options = Object.extend({ | |||
choices: 10, | |||
partialSearch: true, | |||
partialChars: 2, | |||
ignoreCase: true, | |||
fullSearch: false, | |||
selector: function(instance) { | |||
var ret = []; // Beginning matches | |||
var partial = []; // Inside matches | |||
var entry = instance.getToken(); | |||
var count = 0; | |||
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()) : | |||
elem.indexOf(entry); | |||
while (foundPos != -1) { | |||
if (foundPos == 0 && elem.length != entry.length) { | |||
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + | |||
elem.substr(entry.length) + "</li>"); | |||
break; | |||
} 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("<li>" + elem.substr(0, foundPos) + "<strong>" + | |||
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( | |||
foundPos + entry.length) + "</li>"); | |||
break; | |||
} | |||
} | |||
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)) | |||
return "<ul>" + ret.join('') + "</ul>"; | |||
} | |||
}, options || {}); | |||
} | |||
}); | |||
// AJAX in-place editor | |||
// | |||
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor | |||
// 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 | |||
// waits 1 ms (with setTimeout) until it does the activation | |||
Field.scrollFreeActivate = function(field) { | |||
setTimeout(function() { | |||
Field.activate(field); | |||
}, 1); | |||
} | |||
Ajax.InPlaceEditor = Class.create(); | |||
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; | |||
Ajax.InPlaceEditor.prototype = { | |||
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; | |||
} | |||
} | |||
if (this.options.externalControl) { | |||
this.options.externalControl = $(this.options.externalControl); | |||
} | |||
this.originalBackground = Element.getStyle(this.element, 'background-color'); | |||
if (!this.originalBackground) { | |||
this.originalBackground = "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); | |||
} | |||
}, | |||
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); | |||
} | |||
return false; | |||
}, | |||
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); | |||
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); | |||
}, | |||
hasHTMLLineBreaks: function(string) { | |||
if (!this.options.handleLineBreaks) return false; | |||
return string.match(/<br/i) || string.match(/<p>/i); | |||
}, | |||
convertHTMLLineBreaks: function(string) { | |||
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, ""); | |||
}, | |||
createEditField: function() { | |||
var text; | |||
if(this.options.loadTextURL) { | |||
text = this.options.loadingText; | |||
} else { | |||
text = this.getText(); | |||
} | |||
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; | |||
} 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(); | |||
} | |||
this.form.appendChild(this.editField); | |||
}, | |||
getText: function() { | |||
return this.element.innerHTML; | |||
}, | |||
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.removeForm(); | |||
this.leaveHover(); | |||
this.showSaving(); | |||
}, | |||
showSaving: function() { | |||
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); | |||
}, | |||
removeForm: function() { | |||
if(this.form) { | |||
if (this.form.parentNode) Element.remove(this.form); | |||
this.form = null; | |||
} | |||
}, | |||
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) | |||
}, | |||
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 | |||
}); | |||
}, | |||
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); | |||
} | |||
this.editing = false; | |||
this.saving = false; | |||
this.oldInnerHTML = null; | |||
this.onLeaveEditMode(); | |||
}, | |||
onComplete: function(transport) { | |||
this.leaveEditMode(); | |||
this.options.onComplete.bind(this)(transport, this.element); | |||
}, | |||
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); | |||
} | |||
} | |||
}; | |||
// 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 = { | |||
initialize: function(element, delay, callback) { | |||
this.delay = delay || 0.5; | |||
this.element = $(element); | |||
this.callback = callback; | |||
this.timer = null; | |||
this.lastValue = $F(this.element); | |||
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); | |||
}, | |||
delayedListener: function(event) { | |||
if(this.lastValue == $F(this.element)) return; | |||
if(this.timer) clearTimeout(this.timer); | |||
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); | |||
this.lastValue = $F(this.element); | |||
}, | |||
onTimerEvent: function() { | |||
this.timer = null; | |||
this.callback(this.element, $F(this.element)); | |||
} | |||
}; |
@@ -1,584 +0,0 @@ | |||
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) | |||
// | |||
// See scriptaculous.js for full license. | |||
/*--------------------------------------------------------------------------*/ | |||
var Droppables = { | |||
drops: [], | |||
remove: function(element) { | |||
this.drops = this.drops.reject(function(d) { return d.element==$(element) }); | |||
}, | |||
add: function(element) { | |||
element = $(element); | |||
var options = Object.extend({ | |||
greedy: true, | |||
hoverclass: null | |||
}, arguments[1] || {}); | |||
// cache containers | |||
if(options.containment) { | |||
options._containers = []; | |||
var containment = options.containment; | |||
if((typeof containment == 'object') && | |||
(containment.constructor == Array)) { | |||
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 | |||
options.element = element; | |||
this.drops.push(options); | |||
}, | |||
isContained: function(element, drop) { | |||
var parentNode = element.parentNode; | |||
return drop._containers.detect(function(c) { return parentNode == c }); | |||
}, | |||
isAffected: function(point, element, drop) { | |||
return ( | |||
(drop.element!=element) && | |||
((!drop._containers) || | |||
this.isContained(element, drop)) && | |||
((!drop.accept) || | |||
(Element.classNames(element).detect( | |||
function(v) { return drop.accept.include(v) } ) )) && | |||
Position.within(drop.element, point[0], point[1]) ); | |||
}, | |||
deactivate: function(drop) { | |||
if(drop.hoverclass) | |||
Element.removeClassName(drop.element, drop.hoverclass); | |||
this.last_active = null; | |||
}, | |||
activate: function(drop) { | |||
if(drop.hoverclass) | |||
Element.addClassName(drop.element, drop.hoverclass); | |||
this.last_active = drop; | |||
}, | |||
show: function(point, element) { | |||
if(!this.drops.length) return; | |||
if(this.last_active) this.deactivate(this.last_active); | |||
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; | |||
} | |||
} | |||
}); | |||
}, | |||
fire: function(event, element) { | |||
if(!this.last_active) return; | |||
Position.prepare(); | |||
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) | |||
if (this.last_active.onDrop) | |||
this.last_active.onDrop(element, this.last_active.element, event); | |||
}, | |||
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) { | |||
Event.stopObserving(document, "mouseup", this.eventMouseUp); | |||
Event.stopObserving(document, "mousemove", this.eventMouseMove); | |||
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; | |||
}, | |||
deactivate: function(draggbale) { | |||
this.activeDraggable = null; | |||
}, | |||
updateDrag: function(event) { | |||
if(!this.activeDraggable) return; | |||
var pointer = [Event.pointerX(event), Event.pointerY(event)]; | |||
// Mozilla-based browsers fire successive mousemove events with | |||
// 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.activeDraggable) return; | |||
this._lastPointer = null; | |||
this.activeDraggable.endDrag(event); | |||
}, | |||
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); | |||
}); | |||
}, | |||
_cacheObserverCallbacks: function() { | |||
['onStart','onEnd','onDrag'].each( function(eventName) { | |||
Draggables[eventName+'Count'] = Draggables.observers.select( | |||
function(o) { return o[eventName]; } | |||
).length; | |||
}); | |||
} | |||
} | |||
/*--------------------------------------------------------------------------*/ | |||
var Draggable = Class.create(); | |||
Draggable.prototype = { | |||
initialize: function(element) { | |||
var options = Object.extend({ | |||
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}); | |||
}, | |||
endeffect: function(element) { | |||
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); | |||
}, | |||
zindex: 1000, | |||
revert: false, | |||
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } | |||
}, arguments[1] || {}); | |||
this.element = $(element); | |||
if(options.handle && (typeof options.handle == 'string')) | |||
this.handle = Element.childrenWithClassName(this.element, options.handle)[0]; | |||
if(!this.handle) this.handle = $(options.handle); | |||
if(!this.handle) this.handle = this.element; | |||
Element.makePositioned(this.element); // fix IE | |||
this.delta = this.currentDelta(); | |||
this.options = options; | |||
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')]); | |||
}, | |||
initDrag: function(event) { | |||
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; | |||
} | |||
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.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.element.parentNode.insertBefore(this._clone, this.element); | |||
} | |||
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); | |||
Draggables.notify('onDrag', this, event); | |||
this.draw(pointer); | |||
if(this.options.change) this.options.change(this); | |||
// fix AppleWebKit rendering | |||
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); | |||
Event.stop(event); | |||
}, | |||
finishDrag: function(event, success) { | |||
this.dragging = false; | |||
if(this.options.ghosting) { | |||
Position.relativize(this.element); | |||
Element.remove(this._clone); | |||
this._clone = null; | |||
} | |||
if(success) Droppables.fire(event, this.element); | |||
Draggables.notify('onEnd', this, event); | |||
var revert = this.options.revert; | |||
if(revert && typeof revert == 'function') 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]); | |||
} else { | |||
this.delta = d; | |||
} | |||
if(this.options.zindex) | |||
this.element.style.zIndex = this.originalZ; | |||
if(this.options.endeffect) | |||
this.options.endeffect(this.element); | |||
Draggables.deactivate(this); | |||
Droppables.reset(); | |||
}, | |||
keyPress: function(event) { | |||
if(!event.keyCode==Event.KEY_ESC) return; | |||
this.finishDrag(event, false); | |||
Event.stop(event); | |||
}, | |||
endDrag: function(event) { | |||
if(!this.dragging) return; | |||
this.finishDrag(event, true); | |||
Event.stop(event); | |||
}, | |||
draw: function(point) { | |||
var pos = Position.cumulativeOffset(this.element); | |||
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.snap) { | |||
if(typeof this.options.snap == 'function') { | |||
p = this.options.snap(p[0],p[1]); | |||
} else { | |||
if(this.options.snap instanceof Array) { | |||
p = p.map( function(v, i) { | |||
return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) | |||
} else { | |||
p = p.map( function(v) { | |||
return Math.round(v/this.options.snap)*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 | |||
} | |||
} | |||
/*--------------------------------------------------------------------------*/ | |||
var SortableObserver = Class.create(); | |||
SortableObserver.prototype = { | |||
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 }); | |||
}, | |||
destroy: function(element){ | |||
element = $(element); | |||
this.sortables.findAll(function(s) { return s.element == element }).each(function(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 }); | |||
}, | |||
create: function(element) { | |||
element = $(element); | |||
var options = Object.extend({ | |||
element: element, | |||
tag: 'li', // assumes li children, override with tag: 'tagname' | |||
dropOnEmpty: false, | |||
tree: false, // fixme: unimplemented | |||
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, | |||
hoverclass: null, | |||
ghosting: false, | |||
format: null, | |||
onChange: Prototype.emptyFunction, | |||
onUpdate: Prototype.emptyFunction | |||
}, arguments[1] || {}); | |||
// clear any old sortable with same element | |||
this.destroy(element); | |||
// build options for the draggables | |||
var options_for_draggable = { | |||
revert: true, | |||
ghosting: options.ghosting, | |||
constraint: options.constraint, | |||
handle: options.handle }; | |||
if(options.starteffect) | |||
options_for_draggable.starteffect = options.starteffect; | |||
if(options.reverteffect) | |||
options_for_draggable.reverteffect = options.reverteffect; | |||
else | |||
if(options.ghosting) options_for_draggable.reverteffect = function(element) { | |||
element.style.top = 0; | |||
element.style.left = 0; | |||
}; | |||
if(options.endeffect) | |||
options_for_draggable.endeffect = options.endeffect; | |||
if(options.zindex) | |||
options_for_draggable.zindex = options.zindex; | |||
// build options for the droppables | |||
var options_for_droppable = { | |||
overlap: options.overlap, | |||
containment: options.containment, | |||
hoverclass: options.hoverclass, | |||
onHover: Sortable.onHover, | |||
greedy: !options.dropOnEmpty | |||
} | |||
// fix for gecko engine | |||
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}); | |||
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.draggables.push( | |||
new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); | |||
Droppables.add(e, options_for_droppable); | |||
options.droppables.push(e); | |||
}); | |||
// keep reference | |||
this.sortables.push(options); | |||
// for onupdate | |||
Draggables.addObserver(new SortableObserver(element, options.onUpdate)); | |||
}, | |||
// 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 (elements.length>0 ? elements.flatten() : null); | |||
}, | |||
onHover: function(element, dropon, overlap) { | |||
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) | |||
Sortable.options(oldParentNode).onChange(element); | |||
Sortable.options(dropon.parentNode).onChange(element); | |||
} | |||
} else { | |||
Sortable.mark(dropon, 'after'); | |||
var nextElement = dropon.nextSibling || null; | |||
if(nextElement != element) { | |||
var oldParentNode = element.parentNode; | |||
element.style.visibility = "hidden"; // fix gecko rendering | |||
dropon.parentNode.insertBefore(element, nextElement); | |||
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); | |||
Sortable.options(oldParentNode).onChange(element); | |||
Sortable.options(dropon).onChange(element); | |||
} | |||
}, | |||
unmark: function() { | |||
if(Sortable._marker) Element.hide(Sortable._marker); | |||
}, | |||
mark: function(dropon, position) { | |||
// mark on ghosting only | |||
var sortable = Sortable.options(dropon.parentNode); | |||
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'; | |||
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'; | |||
if(position=='after') | |||
if(sortable.overlap == 'horizontal') | |||
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; | |||
else | |||
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; | |||
Element.show(Sortable._marker); | |||
}, | |||
serialize: function(element) { | |||
element = $(element); | |||
var sortableOptions = this.options(element); | |||
var options = Object.extend({ | |||
tag: sortableOptions.tag, | |||
only: sortableOptions.only, | |||
name: element.id, | |||
format: sortableOptions.format || /^[^_]*_(.*)$/ | |||
}, 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("&"); | |||
} | |||
} |
@@ -1,854 +0,0 @@ | |||
// Copyright (c) 2005 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; | |||
} | |||
Element.setStyle = function(element, style) { | |||
element = $(element); | |||
for(k in style) element.style[k.camelize()] = style[k]; | |||
} | |||
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.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.childrenWithClassName = function(element, className) { | |||
return $A($(element).getElementsByTagName('*')).select( | |||
function(c) { return Element.hasClassName(c, className) }); | |||
} | |||
Array.prototype.call = function() { | |||
var args = arguments; | |||
this.each(function(f){ f.apply(this, args) }); | |||
} | |||
/*--------------------------------------------------------------------------*/ | |||
var Effect = { | |||
tagifyText: function(element) { | |||
var tagifyStyle = 'position:relative'; | |||
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; | |||
element = $(element); | |||
$A(element.childNodes).each( function(child) { | |||
if(child.nodeType==3) { | |||
child.nodeValue.toArray().each( function(character) { | |||
element.insertBefore( | |||
Builder.node('span',{style: tagifyStyle}, | |||
character == ' ' ? String.fromCharCode(160) : character), | |||
child); | |||
}); | |||
Element.remove(child); | |||
} | |||
}); | |||
}, | |||
multiple: function(element, effect) { | |||
var elements; | |||
if(((typeof element == 'object') || | |||
(typeof element == 'function')) && | |||
(element.length)) | |||
elements = element; | |||
else | |||
elements = $(element).childNodes; | |||
var options = Object.extend({ | |||
speed: 0.1, | |||
delay: 0.0 | |||
}, arguments[2] || {}); | |||
var masterDelay = options.delay; | |||
$A(elements).each( function(element, index) { | |||
new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); | |||
}); | |||
} | |||
}; | |||
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; | |||
} | |||
/* ------------- core effects ------------- */ | |||
Effect.Queue = { | |||
effects: [], | |||
_each: function(iterator) { | |||
this.effects._each(iterator); | |||
}, | |||
interval: null, | |||
add: function(effect) { | |||
var timestamp = new Date().getTime(); | |||
switch(effect.options.queue) { | |||
case 'front': | |||
// 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 '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); | |||
}, | |||
remove: function(effect) { | |||
this.effects = this.effects.reject(function(e) { return e==effect }); | |||
if(this.effects.length == 0) { | |||
clearInterval(this.interval); | |||
this.interval = null; | |||
} | |||
}, | |||
loop: function() { | |||
var timePos = new Date().getTime(); | |||
this.effects.invoke('loop', timePos); | |||
} | |||
} | |||
Object.extend(Effect.Queue, Enumerable); | |||
Effect.Base = function() {}; | |||
Effect.Base.prototype = { | |||
position: null, | |||
setOptions: function(options) { | |||
this.options = Object.extend({ | |||
transition: Effect.Transitions.sinoidal, | |||
duration: 1.0, // seconds | |||
fps: 25.0, // max. 25fps due to Effect.Queue implementation | |||
sync: false, // true for combining | |||
from: 0.0, | |||
to: 1.0, | |||
delay: 0.0, | |||
queue: 'parallel' | |||
}, options || {}); | |||
}, | |||
start: function(options) { | |||
this.setOptions(options || {}); | |||
this.currentFrame = 0; | |||
this.state = 'idle'; | |||
this.startOn = this.options.delay*1000; | |||
this.finishOn = this.startOn + (this.options.duration*1000); | |||
this.event('beforeStart'); | |||
if(!this.options.sync) Effect.Queue.add(this); | |||
}, | |||
loop: function(timePos) { | |||
if(timePos >= this.startOn) { | |||
if(timePos >= this.finishOn) { | |||
this.render(1.0); | |||
this.cancel(); | |||
this.event('beforeFinish'); | |||
if(this.finish) this.finish(); | |||
this.event('afterFinish'); | |||
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) { | |||
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); | |||
this.state = 'finished'; | |||
}, | |||
event: function(eventName) { | |||
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); | |||
if(this.options[eventName]) this.options[eventName](this); | |||
}, | |||
inspect: function() { | |||
return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>'; | |||
} | |||
} | |||
Effect.Parallel = Class.create(); | |||
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { | |||
initialize: function(effects) { | |||
this.effects = effects || []; | |||
this.start(arguments[1]); | |||
}, | |||
update: function(position) { | |||
this.effects.invoke('render', position); | |||
}, | |||
finish: function(position) { | |||
this.effects.each( function(effect) { | |||
effect.render(1.0); | |||
effect.cancel(); | |||
effect.event('beforeFinish'); | |||
if(effect.finish) effect.finish(position); | |||
effect.event('afterFinish'); | |||
}); | |||
} | |||
}); | |||
Effect.Opacity = Class.create(); | |||
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { | |||
initialize: function(element) { | |||
this.element = $(element); | |||
// make this work on IE on elements without 'layout' | |||
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) | |||
Element.setStyle(this.element, {zoom: 1}); | |||
var options = Object.extend({ | |||
from: Element.getOpacity(this.element) || 0.0, | |||
to: 1.0 | |||
}, arguments[1] || {}); | |||
this.start(options); | |||
}, | |||
update: function(position) { | |||
Element.setOpacity(this.element, 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]); | |||
}, | |||
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'); | |||
}, | |||
update: function(position) { | |||
Element.setStyle(this.element, { | |||
top: this.toTop * position + this.originalTop + 'px', | |||
left: this.toLeft * position + this.originalLeft + 'px' | |||
}); | |||
} | |||
}); | |||
Effect.Scale = Class.create(); | |||
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { | |||
initialize: function(element, percent) { | |||
this.element = $(element) | |||
var options = Object.extend({ | |||
scaleX: true, | |||
scaleY: true, | |||
scaleContent: true, | |||
scaleFromCenter: false, | |||
scaleMode: 'box', // 'box' or 'contents' or {} with provided values | |||
scaleFrom: 100.0, | |||
scaleTo: percent | |||
}, arguments[2] || {}); | |||
this.start(options); | |||
}, | |||
setup: function() { | |||
this.restoreAfterFinish = this.options.restoreAfterFinish || false; | |||
this.elementPositioning = Element.getStyle(this.element,'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) { | |||
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') | |||
this.dims = [this.element.offsetHeight, this.element.offsetWidth]; | |||
if(/^content/.test(this.options.scaleMode)) | |||
this.dims = [this.element.scrollHeight, this.element.scrollWidth]; | |||
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 }); | |||
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); | |||
}, | |||
finish: function(position) { | |||
if (this.restoreAfterFinish) Element.setStyle(this.element, 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 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'; | |||
} else { | |||
if(this.options.scaleY) d.top = -topd + 'px'; | |||
if(this.options.scaleX) d.left = -leftd + 'px'; | |||
} | |||
} | |||
Element.setStyle(this.element, d); | |||
} | |||
}); | |||
Effect.Highlight = Class.create(); | |||
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { | |||
initialize: function(element) { | |||
this.element = $(element); | |||
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; } | |||
// 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'); | |||
// 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)) }); | |||
}, | |||
finish: function() { | |||
Element.setStyle(this.element, 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)); | |||
} | |||
}); | |||
/* ------------- combination effects ------------- */ | |||
Effect.Fade = function(element) { | |||
var oldOpacity = Element.getInlineOpacity(element); | |||
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] || {}); | |||
return new Effect.Opacity(element,options); | |||
} | |||
Effect.Appear = function(element) { | |||
var options = Object.extend({ | |||
from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0), | |||
to: 1.0, | |||
beforeSetup: function(effect) { with(Element) { | |||
setOpacity(effect.element, effect.options.from); | |||
show(effect.element); }} | |||
}, 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') }; | |||
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] || {}) | |||
); | |||
} | |||
Effect.BlindUp = function(element) { | |||
element = $(element); | |||
Element.makeClipping(element); | |||
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] || {}) | |||
); | |||
} | |||
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] || {}) | |||
); | |||
} | |||
Effect.SwitchOff = function(element) { | |||
element = $(element); | |||
var oldOpacity = Element.getInlineOpacity(element); | |||
return new Effect.Appear(element, { | |||
duration: 0.4, | |||
from: 0, | |||
transition: Effect.Transitions.flicker, | |||
afterFinishInternal: function(effect) { | |||
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}); | |||
}} | |||
}) | |||
} | |||
}); | |||
} | |||
Effect.DropOut = function(element) { | |||
element = $(element); | |||
var oldStyle = { | |||
top: Element.getStyle(element, 'top'), | |||
left: Element.getStyle(element, 'left'), | |||
opacity: Element.getInlineOpacity(element) }; | |||
return new Effect.Parallel( | |||
[ new Effect.MoveBy(element, 100, 0, { 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] || {})); | |||
} | |||
Effect.Shake = function(element) { | |||
element = $(element); | |||
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); | |||
}}}) }}) }}) }}) }}) }}); | |||
} | |||
Effect.SlideDown = function(element) { | |||
element = $(element); | |||
Element.cleanWhitespace(element); | |||
// 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, | |||
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] || {}) | |||
); | |||
} | |||
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, | |||
scaleMode: 'box', | |||
scaleFrom: 100, | |||
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] || {}) | |||
); | |||
} | |||
// 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); }} | |||
}); | |||
} | |||
Effect.Grow = function(element) { | |||
element = $(element); | |||
var options = Object.extend({ | |||
direction: 'center', | |||
moveTransistion: Effect.Transitions.sinoidal, | |||
scaleTransition: Effect.Transitions.sinoidal, | |||
opacityTransition: Effect.Transitions.full | |||
}, arguments[1] || {}); | |||
var oldStyle = { | |||
top: element.style.top, | |||
left: element.style.left, | |||
height: element.style.height, | |||
width: element.style.width, | |||
opacity: Element.getInlineOpacity(element) }; | |||
var dims = Element.getDimensions(element); | |||
var initialMoveX, initialMoveY; | |||
var moveX, moveY; | |||
switch (options.direction) { | |||
case 'top-left': | |||
initialMoveX = initialMoveY = moveX = moveY = 0; | |||
break; | |||
case 'top-right': | |||
initialMoveX = dims.width; | |||
initialMoveY = moveY = 0; | |||
moveX = -dims.width; | |||
break; | |||
case 'bottom-left': | |||
initialMoveX = moveX = 0; | |||
initialMoveY = dims.height; | |||
moveY = -dims.height; | |||
break; | |||
case 'bottom-right': | |||
initialMoveX = dims.width; | |||
initialMoveY = dims.height; | |||
moveX = -dims.width; | |||
moveY = -dims.height; | |||
break; | |||
case 'center': | |||
initialMoveX = dims.width / 2; | |||
initialMoveY = dims.height / 2; | |||
moveX = -dims.width / 2; | |||
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); | |||
}}, | |||
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.Scale(effect.element, 100, { | |||
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); }} | |||
}, options) | |||
) | |||
} | |||
}); | |||
} | |||
Effect.Shrink = function(element) { | |||
element = $(element); | |||
var options = Object.extend({ | |||
direction: 'center', | |||
moveTransistion: Effect.Transitions.sinoidal, | |||
scaleTransition: Effect.Transitions.sinoidal, | |||
opacityTransition: Effect.Transitions.none | |||
}, arguments[1] || {}); | |||
var oldStyle = { | |||
top: element.style.top, | |||
left: element.style.left, | |||
height: element.style.height, | |||
width: element.style.width, | |||
opacity: Element.getInlineOpacity(element) }; | |||
var dims = Element.getDimensions(element); | |||
var moveX, moveY; | |||
switch (options.direction) { | |||
case 'top-left': | |||
moveX = moveY = 0; | |||
break; | |||
case 'top-right': | |||
moveX = dims.width; | |||
moveY = 0; | |||
break; | |||
case 'bottom-left': | |||
moveX = 0; | |||
moveY = dims.height; | |||
break; | |||
case 'bottom-right': | |||
moveX = dims.width; | |||
moveY = dims.height; | |||
break; | |||
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); }} | |||
}, 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}); } | |||
}, options), {transition: reverser})); | |||
} | |||
Effect.Fold = function(element) { | |||
element = $(element); | |||
var oldStyle = { | |||
top: element.style.top, | |||
left: element.style.left, | |||
width: element.style.width, | |||
height: element.style.height }; | |||
Element.makeClipping(element); | |||
return new Effect.Scale(element, 5, Object.extend({ | |||
scaleContent: false, | |||
scaleX: false, | |||
afterFinishInternal: function(effect) { | |||
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] || {})); | |||
} |
@@ -1 +0,0 @@ | |||
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/about' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/breakpointer' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/console' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/destroy' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/generate' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../../config/boot' | |||
require 'commands/performance/benchmarker' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../../config/boot' | |||
require 'commands/performance/profiler' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/plugin' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../../config/boot' | |||
require 'commands/process/reaper' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../../config/boot' | |||
require 'commands/process/spawner' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../../config/boot' | |||
require 'commands/process/spinner' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/runner' |
@@ -1,3 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require File.dirname(__FILE__) + '/../config/boot' | |||
require 'commands/server' |
@@ -1,18 +0,0 @@ | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
require 'login_controller' | |||
# Re-raise errors caught by the controller. | |||
class LoginController; def rescue_action(e) raise e end; end | |||
class LoginControllerTest < Test::Unit::TestCase | |||
def setup | |||
@controller = LoginController.new | |||
@request = ActionController::TestRequest.new | |||
@response = ActionController::TestResponse.new | |||
end | |||
# Replace this with your real tests. | |||
def test_truth | |||
assert true | |||
end | |||
end |
@@ -1,18 +0,0 @@ | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
require 'server_controller' | |||
# Re-raise errors caught by the controller. | |||
class ServerController; def rescue_action(e) raise e end; end | |||
class ServerControllerTest < Test::Unit::TestCase | |||
def setup | |||
@controller = ServerController.new | |||
@request = ActionController::TestRequest.new | |||
@response = ActionController::TestResponse.new | |||
end | |||
# Replace this with your real tests. | |||
def test_truth | |||
assert true | |||
end | |||
end |
@@ -1,28 +0,0 @@ | |||
ENV["RAILS_ENV"] = "test" | |||
require File.expand_path(File.dirname(__FILE__) + "/../config/environment") | |||
require 'test_help' | |||
class Test::Unit::TestCase | |||
# Transactional fixtures accelerate your tests by wrapping each test method | |||
# in a transaction that's rolled back on completion. This ensures that the | |||
# test database remains unchanged so your fixtures don't have to be reloaded | |||
# between every test method. Fewer database queries means faster tests. | |||
# | |||
# Read Mike Clark's excellent walkthrough at | |||
# http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting | |||
# | |||
# Every Active Record database supports transactions except MyISAM tables | |||
# in MySQL. Turn off transactional fixtures in this case; however, if you | |||
# don't care one way or the other, switching from MyISAM to InnoDB tables | |||
# is recommended. | |||
self.use_transactional_fixtures = true | |||
# Instantiated fixtures are slow, but give you @david where otherwise you | |||
# would need people(:david). If you don't want to migrate your existing | |||
# test cases which use the @david style and don't mind the speed hit (each | |||
# instantiated fixtures translates to a database query per test method), | |||
# then set this back to true. | |||
self.use_instantiated_fixtures = false | |||
# Add more helper methods to be used by all tests here... | |||
end |
@@ -1,112 +0,0 @@ | |||
# Copyright (C) 2001 Daiki Ueno <ueno@unixuser.org> | |||
# This library is distributed under the terms of the Ruby license. | |||
# This module provides common interface to HMAC engines. | |||
# HMAC standard is documented in RFC 2104: | |||
# | |||
# H. Krawczyk et al., "HMAC: Keyed-Hashing for Message Authentication", | |||
# RFC 2104, February 1997 | |||
# | |||
# These APIs are inspired by JCE 1.2's javax.crypto.Mac interface. | |||
# | |||
# <URL:http://java.sun.com/security/JCE1.2/spec/apidoc/javax/crypto/Mac.html> | |||
module HMAC | |||
class Base | |||
def initialize(algorithm, block_size, output_length, key) | |||
@algorithm = algorithm | |||
@block_size = block_size | |||
@output_length = output_length | |||
@status = STATUS_UNDEFINED | |||
@key_xor_ipad = '' | |||
@key_xor_opad = '' | |||
set_key(key) unless key.nil? | |||
end | |||
private | |||
def check_status | |||
unless @status == STATUS_INITIALIZED | |||
raise RuntimeError, | |||
"The underlying hash algorithm has not yet been initialized." | |||
end | |||
end | |||
public | |||
def set_key(key) | |||
# If key is longer than the block size, apply hash function | |||
# to key and use the result as a real key. | |||
key = @algorithm.digest(key) if key.size > @block_size | |||
key_xor_ipad = "\x36" * @block_size | |||
key_xor_opad = "\x5C" * @block_size | |||
for i in 0 .. key.size - 1 | |||
key_xor_ipad[i] ^= key[i] | |||
key_xor_opad[i] ^= key[i] | |||
end | |||
@key_xor_ipad = key_xor_ipad | |||
@key_xor_opad = key_xor_opad | |||
@md = @algorithm.new | |||
@status = STATUS_INITIALIZED | |||
end | |||
def reset_key | |||
@key_xor_ipad.gsub!(/./, '?') | |||
@key_xor_opad.gsub!(/./, '?') | |||
@key_xor_ipad[0..-1] = '' | |||
@key_xor_opad[0..-1] = '' | |||
@status = STATUS_UNDEFINED | |||
end | |||
def update(text) | |||
check_status | |||
# perform inner H | |||
md = @algorithm.new | |||
md.update(@key_xor_ipad) | |||
md.update(text) | |||
str = md.digest | |||
# perform outer H | |||
md = @algorithm.new | |||
md.update(@key_xor_opad) | |||
md.update(str) | |||
@md = md | |||
end | |||
alias << update | |||
def digest | |||
check_status | |||
@md.digest | |||
end | |||
def hexdigest | |||
check_status | |||
@md.hexdigest | |||
end | |||
alias to_s hexdigest | |||
# These two class methods below are safer than using above | |||
# instance methods combinatorially because an instance will have | |||
# held a key even if it's no longer in use. | |||
def Base.digest(key, text) | |||
begin | |||
hmac = self.new(key) | |||
hmac.update(text) | |||
hmac.digest | |||
ensure | |||
hmac.reset_key | |||
end | |||
end | |||
def Base.hexdigest(key, text) | |||
begin | |||
hmac = self.new(key) | |||
hmac.update(text) | |||
hmac.hexdigest | |||
ensure | |||
hmac.reset_key | |||
end | |||
end | |||
private_class_method :new, :digest, :hexdigest | |||
end | |||
STATUS_UNDEFINED, STATUS_INITIALIZED = 0, 1 | |||
end |
@@ -1,11 +0,0 @@ | |||
require 'hmac/hmac' | |||
require 'digest/sha1' | |||
module HMAC | |||
class SHA1 < Base | |||
def initialize(key = nil) | |||
super(Digest::SHA1, 64, 20, key) | |||
end | |||
public_class_method :new, :digest, :hexdigest | |||
end | |||
end |
@@ -1,25 +0,0 @@ | |||
require 'hmac/hmac' | |||
require 'digest/sha2' | |||
module HMAC | |||
class SHA256 < Base | |||
def initialize(key = nil) | |||
super(Digest::SHA256, 64, 32, key) | |||
end | |||
public_class_method :new, :digest, :hexdigest | |||
end | |||
class SHA384 < Base | |||
def initialize(key = nil) | |||
super(Digest::SHA384, 128, 48, key) | |||
end | |||
public_class_method :new, :digest, :hexdigest | |||
end | |||
class SHA512 < Base | |||
def initialize(key = nil) | |||
super(Digest::SHA512, 128, 64, key) | |||
end | |||
public_class_method :new, :digest, :hexdigest | |||
end | |||
end |
@@ -1,20 +0,0 @@ | |||
# Copyright 2006-2007 JanRain, Inc. | |||
# | |||
# Licensed under the Apache License, Version 2.0 (the "License"); you | |||
# may not use this file except in compliance with the License. You may | |||
# obtain a copy of the License at | |||
# | |||
# http://www.apache.org/licenses/LICENSE-2.0 | |||
# | |||
# Unless required by applicable law or agreed to in writing, software | |||
# distributed under the License is distributed on an "AS IS" BASIS, | |||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
# implied. See the License for the specific language governing | |||
# permissions and limitations under the License. | |||
module OpenID | |||
VERSION = "2.1.4" | |||
end | |||
require "openid/consumer" | |||
require 'openid/server' |
@@ -1,249 +0,0 @@ | |||
require "openid/kvform" | |||
require "openid/util" | |||
require "openid/cryptutil" | |||
require "openid/message" | |||
module OpenID | |||
def self.get_secret_size(assoc_type) | |||
if assoc_type == 'HMAC-SHA1' | |||
return 20 | |||
elsif assoc_type == 'HMAC-SHA256' | |||
return 32 | |||
else | |||
raise ArgumentError("Unsupported association type: #{assoc_type}") | |||
end | |||
end | |||
# An Association holds the shared secret between a relying party and | |||
# an OpenID provider. | |||
class Association | |||
attr_reader :handle, :secret, :issued, :lifetime, :assoc_type | |||
FIELD_ORDER = | |||
[:version, :handle, :secret, :issued, :lifetime, :assoc_type,] | |||
# Load a serialized Association | |||
def self.deserialize(serialized) | |||
parsed = Util.kv_to_seq(serialized) | |||
parsed_fields = parsed.map{|k, v| k.to_sym} | |||
if parsed_fields != FIELD_ORDER | |||
raise ProtocolError, 'Unexpected fields in serialized association'\ | |||
" (Expected #{FIELD_ORDER.inspect}, got #{parsed_fields.inspect})" | |||
end | |||
version, handle, secret64, issued_s, lifetime_s, assoc_type = | |||
parsed.map {|field, value| value} | |||
if version != '2' | |||
raise ProtocolError, "Attempted to deserialize unsupported version "\ | |||
"(#{parsed[0][1].inspect})" | |||
end | |||
self.new(handle, | |||
Util.from_base64(secret64), | |||
Time.at(issued_s.to_i), | |||
lifetime_s.to_i, | |||
assoc_type) | |||
end | |||
# Create an Association with an issued time of now | |||
def self.from_expires_in(expires_in, handle, secret, assoc_type) | |||
issued = Time.now | |||
self.new(handle, secret, issued, expires_in, assoc_type) | |||
end | |||
def initialize(handle, secret, issued, lifetime, assoc_type) | |||
@handle = handle | |||
@secret = secret | |||
@issued = issued | |||
@lifetime = lifetime | |||
@assoc_type = assoc_type | |||
end | |||
# Serialize the association to a form that's consistent across | |||
# JanRain OpenID libraries. | |||
def serialize | |||
data = { | |||
:version => '2', | |||
:handle => handle, | |||
:secret => Util.to_base64(secret), | |||
:issued => issued.to_i.to_s, | |||
:lifetime => lifetime.to_i.to_s, | |||
:assoc_type => assoc_type, | |||
} | |||
Util.assert(data.length == FIELD_ORDER.length) | |||
pairs = FIELD_ORDER.map{|field| [field.to_s, data[field]]} | |||
return Util.seq_to_kv(pairs, strict=true) | |||
end | |||
# The number of seconds until this association expires | |||
def expires_in(now=nil) | |||
if now.nil? | |||
now = Time.now.to_i | |||
else | |||
now = now.to_i | |||
end | |||
time_diff = (issued.to_i + lifetime) - now | |||
if time_diff < 0 | |||
return 0 | |||
else | |||
return time_diff | |||
end | |||
end | |||
# Generate a signature for a sequence of [key, value] pairs | |||
def sign(pairs) | |||
kv = Util.seq_to_kv(pairs) | |||
case assoc_type | |||
when 'HMAC-SHA1' | |||
CryptUtil.hmac_sha1(@secret, kv) | |||
when 'HMAC-SHA256' | |||
CryptUtil.hmac_sha256(@secret, kv) | |||
else | |||
raise ProtocolError, "Association has unknown type: "\ | |||
"#{assoc_type.inspect}" | |||
end | |||
end | |||
# Generate the list of pairs that form the signed elements of the | |||
# given message | |||
def make_pairs(message) | |||
signed = message.get_arg(OPENID_NS, 'signed') | |||
if signed.nil? | |||
raise ProtocolError, 'Missing signed list' | |||
end | |||
signed_fields = signed.split(',', -1) | |||
data = message.to_post_args | |||
signed_fields.map {|field| [field, data.fetch('openid.'+field,'')] } | |||
end | |||
# Return whether the message's signature passes | |||
def check_message_signature(message) | |||
message_sig = message.get_arg(OPENID_NS, 'sig') | |||
if message_sig.nil? | |||
raise ProtocolError, "#{message} has no sig." | |||
end | |||
calculated_sig = get_message_signature(message) | |||
return calculated_sig == message_sig | |||
end | |||
# Get the signature for this message | |||
def get_message_signature(message) | |||
Util.to_base64(sign(make_pairs(message))) | |||
end | |||
def ==(other) | |||
(other.class == self.class and | |||
other.handle == self.handle and | |||
other.secret == self.secret and | |||
# The internals of the time objects seemed to differ | |||
# in an opaque way when serializing/unserializing. | |||
# I don't think this will be a problem. | |||
other.issued.to_i == self.issued.to_i and | |||
other.lifetime == self.lifetime and | |||
other.assoc_type == self.assoc_type) | |||
end | |||
# Add a signature (and a signed list) to a message. | |||
def sign_message(message) | |||
if (message.has_key?(OPENID_NS, 'sig') or | |||
message.has_key?(OPENID_NS, 'signed')) | |||
raise ArgumentError, 'Message already has signed list or signature' | |||
end | |||
extant_handle = message.get_arg(OPENID_NS, 'assoc_handle') | |||
if extant_handle and extant_handle != self.handle | |||
raise ArgumentError, "Message has a different association handle" | |||
end | |||
signed_message = message.copy() | |||
signed_message.set_arg(OPENID_NS, 'assoc_handle', self.handle) | |||
message_keys = signed_message.to_post_args.keys() | |||
signed_list = [] | |||
message_keys.each { |k| | |||
if k.starts_with?('openid.') | |||
signed_list << k[7..-1] | |||
end | |||
} | |||
signed_list << 'signed' | |||
signed_list.sort! | |||
signed_message.set_arg(OPENID_NS, 'signed', signed_list.join(',')) | |||
sig = get_message_signature(signed_message) | |||
signed_message.set_arg(OPENID_NS, 'sig', sig) | |||
return signed_message | |||
end | |||
end | |||
class AssociationNegotiator | |||
attr_reader :allowed_types | |||
def self.get_session_types(assoc_type) | |||
case assoc_type | |||
when 'HMAC-SHA1' | |||
['DH-SHA1', 'no-encryption'] | |||
when 'HMAC-SHA256' | |||
['DH-SHA256', 'no-encryption'] | |||
else | |||
raise ProtocolError, "Unknown association type #{assoc_type.inspect}" | |||
end | |||
end | |||
def self.check_session_type(assoc_type, session_type) | |||
if !get_session_types(assoc_type).include?(session_type) | |||
raise ProtocolError, "Session type #{session_type.inspect} not "\ | |||
"valid for association type #{assoc_type.inspect}" | |||
end | |||
end | |||
def initialize(allowed_types) | |||
self.allowed_types=(allowed_types) | |||
end | |||
def copy | |||
Marshal.load(Marshal.dump(self)) | |||
end | |||
def allowed_types=(allowed_types) | |||
allowed_types.each do |assoc_type, session_type| | |||
self.class.check_session_type(assoc_type, session_type) | |||
end | |||
@allowed_types = allowed_types | |||
end | |||
def add_allowed_type(assoc_type, session_type=nil) | |||
if session_type.nil? | |||
session_types = self.class.get_session_types(assoc_type) | |||
else | |||
self.class.check_session_type(assoc_type, session_type) | |||
session_types = [session_type] | |||
end | |||
for session_type in session_types do | |||
@allowed_types << [assoc_type, session_type] | |||
end | |||
end | |||
def allowed?(assoc_type, session_type) | |||
@allowed_types.include?([assoc_type, session_type]) | |||
end | |||
def get_allowed_type | |||
@allowed_types.empty? ? nil : @allowed_types[0] | |||
end | |||
end | |||
DefaultNegotiator = | |||
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'], | |||
['HMAC-SHA1', 'no-encryption'], | |||
['HMAC-SHA256', 'DH-SHA256'], | |||
['HMAC-SHA256', 'no-encryption']]) | |||
EncryptedNegotiator = | |||
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'], | |||
['HMAC-SHA256', 'DH-SHA256']]) | |||
end |
@@ -1,395 +0,0 @@ | |||
require "openid/consumer/idres.rb" | |||
require "openid/consumer/checkid_request.rb" | |||
require "openid/consumer/associationmanager.rb" | |||
require "openid/consumer/responses.rb" | |||
require "openid/consumer/discovery_manager" | |||
require "openid/consumer/discovery" | |||
require "openid/message" | |||
require "openid/yadis/discovery" | |||
require "openid/store/nonce" | |||
module OpenID | |||
# OpenID support for Relying Parties (aka Consumers). | |||
# | |||
# This module documents the main interface with the OpenID consumer | |||
# library. The only part of the library which has to be used and | |||
# isn't documented in full here is the store required to create an | |||
# Consumer instance. | |||
# | |||
# = OVERVIEW | |||
# | |||
# The OpenID identity verification process most commonly uses the | |||
# following steps, as visible to the user of this library: | |||
# | |||
# 1. The user enters their OpenID into a field on the consumer's | |||
# site, and hits a login button. | |||
# | |||
# 2. The consumer site discovers the user's OpenID provider using | |||
# the Yadis protocol. | |||
# | |||
# 3. The consumer site sends the browser a redirect to the OpenID | |||
# provider. This is the authentication request as described in | |||
# the OpenID specification. | |||
# | |||
# 4. The OpenID provider's site sends the browser a redirect back to | |||
# the consumer site. This redirect contains the provider's | |||
# response to the authentication request. | |||
# | |||
# The most important part of the flow to note is the consumer's site | |||
# must handle two separate HTTP requests in order to perform the | |||
# full identity check. | |||
# | |||
# = LIBRARY DESIGN | |||
# | |||
# This consumer library is designed with that flow in mind. The | |||
# goal is to make it as easy as possible to perform the above steps | |||
# securely. | |||
# | |||
# At a high level, there are two important parts in the consumer | |||
# library. The first important part is this module, which contains | |||
# the interface to actually use this library. The second is | |||
# openid/store/interface.rb, which describes the interface to use if | |||
# you need to create a custom method for storing the state this | |||
# library needs to maintain between requests. | |||
# | |||
# In general, the second part is less important for users of the | |||
# library to know about, as several implementations are provided | |||
# which cover a wide variety of situations in which consumers may | |||
# use the library. | |||
# | |||
# The Consumer class has methods corresponding to the actions | |||
# necessary in each of steps 2, 3, and 4 described in the overview. | |||
# Use of this library should be as easy as creating an Consumer | |||
# instance and calling the methods appropriate for the action the | |||
# site wants to take. | |||
# | |||
# This library automatically detects which version of the OpenID | |||
# protocol should be used for a transaction and constructs the | |||
# proper requests and responses. Users of this library do not need | |||
# to worry about supporting multiple protocol versions; the library | |||
# supports them implicitly. Depending on the version of the | |||
# protocol in use, the OpenID transaction may be more secure. See | |||
# the OpenID specifications for more information. | |||
# | |||
# = SESSIONS, STORES, AND STATELESS MODE | |||
# | |||
# The Consumer object keeps track of two types of state: | |||
# | |||
# 1. State of the user's current authentication attempt. Things | |||
# like the identity URL, the list of endpoints discovered for | |||
# that URL, and in case where some endpoints are unreachable, the | |||
# list of endpoints already tried. This state needs to be held | |||
# from Consumer.begin() to Consumer.complete(), but it is only | |||
# applicable to a single session with a single user agent, and at | |||
# the end of the authentication process (i.e. when an OP replies | |||
# with either <tt>id_res</tt>. or <tt>cancel</tt> it may be | |||
# discarded. | |||
# | |||
# 2. State of relationships with servers, i.e. shared secrets | |||
# (associations) with servers and nonces seen on signed messages. | |||
# This information should persist from one session to the next | |||
# and should not be bound to a particular user-agent. | |||
# | |||
# These two types of storage are reflected in the first two | |||
# arguments of Consumer's constructor, <tt>session</tt> and | |||
# <tt>store</tt>. <tt>session</tt> is a dict-like object and we | |||
# hope your web framework provides you with one of these bound to | |||
# the user agent. <tt>store</tt> is an instance of Store. | |||
# | |||
# Since the store does hold secrets shared between your application | |||
# and the OpenID provider, you should be careful about how you use | |||
# it in a shared hosting environment. If the filesystem or database | |||
# permissions of your web host allow strangers to read from them, do | |||
# not store your data there! If you have no safe place to store | |||
# your data, construct your consumer with nil for the store, and it | |||
# will operate only in stateless mode. Stateless mode may be | |||
# slower, put more load on the OpenID provider, and trusts the | |||
# provider to keep you safe from replay attacks. | |||
# | |||
# Several store implementation are provided, and the interface is | |||
# fully documented so that custom stores can be used as well. See | |||
# the documentation for the Consumer class for more information on | |||
# the interface for stores. The implementations that are provided | |||
# allow the consumer site to store the necessary data in several | |||
# different ways, including several SQL databases and normal files | |||
# on disk. | |||
# | |||
# = IMMEDIATE MODE | |||
# | |||
# In the flow described above, the user may need to confirm to the | |||
# OpenID provider that it's ok to disclose his or her identity. The | |||
# provider may draw pages asking for information from the user | |||
# before it redirects the browser back to the consumer's site. This | |||
# is generally transparent to the consumer site, so it is typically | |||
# ignored as an implementation detail. | |||
# | |||
# There can be times, however, where the consumer site wants to get | |||
# a response immediately. When this is the case, the consumer can | |||
# put the library in immediate mode. In immediate mode, there is an | |||
# extra response possible from the server, which is essentially the | |||
# server reporting that it doesn't have enough information to answer | |||
# the question yet. | |||
# | |||
# = USING THIS LIBRARY | |||
# | |||
# Integrating this library into an application is usually a | |||
# relatively straightforward process. The process should basically | |||
# follow this plan: | |||
# | |||
# Add an OpenID login field somewhere on your site. When an OpenID | |||
# is entered in that field and the form is submitted, it should make | |||
# a request to the your site which includes that OpenID URL. | |||
# | |||
# First, the application should instantiate a Consumer with a | |||
# session for per-user state and store for shared state using the | |||
# store of choice. | |||
# | |||
# Next, the application should call the <tt>begin</tt> method of | |||
# Consumer instance. This method takes the OpenID URL as entered by | |||
# the user. The <tt>begin</tt> method returns a CheckIDRequest | |||
# object. | |||
# | |||
# Next, the application should call the redirect_url method on the | |||
# CheckIDRequest object. The parameter <tt>return_to</tt> is the | |||
# URL that the OpenID server will send the user back to after | |||
# attempting to verify his or her identity. The <tt>realm</tt> | |||
# parameter is the URL (or URL pattern) that identifies your web | |||
# site to the user when he or she is authorizing it. Send a | |||
# redirect to the resulting URL to the user's browser. | |||
# | |||
# That's the first half of the authentication process. The second | |||
# half of the process is done after the user's OpenID Provider sends | |||
# the user's browser a redirect back to your site to complete their | |||
# login. | |||
# | |||
# When that happens, the user will contact your site at the URL | |||
# given as the <tt>return_to</tt> URL to the redirect_url call made | |||
# above. The request will have several query parameters added to | |||
# the URL by the OpenID provider as the information necessary to | |||
# finish the request. | |||
# | |||
# Get a Consumer instance with the same session and store as before | |||
# and call its complete() method, passing in all the received query | |||
# arguments and URL currently being handled. | |||
# | |||
# There are multiple possible return types possible from that | |||
# method. These indicate the whether or not the login was | |||
# successful, and include any additional information appropriate for | |||
# their type. | |||
class Consumer | |||
attr_accessor :session_key_prefix | |||
# Initialize a Consumer instance. | |||
# | |||
# You should create a new instance of the Consumer object with | |||
# every HTTP request that handles OpenID transactions. | |||
# | |||
# session: the session object to use to store request information. | |||
# The session should behave like a hash. | |||
# | |||
# store: an object that implements the interface in Store. | |||
def initialize(session, store) | |||
@session = session | |||
@store = store | |||
@session_key_prefix = 'OpenID::Consumer::' | |||
end | |||
# Start the OpenID authentication process. See steps 1-2 in the | |||
# overview for the Consumer class. | |||
# | |||
# user_url: Identity URL given by the user. This method performs a | |||
# textual transformation of the URL to try and make sure it is | |||
# normalized. For example, a user_url of example.com will be | |||
# normalized to http://example.com/ normalizing and resolving any | |||
# redirects the server might issue. | |||
# | |||
# anonymous: A boolean value. Whether to make an anonymous | |||
# request of the OpenID provider. Such a request does not ask for | |||
# an authorization assertion for an OpenID identifier, but may be | |||
# used with extensions to pass other data. e.g. "I don't care who | |||
# you are, but I'd like to know your time zone." | |||
# | |||
# Returns a CheckIDRequest object containing the discovered | |||
# information, with a method for building a redirect URL to the | |||
# server, as described in step 3 of the overview. This object may | |||
# also be used to add extension arguments to the request, using | |||
# its add_extension_arg method. | |||
# | |||
# Raises DiscoveryFailure when no OpenID server can be found for | |||
# this URL. | |||
def begin(openid_identifier, anonymous=false) | |||
manager = discovery_manager(openid_identifier) | |||
service = manager.get_next_service(&method(:discover)) | |||
if service.nil? | |||
raise DiscoveryFailure.new("No usable OpenID services were found "\ | |||
"for #{openid_identifier.inspect}", nil) | |||
else | |||
begin_without_discovery(service, anonymous) | |||
end | |||
end | |||
# Start OpenID verification without doing OpenID server | |||
# discovery. This method is used internally by Consumer.begin() | |||
# after discovery is performed, and exists to provide an interface | |||
# for library users needing to perform their own discovery. | |||
# | |||
# service: an OpenID service endpoint descriptor. This object and | |||
# factories for it are found in the openid/consumer/discovery.rb | |||
# module. | |||
# | |||
# Returns an OpenID authentication request object. | |||
def begin_without_discovery(service, anonymous) | |||
assoc = association_manager(service).get_association | |||
checkid_request = CheckIDRequest.new(assoc, service) | |||
checkid_request.anonymous = anonymous | |||
if service.compatibility_mode | |||
rt_args = checkid_request.return_to_args | |||
rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce | |||
rt_args[Consumer.openid1_return_to_claimed_id_name] = | |||
service.claimed_id | |||
end | |||
self.last_requested_endpoint = service | |||
return checkid_request | |||
end | |||
# Called to interpret the server's response to an OpenID | |||
# request. It is called in step 4 of the flow described in the | |||
# Consumer overview. | |||
# | |||
# query: A hash of the query parameters for this HTTP request. | |||
# Note that in rails, this is <b>not</b> <tt>params</tt> but | |||
# <tt>params.reject{|k,v|request.path_parameters[k]}</tt> | |||
# because <tt>controller</tt> and <tt>action</tt> and other | |||
# "path parameters" are included in params. | |||
# | |||
# current_url: Extract the URL of the current request from your | |||
# application's web request framework and specify it here to have it | |||
# checked against the openid.return_to value in the response. Do not | |||
# just pass <tt>args['openid.return_to']</tt> here; that will defeat the | |||
# purpose of this check. (See OpenID Authentication 2.0 section 11.1.) | |||
# | |||
# If the return_to URL check fails, the status of the completion will be | |||
# FAILURE. | |||
# | |||
# Returns a subclass of Response. The type of response is | |||
# indicated by the status attribute, which will be one of | |||
# SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. | |||
def complete(query, current_url) | |||
message = Message.from_post_args(query) | |||
mode = message.get_arg(OPENID_NS, 'mode', 'invalid') | |||
begin | |||
meth = method('complete_' + mode) | |||
rescue NameError | |||
meth = method(:complete_invalid) | |||
end | |||
response = meth.call(message, current_url) | |||
cleanup_last_requested_endpoint | |||
if [SUCCESS, CANCEL].member?(response.status) | |||
cleanup_session | |||
end | |||
return response | |||
end | |||
protected | |||
def session_get(name) | |||
@session[session_key(name)] | |||
end | |||
def session_set(name, val) | |||
@session[session_key(name)] = val | |||
end | |||
def session_key(suffix) | |||
@session_key_prefix + suffix | |||
end | |||
def last_requested_endpoint | |||
session_get('last_requested_endpoint') | |||
end | |||
def last_requested_endpoint=(endpoint) | |||
session_set('last_requested_endpoint', endpoint) | |||
end | |||
def cleanup_last_requested_endpoint | |||
@session[session_key('last_requested_endpoint')] = nil | |||
end | |||
def discovery_manager(openid_identifier) | |||
DiscoveryManager.new(@session, openid_identifier, @session_key_prefix) | |||
end | |||
def cleanup_session | |||
discovery_manager(nil).cleanup(true) | |||
end | |||
def discover(identifier) | |||
OpenID.discover(identifier) | |||
end | |||
def negotiator | |||
DefaultNegotiator | |||
end | |||
def association_manager(service) | |||
AssociationManager.new(@store, service.server_url, | |||
service.compatibility_mode, negotiator) | |||
end | |||
def handle_idres(message, current_url) | |||
IdResHandler.new(message, current_url, @store, last_requested_endpoint) | |||
end | |||
def complete_invalid(message, unused_return_to) | |||
mode = message.get_arg(OPENID_NS, 'mode', '<No mode set>') | |||
return FailureResponse.new(last_requested_endpoint, | |||
"Invalid openid.mode: #{mode}") | |||
end | |||
def complete_cancel(unused_message, unused_return_to) | |||
return CancelResponse.new(last_requested_endpoint) | |||
end | |||
def complete_error(message, unused_return_to) | |||
error = message.get_arg(OPENID_NS, 'error') | |||
contact = message.get_arg(OPENID_NS, 'contact') | |||
reference = message.get_arg(OPENID_NS, 'reference') | |||
return FailureResponse.new(last_requested_endpoint, | |||
error, contact, reference) | |||
end | |||
def complete_setup_needed(message, unused_return_to) | |||
if message.is_openid1 | |||
return complete_invalid(message, nil) | |||
else | |||
setup_url = message.get_arg(OPENID2_NS, 'user_setup_url') | |||
return SetupNeededResponse.new(last_requested_endpoint, setup_url) | |||
end | |||
end | |||
def complete_id_res(message, current_url) | |||
if message.is_openid1 | |||
setup_url = message.get_arg(OPENID1_NS, 'user_setup_url') | |||
if !setup_url.nil? | |||
return SetupNeededResponse.new(last_requested_endpoint, setup_url) | |||
end | |||
end | |||
begin | |||
idres = handle_idres(message, current_url) | |||
rescue OpenIDError => why | |||
return FailureResponse.new(last_requested_endpoint, why.message) | |||
else | |||
return SuccessResponse.new(idres.endpoint, message, | |||
idres.signed_fields) | |||
end | |||
end | |||
end | |||
end |
@@ -1,340 +0,0 @@ | |||
require "openid/dh" | |||
require "openid/util" | |||
require "openid/kvpost" | |||
require "openid/cryptutil" | |||
require "openid/protocolerror" | |||
require "openid/association" | |||
module OpenID | |||
class Consumer | |||
# A superclass for implementing Diffie-Hellman association sessions. | |||
class DiffieHellmanSession | |||
class << self | |||
attr_reader :session_type, :secret_size, :allowed_assoc_types, | |||
:hashfunc | |||
end | |||
def initialize(dh=nil) | |||
if dh.nil? | |||
dh = DiffieHellman.from_defaults | |||
end | |||
@dh = dh | |||
end | |||
# Return the query parameters for requesting an association | |||
# using this Diffie-Hellman association session | |||
def get_request | |||
args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)} | |||
if (!@dh.using_default_values?) | |||
args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus) | |||
args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator) | |||
end | |||
return args | |||
end | |||
# Process the response from a successful association request and | |||
# return the shared secret for this association | |||
def extract_secret(response) | |||
dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public', | |||
NO_DEFAULT) | |||
enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT) | |||
dh_server_public = CryptUtil.base64_to_num(dh_server_public64) | |||
enc_mac_key = Util.from_base64(enc_mac_key64) | |||
return @dh.xor_secret(self.class.hashfunc, | |||
dh_server_public, enc_mac_key) | |||
end | |||
end | |||
# A Diffie-Hellman association session that uses SHA1 as its hash | |||
# function | |||
class DiffieHellmanSHA1Session < DiffieHellmanSession | |||
@session_type = 'DH-SHA1' | |||
@secret_size = 20 | |||
@allowed_assoc_types = ['HMAC-SHA1'] | |||
@hashfunc = CryptUtil.method(:sha1) | |||
end | |||
# A Diffie-Hellman association session that uses SHA256 as its hash | |||
# function | |||
class DiffieHellmanSHA256Session < DiffieHellmanSession | |||
@session_type = 'DH-SHA256' | |||
@secret_size = 32 | |||
@allowed_assoc_types = ['HMAC-SHA256'] | |||
@hashfunc = CryptUtil.method(:sha256) | |||
end | |||
# An association session that does not use encryption | |||
class NoEncryptionSession | |||
class << self | |||
attr_reader :session_type, :allowed_assoc_types | |||
end | |||
@session_type = 'no-encryption' | |||
@allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] | |||
def get_request | |||
return {} | |||
end | |||
def extract_secret(response) | |||
mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT) | |||
return Util.from_base64(mac_key64) | |||
end | |||
end | |||
# An object that manages creating and storing associations for an | |||
# OpenID provider endpoint | |||
class AssociationManager | |||
def self.create_session(session_type) | |||
case session_type | |||
when 'no-encryption' | |||
NoEncryptionSession.new | |||
when 'DH-SHA1' | |||
DiffieHellmanSHA1Session.new | |||
when 'DH-SHA256' | |||
DiffieHellmanSHA256Session.new | |||
else | |||
raise ArgumentError, "Unknown association session type: "\ | |||
"#{session_type.inspect}" | |||
end | |||
end | |||
def initialize(store, server_url, compatibility_mode=false, | |||
negotiator=nil) | |||
@store = store | |||
@server_url = server_url | |||
@compatibility_mode = compatibility_mode | |||
@negotiator = negotiator || DefaultNegotiator | |||
end | |||
def get_association | |||
if @store.nil? | |||
return nil | |||
end | |||
assoc = @store.get_association(@server_url) | |||
if assoc.nil? || assoc.expires_in <= 0 | |||
assoc = negotiate_association | |||
if !assoc.nil? | |||
@store.store_association(@server_url, assoc) | |||
end | |||
end | |||
return assoc | |||
end | |||
def negotiate_association | |||
assoc_type, session_type = @negotiator.get_allowed_type | |||
begin | |||
return request_association(assoc_type, session_type) | |||
rescue ServerError => why | |||
supported_types = extract_supported_association_type(why, assoc_type) | |||
if !supported_types.nil? | |||
# Attempt to create an association from the assoc_type and | |||
# session_type that the server told us it supported. | |||
assoc_type, session_type = supported_types | |||
begin | |||
return request_association(assoc_type, session_type) | |||
rescue ServerError => why | |||
Util.log("Server #{@server_url} refused its suggested " \ | |||
"association type: session_type=#{session_type}, " \ | |||
"assoc_type=#{assoc_type}") | |||
return nil | |||
end | |||
end | |||
end | |||
end | |||
protected | |||
def extract_supported_association_type(server_error, assoc_type) | |||
# Any error message whose code is not 'unsupported-type' should | |||
# be considered a total failure. | |||
if (server_error.error_code != 'unsupported-type' or | |||
server_error.message.is_openid1) | |||
Util.log("Server error when requesting an association from "\ | |||
"#{@server_url}: #{server_error.error_text}") | |||
return nil | |||
end | |||
# The server didn't like the association/session type that we | |||
# sent, and it sent us back a message that might tell us how to | |||
# handle it. | |||
Util.log("Unsupported association type #{assoc_type}: "\ | |||
"#{server_error.error_text}") | |||
# Extract the session_type and assoc_type from the error message | |||
assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type') | |||
session_type = server_error.message.get_arg(OPENID_NS, 'session_type') | |||
if assoc_type.nil? or session_type.nil? | |||
Util.log("Server #{@server_url} responded with unsupported "\ | |||
"association session but did not supply a fallback.") | |||
return nil | |||
elsif !@negotiator.allowed?(assoc_type, session_type) | |||
Util.log("Server sent unsupported session/association type: "\ | |||
"session_type=#{session_type}, assoc_type=#{assoc_type}") | |||
return nil | |||
else | |||
return [assoc_type, session_type] | |||
end | |||
end | |||
# Make and process one association request to this endpoint's OP | |||
# endpoint URL. Returns an association object or nil if the | |||
# association processing failed. Raises ServerError when the | |||
# remote OpenID server returns an error. | |||
def request_association(assoc_type, session_type) | |||
assoc_session, args = create_associate_request(assoc_type, session_type) | |||
begin | |||
response = OpenID.make_kv_post(args, @server_url) | |||
return extract_association(response, assoc_session) | |||
rescue HTTPStatusError => why | |||
Util.log("Got HTTP status error when requesting association: #{why}") | |||
return nil | |||
rescue Message::KeyNotFound => why | |||
Util.log("Missing required parameter in response from "\ | |||
"#{@server_url}: #{why}") | |||
return nil | |||
rescue ProtocolError => why | |||
Util.log("Protocol error processing response from #{@server_url}: "\ | |||
"#{why}") | |||
return nil | |||
end | |||
end | |||
# Create an association request for the given assoc_type and | |||
# session_type. Returns a pair of the association session object | |||
# and the request message that will be sent to the server. | |||
def create_associate_request(assoc_type, session_type) | |||
assoc_session = self.class.create_session(session_type) | |||
args = { | |||
'mode' => 'associate', | |||
'assoc_type' => assoc_type, | |||
} | |||
if !@compatibility_mode | |||
args['ns'] = OPENID2_NS | |||
end | |||
# Leave out the session type if we're in compatibility mode | |||
# *and* it's no-encryption. | |||
if !@compatibility_mode || | |||
assoc_session.class.session_type != 'no-encryption' | |||
args['session_type'] = assoc_session.class.session_type | |||
end | |||
args.merge!(assoc_session.get_request) | |||
message = Message.from_openid_args(args) | |||
return assoc_session, message | |||
end | |||
# Given an association response message, extract the OpenID 1.X | |||
# session type. Returns the association type for this message | |||
# | |||
# This function mostly takes care of the 'no-encryption' default | |||
# behavior in OpenID 1. | |||
# | |||
# If the association type is plain-text, this function will | |||
# return 'no-encryption' | |||
def get_openid1_session_type(assoc_response) | |||
# If it's an OpenID 1 message, allow session_type to default | |||
# to nil (which signifies "no-encryption") | |||
session_type = assoc_response.get_arg(OPENID1_NS, 'session_type') | |||
# Handle the differences between no-encryption association | |||
# respones in OpenID 1 and 2: | |||
# no-encryption is not really a valid session type for | |||
# OpenID 1, but we'll accept it anyway, while issuing a | |||
# warning. | |||
if session_type == 'no-encryption' | |||
Util.log("WARNING: #{@server_url} sent 'no-encryption'"\ | |||
"for OpenID 1.X") | |||
# Missing or empty session type is the way to flag a | |||
# 'no-encryption' response. Change the session type to | |||
# 'no-encryption' so that it can be handled in the same | |||
# way as OpenID 2 'no-encryption' respones. | |||
elsif session_type == '' || session_type.nil? | |||
session_type = 'no-encryption' | |||
end | |||
return session_type | |||
end | |||
def self.extract_expires_in(message) | |||
# expires_in should be a base-10 string. | |||
expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT) | |||
if !(/\A\d+\Z/ =~ expires_in_str) | |||
raise ProtocolError, "Invalid expires_in field: #{expires_in_str}" | |||
end | |||
expires_in_str.to_i | |||
end | |||
# Attempt to extract an association from the response, given the | |||
# association response message and the established association | |||
# session. | |||
def extract_association(assoc_response, assoc_session) | |||
# Extract the common fields from the response, raising an | |||
# exception if they are not found | |||
assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type', | |||
NO_DEFAULT) | |||
assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle', | |||
NO_DEFAULT) | |||
expires_in = self.class.extract_expires_in(assoc_response) | |||
# OpenID 1 has funny association session behaviour. | |||
if assoc_response.is_openid1 | |||
session_type = get_openid1_session_type(assoc_response) | |||
else | |||
session_type = assoc_response.get_arg(OPENID2_NS, 'session_type', | |||
NO_DEFAULT) | |||
end | |||
# Session type mismatch | |||
if assoc_session.class.session_type != session_type | |||
if (assoc_response.is_openid1 and session_type == 'no-encryption') | |||
# In OpenID 1, any association request can result in a | |||
# 'no-encryption' association response. Setting | |||
# assoc_session to a new no-encryption session should | |||
# make the rest of this function work properly for | |||
# that case. | |||
assoc_session = NoEncryptionSession.new | |||
else | |||
# Any other mismatch, regardless of protocol version | |||
# results in the failure of the association session | |||
# altogether. | |||
raise ProtocolError, "Session type mismatch. Expected "\ | |||
"#{assoc_session.class.session_type}, got "\ | |||
"#{session_type}" | |||
end | |||
end | |||
# Make sure assoc_type is valid for session_type | |||
if !assoc_session.class.allowed_assoc_types.member?(assoc_type) | |||
raise ProtocolError, "Unsupported assoc_type for session "\ | |||
"#{assoc_session.class.session_type} "\ | |||
"returned: #{assoc_type}" | |||
end | |||
# Delegate to the association session to extract the secret | |||
# from the response, however is appropriate for that session | |||
# type. | |||
begin | |||
secret = assoc_session.extract_secret(assoc_response) | |||
rescue Message::KeyNotFound, ArgumentError => why | |||
raise ProtocolError, "Malformed response for "\ | |||
"#{assoc_session.class.session_type} "\ | |||
"session: #{why.message}" | |||
end | |||
return Association.from_expires_in(expires_in, assoc_handle, secret, | |||
assoc_type) | |||
end | |||
end | |||
end | |||
end |
@@ -1,186 +0,0 @@ | |||
require "openid/message" | |||
require "openid/util" | |||
module OpenID | |||
class Consumer | |||
# An object that holds the state necessary for generating an | |||
# OpenID authentication request. This object holds the association | |||
# with the server and the discovered information with which the | |||
# request will be made. | |||
# | |||
# It is separate from the consumer because you may wish to add | |||
# things to the request before sending it on its way to the | |||
# server. It also has serialization options that let you encode | |||
# the authentication request as a URL or as a form POST. | |||
class CheckIDRequest | |||
attr_accessor :return_to_args, :message | |||
attr_reader :endpoint | |||
# Users of this library should not create instances of this | |||
# class. Instances of this class are created by the library | |||
# when needed. | |||
def initialize(assoc, endpoint) | |||
@assoc = assoc | |||
@endpoint = endpoint | |||
@return_to_args = {} | |||
@message = Message.new(endpoint.preferred_namespace) | |||
@anonymous = false | |||
end | |||
attr_reader :anonymous | |||
# Set whether this request should be made anonymously. If a | |||
# request is anonymous, the identifier will not be sent in the | |||
# request. This is only useful if you are making another kind of | |||
# request with an extension in this request. | |||
# | |||
# Anonymous requests are not allowed when the request is made | |||
# with OpenID 1. | |||
def anonymous=(is_anonymous) | |||
if is_anonymous && @message.is_openid1 | |||
raise ArgumentError, ("OpenID1 requests MUST include the "\ | |||
"identifier in the request") | |||
end | |||
@anonymous = is_anonymous | |||
end | |||
# Add an object that implements the extension interface for | |||
# adding arguments to an OpenID message to this checkid request. | |||
# | |||
# extension_request: an OpenID::Extension object. | |||
def add_extension(extension_request) | |||
extension_request.to_message(@message) | |||
end | |||
# Add an extension argument to this OpenID authentication | |||
# request. You probably want to use add_extension and the | |||
# OpenID::Extension interface. | |||
# | |||
# Use caution when adding arguments, because they will be | |||
# URL-escaped and appended to the redirect URL, which can easily | |||
# get quite long. | |||
def add_extension_arg(namespace, key, value) | |||
@message.set_arg(namespace, key, value) | |||
end | |||
# Produce a OpenID::Message representing this request. | |||
# | |||
# Not specifying a return_to URL means that the user will not be | |||
# returned to the site issuing the request upon its completion. | |||
# | |||
# If immediate mode is requested, the OpenID provider is to send | |||
# back a response immediately, useful for behind-the-scenes | |||
# authentication attempts. Otherwise the OpenID provider may | |||
# engage the user before providing a response. This is the | |||
# default case, as the user may need to provide credentials or | |||
# approve the request before a positive response can be sent. | |||
def get_message(realm, return_to=nil, immediate=false) | |||
if !return_to.nil? | |||
return_to = Util.append_args(return_to, @return_to_args) | |||
elsif immediate | |||
raise ArgumentError, ('"return_to" is mandatory when using '\ | |||
'"checkid_immediate"') | |||
elsif @message.is_openid1 | |||
raise ArgumentError, ('"return_to" is mandatory for OpenID 1 '\ | |||
'requests') | |||
elsif @return_to_args.empty? | |||
raise ArgumentError, ('extra "return_to" arguments were specified, '\ | |||
'but no return_to was specified') | |||
end | |||
message = @message.copy | |||
mode = immediate ? 'checkid_immediate' : 'checkid_setup' | |||
message.set_arg(OPENID_NS, 'mode', mode) | |||
realm_key = message.is_openid1 ? 'trust_root' : 'realm' | |||
message.set_arg(OPENID_NS, realm_key, realm) | |||
if !return_to.nil? | |||
message.set_arg(OPENID_NS, 'return_to', return_to) | |||
end | |||
if not @anonymous | |||
if @endpoint.is_op_identifier | |||
# This will never happen when we're in OpenID 1 | |||
# compatibility mode, as long as is_op_identifier() | |||
# returns false whenever preferred_namespace returns | |||
# OPENID1_NS. | |||
claimed_id = request_identity = IDENTIFIER_SELECT | |||
else | |||
request_identity = @endpoint.get_local_id | |||
claimed_id = @endpoint.claimed_id | |||
end | |||
# This is true for both OpenID 1 and 2 | |||
message.set_arg(OPENID_NS, 'identity', request_identity) | |||
if message.is_openid2 | |||
message.set_arg(OPENID2_NS, 'claimed_id', claimed_id) | |||
end | |||
end | |||
if @assoc | |||
message.set_arg(OPENID_NS, 'assoc_handle', @assoc.handle) | |||
assoc_log_msg = "with assocication #{@assoc.handle}" | |||
else | |||
assoc_log_msg = 'using stateless mode.' | |||
end | |||
Util.log("Generated #{mode} request to #{@endpoint.server_url} "\ | |||
"#{assoc_log_msg}") | |||
return message | |||
end | |||
# Returns a URL with an encoded OpenID request. | |||
# | |||
# The resulting URL is the OpenID provider's endpoint URL with | |||
# parameters appended as query arguments. You should redirect | |||
# the user agent to this URL. | |||
# | |||
# OpenID 2.0 endpoints also accept POST requests, see | |||
# 'send_redirect?' and 'form_markup'. | |||
def redirect_url(realm, return_to=nil, immediate=false) | |||
message = get_message(realm, return_to, immediate) | |||
return message.to_url(@endpoint.server_url) | |||
end | |||
# Get html for a form to submit this request to the IDP. | |||
# | |||
# form_tag_attrs is a hash of attributes to be added to the form | |||
# tag. 'accept-charset' and 'enctype' have defaults that can be | |||
# overridden. If a value is supplied for 'action' or 'method', | |||
# it will be replaced. | |||
def form_markup(realm, return_to=nil, immediate=false, | |||
form_tag_attrs=nil) | |||
message = get_message(realm, return_to, immediate) | |||
return message.to_form_markup(@endpoint.server_url, form_tag_attrs) | |||
end | |||
# Get a complete HTML document that autosubmits the request to the IDP | |||
# with javascript. This method wraps form_markup - see that method's | |||
# documentation for help with the parameters. | |||
def html_markup(realm, return_to=nil, immediate=false, | |||
form_tag_attrs=nil) | |||
Util.auto_submit_html(form_markup(realm, | |||
return_to, | |||
immediate, | |||
form_tag_attrs)) | |||
end | |||
# Should this OpenID authentication request be sent as a HTTP | |||
# redirect or as a POST (form submission)? | |||
# | |||
# This takes the same parameters as redirect_url or form_markup | |||
def send_redirect?(realm, return_to=nil, immediate=false) | |||
if @endpoint.compatibility_mode | |||
return true | |||
else | |||
url = redirect_url(realm, return_to, immediate) | |||
return url.length <= OPENID1_URL_LIMIT | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -1,498 +0,0 @@ | |||
# Functions to discover OpenID endpoints from identifiers. | |||
require 'uri' | |||
require 'openid/util' | |||
require 'openid/fetchers' | |||
require 'openid/urinorm' | |||
require 'openid/message' | |||
require 'openid/yadis/discovery' | |||
require 'openid/yadis/xrds' | |||
require 'openid/yadis/xri' | |||
require 'openid/yadis/services' | |||
require 'openid/yadis/filters' | |||
require 'openid/consumer/html_parse' | |||
require 'openid/yadis/xrires' | |||
module OpenID | |||
OPENID_1_0_NS = 'http://openid.net/xmlns/1.0' | |||
OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server' | |||
OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon' | |||
OPENID_1_1_TYPE = 'http://openid.net/signon/1.1' | |||
OPENID_1_0_TYPE = 'http://openid.net/signon/1.0' | |||
OPENID_1_0_MESSAGE_NS = OPENID1_NS | |||
OPENID_2_0_MESSAGE_NS = OPENID2_NS | |||
# Object representing an OpenID service endpoint. | |||
class OpenIDServiceEndpoint | |||
# OpenID service type URIs, listed in order of preference. The | |||
# ordering of this list affects yadis and XRI service discovery. | |||
OPENID_TYPE_URIS = [ | |||
OPENID_IDP_2_0_TYPE, | |||
OPENID_2_0_TYPE, | |||
OPENID_1_1_TYPE, | |||
OPENID_1_0_TYPE, | |||
] | |||
# the verified identifier. | |||
attr_accessor :claimed_id | |||
# For XRI, the persistent identifier. | |||
attr_accessor :canonical_id | |||
attr_accessor :server_url, :type_uris, :local_id, :used_yadis | |||
def initialize | |||
@claimed_id = nil | |||
@server_url = nil | |||
@type_uris = [] | |||
@local_id = nil | |||
@canonical_id = nil | |||
@used_yadis = false # whether this came from an XRDS | |||
@display_identifier = nil | |||
end | |||
def display_identifier | |||
return @display_identifier if @display_identifier | |||
return @claimed_id if @claimed_id.nil? | |||
begin | |||
parsed_identifier = URI.parse(@claimed_id) | |||
rescue URI::InvalidURIError | |||
raise ProtocolError, "Claimed identifier #{claimed_id} is not a valid URI" | |||
end | |||
return @claimed_id if not parsed_identifier.fragment | |||
disp = parsed_identifier | |||
disp.fragment = nil | |||
return disp.to_s | |||
end | |||
def display_identifier=(display_identifier) | |||
@display_identifier = display_identifier | |||
end | |||
def uses_extension(extension_uri) | |||
return @type_uris.member?(extension_uri) | |||
end | |||
def preferred_namespace | |||
if (@type_uris.member?(OPENID_IDP_2_0_TYPE) or | |||
@type_uris.member?(OPENID_2_0_TYPE)) | |||
return OPENID_2_0_MESSAGE_NS | |||
else | |||
return OPENID_1_0_MESSAGE_NS | |||
end | |||
end | |||
def supports_type(type_uri) | |||
# Does this endpoint support this type? | |||
# | |||
# I consider C{/server} endpoints to implicitly support C{/signon}. | |||
( | |||
@type_uris.member?(type_uri) or | |||
(type_uri == OPENID_2_0_TYPE and is_op_identifier()) | |||
) | |||
end | |||
def compatibility_mode | |||
return preferred_namespace() != OPENID_2_0_MESSAGE_NS | |||
end | |||
def is_op_identifier | |||
return @type_uris.member?(OPENID_IDP_2_0_TYPE) | |||
end | |||
def parse_service(yadis_url, uri, type_uris, service_element) | |||
# Set the state of this object based on the contents of the | |||
# service element. | |||
@type_uris = type_uris | |||
@server_url = uri | |||
@used_yadis = true | |||
if !is_op_identifier() | |||
# XXX: This has crappy implications for Service elements that | |||
# contain both 'server' and 'signon' Types. But that's a | |||
# pathological configuration anyway, so I don't think I care. | |||
@local_id = OpenID.find_op_local_identifier(service_element, | |||
@type_uris) | |||
@claimed_id = yadis_url | |||
end | |||
end | |||
def get_local_id | |||
# Return the identifier that should be sent as the | |||
# openid.identity parameter to the server. | |||
if @local_id.nil? and @canonical_id.nil? | |||
return @claimed_id | |||
else | |||
return (@local_id or @canonical_id) | |||
end | |||
end | |||
def self.from_basic_service_endpoint(endpoint) | |||
# Create a new instance of this class from the endpoint object | |||
# passed in. | |||
# | |||
# @return: nil or OpenIDServiceEndpoint for this endpoint object""" | |||
type_uris = endpoint.match_types(OPENID_TYPE_URIS) | |||
# If any Type URIs match and there is an endpoint URI specified, | |||
# then this is an OpenID endpoint | |||
if (!type_uris.nil? and !type_uris.empty?) and !endpoint.uri.nil? | |||
openid_endpoint = self.new | |||
openid_endpoint.parse_service( | |||
endpoint.yadis_url, | |||
endpoint.uri, | |||
endpoint.type_uris, | |||
endpoint.service_element) | |||
else | |||
openid_endpoint = nil | |||
end | |||
return openid_endpoint | |||
end | |||
def self.from_html(uri, html) | |||
# Parse the given document as HTML looking for an OpenID <link | |||
# rel=...> | |||
# | |||
# @rtype: [OpenIDServiceEndpoint] | |||
discovery_types = [ | |||
[OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'], | |||
[OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'], | |||
] | |||
link_attrs = OpenID.parse_link_attrs(html) | |||
services = [] | |||
discovery_types.each { |type_uri, op_endpoint_rel, local_id_rel| | |||
op_endpoint_url = OpenID.find_first_href(link_attrs, op_endpoint_rel) | |||
if !op_endpoint_url | |||
next | |||
end | |||
service = self.new | |||
service.claimed_id = uri | |||
service.local_id = OpenID.find_first_href(link_attrs, local_id_rel) | |||
service.server_url = op_endpoint_url | |||
service.type_uris = [type_uri] | |||
services << service | |||
} | |||
return services | |||
end | |||
def self.from_xrds(uri, xrds) | |||
# Parse the given document as XRDS looking for OpenID services. | |||
# | |||
# @rtype: [OpenIDServiceEndpoint] | |||
# | |||
# @raises L{XRDSError}: When the XRDS does not parse. | |||
return Yadis::apply_filter(uri, xrds, self) | |||
end | |||
def self.from_discovery_result(discoveryResult) | |||
# Create endpoints from a DiscoveryResult. | |||
# | |||
# @type discoveryResult: L{DiscoveryResult} | |||
# | |||
# @rtype: list of L{OpenIDServiceEndpoint} | |||
# | |||
# @raises L{XRDSError}: When the XRDS does not parse. | |||
if discoveryResult.is_xrds() | |||
meth = self.method('from_xrds') | |||
else | |||
meth = self.method('from_html') | |||
end | |||
return meth.call(discoveryResult.normalized_uri, | |||
discoveryResult.response_text) | |||
end | |||
def self.from_op_endpoint_url(op_endpoint_url) | |||
# Construct an OP-Identifier OpenIDServiceEndpoint object for | |||
# a given OP Endpoint URL | |||
# | |||
# @param op_endpoint_url: The URL of the endpoint | |||
# @rtype: OpenIDServiceEndpoint | |||
service = self.new | |||
service.server_url = op_endpoint_url | |||
service.type_uris = [OPENID_IDP_2_0_TYPE] | |||
return service | |||
end | |||
def to_s | |||
return sprintf("<%s server_url=%s claimed_id=%s " + | |||
"local_id=%s canonical_id=%s used_yadis=%s>", | |||
self.class, @server_url, @claimed_id, | |||
@local_id, @canonical_id, @used_yadis) | |||
end | |||
end | |||
def self.find_op_local_identifier(service_element, type_uris) | |||
# Find the OP-Local Identifier for this xrd:Service element. | |||
# | |||
# This considers openid:Delegate to be a synonym for xrd:LocalID | |||
# if both OpenID 1.X and OpenID 2.0 types are present. If only | |||
# OpenID 1.X is present, it returns the value of | |||
# openid:Delegate. If only OpenID 2.0 is present, it returns the | |||
# value of xrd:LocalID. If there is more than one LocalID tag and | |||
# the values are different, it raises a DiscoveryFailure. This is | |||
# also triggered when the xrd:LocalID and openid:Delegate tags are | |||
# different. | |||
# XXX: Test this function on its own! | |||
# Build the list of tags that could contain the OP-Local | |||
# Identifier | |||
local_id_tags = [] | |||
if type_uris.member?(OPENID_1_1_TYPE) or | |||
type_uris.member?(OPENID_1_0_TYPE) | |||
# local_id_tags << Yadis::nsTag(OPENID_1_0_NS, 'openid', 'Delegate') | |||
service_element.add_namespace('openid', OPENID_1_0_NS) | |||
local_id_tags << "openid:Delegate" | |||
end | |||
if type_uris.member?(OPENID_2_0_TYPE) | |||
# local_id_tags.append(Yadis::nsTag(XRD_NS_2_0, 'xrd', 'LocalID')) | |||
service_element.add_namespace('xrd', Yadis::XRD_NS_2_0) | |||
local_id_tags << "xrd:LocalID" | |||
end | |||
# Walk through all the matching tags and make sure that they all | |||
# have the same value | |||
local_id = nil | |||
local_id_tags.each { |local_id_tag| | |||
service_element.each_element(local_id_tag) { |local_id_element| | |||
if local_id.nil? | |||
local_id = local_id_element.text | |||
elsif local_id != local_id_element.text | |||
format = 'More than one %s tag found in one service element' | |||
message = sprintf(format, local_id_tag) | |||
raise DiscoveryFailure.new(message, nil) | |||
end | |||
} | |||
} | |||
return local_id | |||
end | |||
def self.normalize_xri(xri) | |||
# Normalize an XRI, stripping its scheme if present | |||
m = /^xri:\/\/(.*)/.match(xri) | |||
xri = m[1] if m | |||
return xri | |||
end | |||
def self.normalize_url(url) | |||
# Normalize a URL, converting normalization failures to | |||
# DiscoveryFailure | |||
begin | |||
normalized = URINorm.urinorm(url) | |||
rescue URI::Error => why | |||
raise DiscoveryFailure.new("Error normalizing #{url}: #{why.message}", nil) | |||
else | |||
defragged = URI::parse(normalized) | |||
defragged.fragment = nil | |||
return defragged.normalize.to_s | |||
end | |||
end | |||
def self.best_matching_service(service, preferred_types) | |||
# Return the index of the first matching type, or something higher | |||
# if no type matches. | |||
# | |||
# This provides an ordering in which service elements that contain | |||
# a type that comes earlier in the preferred types list come | |||
# before service elements that come later. If a service element | |||
# has more than one type, the most preferred one wins. | |||
preferred_types.each_with_index { |value, index| | |||
if service.type_uris.member?(value) | |||
return index | |||
end | |||
} | |||
return preferred_types.length | |||
end | |||
def self.arrange_by_type(service_list, preferred_types) | |||
# Rearrange service_list in a new list so services are ordered by | |||
# types listed in preferred_types. Return the new list. | |||
# Build a list with the service elements in tuples whose | |||
# comparison will prefer the one with the best matching service | |||
prio_services = [] | |||
service_list.each_with_index { |s, index| | |||
prio_services << [best_matching_service(s, preferred_types), index, s] | |||
} | |||
prio_services.sort! | |||
# Now that the services are sorted by priority, remove the sort | |||
# keys from the list. | |||
(0...prio_services.length).each { |i| | |||
prio_services[i] = prio_services[i][2] | |||
} | |||
return prio_services | |||
end | |||
def self.get_op_or_user_services(openid_services) | |||
# Extract OP Identifier services. If none found, return the rest, | |||
# sorted with most preferred first according to | |||
# OpenIDServiceEndpoint.openid_type_uris. | |||
# | |||
# openid_services is a list of OpenIDServiceEndpoint objects. | |||
# | |||
# Returns a list of OpenIDServiceEndpoint objects. | |||
op_services = arrange_by_type(openid_services, [OPENID_IDP_2_0_TYPE]) | |||
openid_services = arrange_by_type(openid_services, | |||
OpenIDServiceEndpoint::OPENID_TYPE_URIS) | |||
if !op_services.empty? | |||
return op_services | |||
else | |||
return openid_services | |||
end | |||
end | |||
def self.discover_yadis(uri) | |||
# Discover OpenID services for a URI. Tries Yadis and falls back | |||
# on old-style <link rel='...'> discovery if Yadis fails. | |||
# | |||
# @param uri: normalized identity URL | |||
# @type uri: str | |||
# | |||
# @return: (claimed_id, services) | |||
# @rtype: (str, list(OpenIDServiceEndpoint)) | |||
# | |||
# @raises DiscoveryFailure: when discovery fails. | |||
# Might raise a yadis.discover.DiscoveryFailure if no document | |||
# came back for that URI at all. I don't think falling back to | |||
# OpenID 1.0 discovery on the same URL will help, so don't bother | |||
# to catch it. | |||
response = Yadis.discover(uri) | |||
yadis_url = response.normalized_uri | |||
body = response.response_text | |||
begin | |||
openid_services = OpenIDServiceEndpoint.from_xrds(yadis_url, body) | |||
rescue Yadis::XRDSError | |||
# Does not parse as a Yadis XRDS file | |||
openid_services = [] | |||
end | |||
if openid_services.empty? | |||
# Either not an XRDS or there are no OpenID services. | |||
if response.is_xrds | |||
# if we got the Yadis content-type or followed the Yadis | |||
# header, re-fetch the document without following the Yadis | |||
# header, with no Accept header. | |||
return self.discover_no_yadis(uri) | |||
end | |||
# Try to parse the response as HTML. | |||
# <link rel="..."> | |||
openid_services = OpenIDServiceEndpoint.from_html(yadis_url, body) | |||
end | |||
return [yadis_url, self.get_op_or_user_services(openid_services)] | |||
end | |||
def self.discover_xri(iname) | |||
endpoints = [] | |||
iname = self.normalize_xri(iname) | |||
begin | |||
canonical_id, services = Yadis::XRI::ProxyResolver.new().query( | |||
iname, OpenIDServiceEndpoint::OPENID_TYPE_URIS) | |||
if canonical_id.nil? | |||
raise Yadis::XRDSError.new(sprintf('No CanonicalID found for XRI %s', iname)) | |||
end | |||
flt = Yadis.make_filter(OpenIDServiceEndpoint) | |||
services.each { |service_element| | |||
endpoints += flt.get_service_endpoints(iname, service_element) | |||
} | |||
rescue Yadis::XRDSError => why | |||
Util.log('xrds error on ' + iname + ': ' + why.to_s) | |||
end | |||
endpoints.each { |endpoint| | |||
# Is there a way to pass this through the filter to the endpoint | |||
# constructor instead of tacking it on after? | |||
endpoint.canonical_id = canonical_id | |||
endpoint.claimed_id = canonical_id | |||
endpoint.display_identifier = iname | |||
} | |||
# FIXME: returned xri should probably be in some normal form | |||
return [iname, self.get_op_or_user_services(endpoints)] | |||
end | |||
def self.discover_no_yadis(uri) | |||
http_resp = OpenID.fetch(uri) | |||
if http_resp.code != "200" and http_resp.code != "206" | |||
raise DiscoveryFailure.new( | |||
"HTTP Response status from identity URL host is not \"200\". "\ | |||
"Got status #{http_resp.code.inspect}", http_resp) | |||
end | |||
claimed_id = http_resp.final_url | |||
openid_services = OpenIDServiceEndpoint.from_html( | |||
claimed_id, http_resp.body) | |||
return [claimed_id, openid_services] | |||
end | |||
def self.discover_uri(uri) | |||
# Hack to work around URI parsing for URls with *no* scheme. | |||
if uri.index("://").nil? | |||
uri = 'http://' + uri | |||
end | |||
begin | |||
parsed = URI::parse(uri) | |||
rescue URI::InvalidURIError => why | |||
raise DiscoveryFailure.new("URI is not valid: #{why.message}", nil) | |||
end | |||
if !parsed.scheme.nil? and !parsed.scheme.empty? | |||
if !['http', 'https'].member?(parsed.scheme) | |||
raise DiscoveryFailure.new( | |||
"URI scheme #{parsed.scheme} is not HTTP or HTTPS", nil) | |||
end | |||
end | |||
uri = self.normalize_url(uri) | |||
claimed_id, openid_services = self.discover_yadis(uri) | |||
claimed_id = self.normalize_url(claimed_id) | |||
return [claimed_id, openid_services] | |||
end | |||
def self.discover(identifier) | |||
if Yadis::XRI::identifier_scheme(identifier) == :xri | |||
normalized_identifier, services = discover_xri(identifier) | |||
else | |||
return discover_uri(identifier) | |||
end | |||
end | |||
end |
@@ -1,123 +0,0 @@ | |||
module OpenID | |||
class Consumer | |||
# A set of discovered services, for tracking which providers have | |||
# been attempted for an OpenID identifier | |||
class DiscoveredServices | |||
attr_reader :current | |||
def initialize(starting_url, yadis_url, services) | |||
@starting_url = starting_url | |||
@yadis_url = yadis_url | |||
@services = services.dup | |||
@current = nil | |||
end | |||
def next | |||
@current = @services.shift | |||
end | |||
def for_url?(url) | |||
[@starting_url, @yadis_url].member?(url) | |||
end | |||
def started? | |||
!@current.nil? | |||
end | |||
def empty? | |||
@services.empty? | |||
end | |||
end | |||
# Manages calling discovery and tracking which endpoints have | |||
# already been attempted. | |||
class DiscoveryManager | |||
def initialize(session, url, session_key_suffix=nil) | |||
@url = url | |||
@session = session | |||
@session_key_suffix = session_key_suffix || 'auth' | |||
end | |||
def get_next_service | |||
manager = get_manager | |||
if !manager.nil? && manager.empty? | |||
destroy_manager | |||
manager = nil | |||
end | |||
if manager.nil? | |||
yadis_url, services = yield @url | |||
manager = create_manager(yadis_url, services) | |||
end | |||
if !manager.nil? | |||
service = manager.next | |||
store(manager) | |||
else | |||
service = nil | |||
end | |||
return service | |||
end | |||
def cleanup(force=false) | |||
manager = get_manager(force) | |||
if !manager.nil? | |||
service = manager.current | |||
destroy_manager(force) | |||
else | |||
service = nil | |||
end | |||
return service | |||
end | |||
protected | |||
def get_manager(force=false) | |||
manager = load | |||
if force || manager.nil? || manager.for_url?(@url) | |||
return manager | |||
else | |||
return nil | |||
end | |||
end | |||
def create_manager(yadis_url, services) | |||
manager = get_manager | |||
if !manager.nil? | |||
raise StandardError, "There is already a manager for #{yadis_url}" | |||
end | |||
if services.empty? | |||
return nil | |||
end | |||
manager = DiscoveredServices.new(@url, yadis_url, services) | |||
store(manager) | |||
return manager | |||
end | |||
def destroy_manager(force=false) | |||
if !get_manager(force).nil? | |||
destroy! | |||
end | |||
end | |||
def session_key | |||
'OpenID::Consumer::DiscoveredServices::' + @session_key_suffix | |||
end | |||
def store(manager) | |||
@session[session_key] = manager | |||
end | |||
def load | |||
@session[session_key] | |||
end | |||
def destroy! | |||
@session[session_key] = nil | |||
end | |||
end | |||
end | |||
end |
@@ -1,134 +0,0 @@ | |||
require "openid/yadis/htmltokenizer" | |||
module OpenID | |||
# Stuff to remove before we start looking for tags | |||
REMOVED_RE = / | |||
# Comments | |||
<!--.*?--> | |||
# CDATA blocks | |||
| <!\[CDATA\[.*?\]\]> | |||
# script blocks | |||
| <script\b | |||
# make sure script is not an XML namespace | |||
(?!:) | |||
[^>]*>.*?<\/script> | |||
/mixu | |||
def OpenID.openid_unescape(s) | |||
s.gsub('&','&').gsub('<','<').gsub('>','>').gsub('"','"') | |||
end | |||
def OpenID.unescape_hash(h) | |||
newh = {} | |||
h.map{|k,v| | |||
newh[k]=openid_unescape(v) | |||
} | |||
newh | |||
end | |||
def OpenID.parse_link_attrs(html) | |||
stripped = html.gsub(REMOVED_RE,'') | |||
parser = HTMLTokenizer.new(stripped) | |||
links = [] | |||
# to keep track of whether or not we are in the head element | |||
in_head = false | |||
in_html = false | |||
saw_head = false | |||
begin | |||
while el = parser.getTag('head', '/head', 'link', 'body', '/body', | |||
'html', '/html') | |||
# we are leaving head or have reached body, so we bail | |||
return links if ['/head', 'body', '/body', '/html'].member?(el.tag_name) | |||
# enforce html > head > link | |||
if el.tag_name == 'html' | |||
in_html = true | |||
end | |||
next unless in_html | |||
if el.tag_name == 'head' | |||
if saw_head | |||
return links #only allow one head | |||
end | |||
saw_head = true | |||
unless el.to_s[-2] == 47 # tag ends with a /: a short tag | |||
in_head = true | |||
end | |||
end | |||
next unless in_head | |||
return links if el.tag_name == 'html' | |||
if el.tag_name == 'link' | |||
links << unescape_hash(el.attr_hash) | |||
end | |||
end | |||
rescue Exception # just stop parsing if there's an error | |||
end | |||
return links | |||
end | |||
def OpenID.rel_matches(rel_attr, target_rel) | |||
# Does this target_rel appear in the rel_str? | |||
# XXX: TESTME | |||
rels = rel_attr.strip().split() | |||
rels.each { |rel| | |||
rel = rel.downcase | |||
if rel == target_rel | |||
return true | |||
end | |||
} | |||
return false | |||
end | |||
def OpenID.link_has_rel(link_attrs, target_rel) | |||
# Does this link have target_rel as a relationship? | |||
# XXX: TESTME | |||
rel_attr = link_attrs['rel'] | |||
return (rel_attr and rel_matches(rel_attr, target_rel)) | |||
end | |||
def OpenID.find_links_rel(link_attrs_list, target_rel) | |||
# Filter the list of link attributes on whether it has target_rel | |||
# as a relationship. | |||
# XXX: TESTME | |||
matchesTarget = lambda { |attrs| link_has_rel(attrs, target_rel) } | |||
result = [] | |||
link_attrs_list.each { |item| | |||
if matchesTarget.call(item) | |||
result << item | |||
end | |||
} | |||
return result | |||
end | |||
def OpenID.find_first_href(link_attrs_list, target_rel) | |||
# Return the value of the href attribute for the first link tag in | |||
# the list that has target_rel as a relationship. | |||
# XXX: TESTME | |||
matches = find_links_rel(link_attrs_list, target_rel) | |||
if !matches or matches.empty? | |||
return nil | |||
end | |||
first = matches[0] | |||
return first['href'] | |||
end | |||
end | |||
@@ -1,523 +0,0 @@ | |||
require "openid/message" | |||
require "openid/protocolerror" | |||
require "openid/kvpost" | |||
require "openid/consumer/discovery" | |||
require "openid/urinorm" | |||
module OpenID | |||
class TypeURIMismatch < ProtocolError | |||
attr_reader :type_uri, :endpoint | |||
def initialize(type_uri, endpoint) | |||
@type_uri = type_uri | |||
@endpoint = endpoint | |||
end | |||
end | |||
class Consumer | |||
@openid1_return_to_nonce_name = 'rp_nonce' | |||
@openid1_return_to_claimed_id_name = 'openid1_claimed_id' | |||
# Set the name of the query parameter that this library will use | |||
# to thread a nonce through an OpenID 1 transaction. It will be | |||
# appended to the return_to URL. | |||
def self.openid1_return_to_nonce_name=(query_arg_name) | |||
@openid1_return_to_nonce_name = query_arg_name | |||
end | |||
# See openid1_return_to_nonce_name= documentation | |||
def self.openid1_return_to_nonce_name | |||
@openid1_return_to_nonce_name | |||
end | |||
# Set the name of the query parameter that this library will use | |||
# to thread the requested URL through an OpenID 1 transaction (for | |||
# use when verifying discovered information). It will be appended | |||
# to the return_to URL. | |||
def self.openid1_return_to_claimed_id_name=(query_arg_name) | |||
@openid1_return_to_claimed_id_name = query_arg_name | |||
end | |||
# See openid1_return_to_claimed_id_name= | |||
def self.openid1_return_to_claimed_id_name | |||
@openid1_return_to_claimed_id_name | |||
end | |||
# Handles an openid.mode=id_res response. This object is | |||
# instantiated and used by the Consumer. | |||
class IdResHandler | |||
attr_reader :endpoint, :message | |||
def initialize(message, current_url, store=nil, endpoint=nil) | |||
@store = store # Fer the nonce and invalidate_handle | |||
@message = message | |||
@endpoint = endpoint | |||
@current_url = current_url | |||
@signed_list = nil | |||
# Start the verification process | |||
id_res | |||
end | |||
def signed_fields | |||
signed_list.map {|x| 'openid.' + x} | |||
end | |||
protected | |||
# This method will raise ProtocolError unless the request is a | |||
# valid id_res response. Once it has been verified, the methods | |||
# 'endpoint', 'message', and 'signed_fields' contain the | |||
# verified information. | |||
def id_res | |||
check_for_fields | |||
verify_return_to | |||
verify_discovery_results | |||
check_signature | |||
check_nonce | |||
end | |||
def server_url | |||
@endpoint.nil? ? nil : @endpoint.server_url | |||
end | |||
def openid_namespace | |||
@message.get_openid_namespace | |||
end | |||
def fetch(field, default=NO_DEFAULT) | |||
@message.get_arg(OPENID_NS, field, default) | |||
end | |||
def signed_list | |||
if @signed_list.nil? | |||
signed_list_str = fetch('signed', nil) | |||
if signed_list_str.nil? | |||
raise ProtocolError, 'Response missing signed list' | |||
end | |||
@signed_list = signed_list_str.split(',', -1) | |||
end | |||
@signed_list | |||
end | |||
def check_for_fields | |||
# XXX: if a field is missing, we should not have to explicitly | |||
# check that it's present, just make sure that the fields are | |||
# actually being used by the rest of the code in | |||
# tests. Although, which fields are signed does need to be | |||
# checked somewhere. | |||
basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed'] | |||
basic_sig_fields = ['return_to', 'identity'] | |||
case openid_namespace | |||
when OPENID2_NS | |||
require_fields = basic_fields + ['op_endpoint'] | |||
require_sigs = basic_sig_fields + | |||
['response_nonce', 'claimed_id', 'assoc_handle',] | |||
when OPENID1_NS | |||
require_fields = basic_fields + ['identity'] | |||
require_sigs = basic_sig_fields | |||
else | |||
raise RuntimeError, "check_for_fields doesn't know about "\ | |||
"namespace #{openid_namespace.inspect}" | |||
end | |||
require_fields.each do |field| | |||
if !@message.has_key?(OPENID_NS, field) | |||
raise ProtocolError, "Missing required field #{field}" | |||
end | |||
end | |||
require_sigs.each do |field| | |||
# Field is present and not in signed list | |||
if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field) | |||
raise ProtocolError, "#{field.inspect} not signed" | |||
end | |||
end | |||
end | |||
def verify_return_to | |||
begin | |||
msg_return_to = URI.parse(URINorm::urinorm(fetch('return_to'))) | |||
rescue URI::InvalidURIError | |||
raise ProtocolError, ("return_to is not a valid URI") | |||
end | |||
verify_return_to_args(msg_return_to) | |||
if !@current_url.nil? | |||
verify_return_to_base(msg_return_to) | |||
end | |||
end | |||
def verify_return_to_args(msg_return_to) | |||
return_to_parsed_query = {} | |||
if !msg_return_to.query.nil? | |||
CGI.parse(msg_return_to.query).each_pair do |k, vs| | |||
return_to_parsed_query[k] = vs[0] | |||
end | |||
end | |||
query = @message.to_post_args | |||
return_to_parsed_query.each_pair do |rt_key, rt_val| | |||
msg_val = query[rt_key] | |||
if msg_val.nil? | |||
raise ProtocolError, "Message missing return_to argument '#{rt_key}'" | |||
elsif msg_val != rt_val | |||
raise ProtocolError, ("Parameter '#{rt_key}' value "\ | |||
"#{msg_val.inspect} does not match "\ | |||
"return_to's value #{rt_val.inspect}") | |||
end | |||
end | |||
@message.get_args(BARE_NS).each_pair do |bare_key, bare_val| | |||
rt_val = return_to_parsed_query[bare_key] | |||
if not return_to_parsed_query.has_key? bare_key | |||
# This may be caused by your web framework throwing extra | |||
# entries in to your parameters hash that were not GET or | |||
# POST parameters. For example, Rails has been known to | |||
# add "controller" and "action" keys; another server adds | |||
# at least a "format" key. | |||
raise ProtocolError, ("Unexpected parameter (not on return_to): "\ | |||
"'#{bare_key}'=#{rt_val.inspect})") | |||
end | |||
if rt_val != bare_val | |||
raise ProtocolError, ("Parameter '#{bare_key}' value "\ | |||
"#{bare_val.inspect} does not match "\ | |||
"return_to's value #{rt_val.inspect}") | |||
end | |||
end | |||
end | |||
def verify_return_to_base(msg_return_to) | |||
begin | |||
app_parsed = URI.parse(URINorm::urinorm(@current_url)) | |||
rescue URI::InvalidURIError | |||
raise ProtocolError, "current_url is not a valid URI: #{@current_url}" | |||
end | |||
[:scheme, :host, :port, :path].each do |meth| | |||
if msg_return_to.send(meth) != app_parsed.send(meth) | |||
raise ProtocolError, "return_to #{meth.to_s} does not match" | |||
end | |||
end | |||
end | |||
# Raises ProtocolError if the signature is bad | |||
def check_signature | |||
if @store.nil? | |||
assoc = nil | |||
else | |||
assoc = @store.get_association(server_url, fetch('assoc_handle')) | |||
end | |||
if assoc.nil? | |||
check_auth | |||
else | |||
if assoc.expires_in <= 0 | |||
# XXX: It might be a good idea sometimes to re-start the | |||
# authentication with a new association. Doing it | |||
# automatically opens the possibility for | |||
# denial-of-service by a server that just returns expired | |||
# associations (or really short-lived associations) | |||
raise ProtocolError, "Association with #{server_url} expired" | |||
elsif !assoc.check_message_signature(@message) | |||
raise ProtocolError, "Bad signature in response from #{server_url}" | |||
end | |||
end | |||
end | |||
def check_auth | |||
Util.log("Using 'check_authentication' with #{server_url}") | |||
begin | |||
request = create_check_auth_request | |||
rescue Message::KeyNotFound => why | |||
raise ProtocolError, "Could not generate 'check_authentication' "\ | |||
"request: #{why.message}" | |||
end | |||
response = OpenID.make_kv_post(request, server_url) | |||
process_check_auth_response(response) | |||
end | |||
def create_check_auth_request | |||
signed_list = @message.get_arg(OPENID_NS, 'signed', NO_DEFAULT).split(',') | |||
# check that we got all the signed arguments | |||
signed_list.each {|k| | |||
@message.get_aliased_arg(k, NO_DEFAULT) | |||
} | |||
ca_message = @message.copy | |||
ca_message.set_arg(OPENID_NS, 'mode', 'check_authentication') | |||
return ca_message | |||
end | |||
# Process the response message from a check_authentication | |||
# request, invalidating associations if requested. | |||
def process_check_auth_response(response) | |||
is_valid = response.get_arg(OPENID_NS, 'is_valid', 'false') | |||
invalidate_handle = response.get_arg(OPENID_NS, 'invalidate_handle') | |||
if !invalidate_handle.nil? | |||
Util.log("Received 'invalidate_handle' from server #{server_url}") | |||
if @store.nil? | |||
Util.log('Unexpectedly got "invalidate_handle" without a store!') | |||
else | |||
@store.remove_association(server_url, invalidate_handle) | |||
end | |||
end | |||
if is_valid != 'true' | |||
raise ProtocolError, ("Server #{server_url} responds that the "\ | |||
"'check_authentication' call is not valid") | |||
end | |||
end | |||
def check_nonce | |||
case openid_namespace | |||
when OPENID1_NS | |||
nonce = | |||
@message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name) | |||
# We generated the nonce, so it uses the empty string as the | |||
# server URL | |||
server_url = '' | |||
when OPENID2_NS | |||
nonce = @message.get_arg(OPENID2_NS, 'response_nonce') | |||
server_url = self.server_url | |||
else | |||
raise StandardError, 'Not reached' | |||
end | |||
if nonce.nil? | |||
raise ProtocolError, 'Nonce missing from response' | |||
end | |||
begin | |||
time, extra = Nonce.split_nonce(nonce) | |||
rescue ArgumentError => why | |||
raise ProtocolError, "Malformed nonce: #{nonce.inspect}" | |||
end | |||
if !@store.nil? && !@store.use_nonce(server_url, time, extra) | |||
raise ProtocolError, ("Nonce already used or out of range: "\ | |||
"#{nonce.inspect}") | |||
end | |||
end | |||
def verify_discovery_results | |||
begin | |||
case openid_namespace | |||
when OPENID1_NS | |||
verify_discovery_results_openid1 | |||
when OPENID2_NS | |||
verify_discovery_results_openid2 | |||
else | |||
raise StandardError, "Not reached: #{openid_namespace}" | |||
end | |||
rescue Message::KeyNotFound => why | |||
raise ProtocolError, "Missing required field: #{why.message}" | |||
end | |||
end | |||
def verify_discovery_results_openid2 | |||
to_match = OpenIDServiceEndpoint.new | |||
to_match.type_uris = [OPENID_2_0_TYPE] | |||
to_match.claimed_id = fetch('claimed_id', nil) | |||
to_match.local_id = fetch('identity', nil) | |||
to_match.server_url = fetch('op_endpoint') | |||
if to_match.claimed_id.nil? && !to_match.local_id.nil? | |||
raise ProtocolError, ('openid.identity is present without '\ | |||
'openid.claimed_id') | |||
elsif !to_match.claimed_id.nil? && to_match.local_id.nil? | |||
raise ProtocolError, ('openid.claimed_id is present without '\ | |||
'openid.identity') | |||
# This is a response without identifiers, so there's really no | |||
# checking that we can do, so return an endpoint that's for | |||
# the specified `openid.op_endpoint' | |||
elsif to_match.claimed_id.nil? | |||
@endpoint = | |||
OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url) | |||
return | |||
end | |||
if @endpoint.nil? | |||
Util.log('No pre-discovered information supplied') | |||
discover_and_verify(to_match.claimed_id, [to_match]) | |||
else | |||
begin | |||
verify_discovery_single(@endpoint, to_match) | |||
rescue ProtocolError => why | |||
Util.log("Error attempting to use stored discovery "\ | |||
"information: #{why.message}") | |||
Util.log("Attempting discovery to verify endpoint") | |||
discover_and_verify(to_match.claimed_id, [to_match]) | |||
end | |||
end | |||
if @endpoint.claimed_id != to_match.claimed_id | |||
@endpoint = @endpoint.dup | |||
@endpoint.claimed_id = to_match.claimed_id | |||
end | |||
end | |||
def verify_discovery_results_openid1 | |||
claimed_id = | |||
@message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name) | |||
if claimed_id.nil? | |||
if @endpoint.nil? | |||
raise ProtocolError, ("When using OpenID 1, the claimed ID must "\ | |||
"be supplied, either by passing it through "\ | |||
"as a return_to parameter or by using a "\ | |||
"session, and supplied to the IdResHandler "\ | |||
"when it is constructed.") | |||
else | |||
claimed_id = @endpoint.claimed_id | |||
end | |||
end | |||
to_match = OpenIDServiceEndpoint.new | |||
to_match.type_uris = [OPENID_1_1_TYPE] | |||
to_match.local_id = fetch('identity') | |||
# Restore delegate information from the initiation phase | |||
to_match.claimed_id = claimed_id | |||
to_match_1_0 = to_match.dup | |||
to_match_1_0.type_uris = [OPENID_1_0_TYPE] | |||
if !@endpoint.nil? | |||
begin | |||
begin | |||
verify_discovery_single(@endpoint, to_match) | |||
rescue TypeURIMismatch | |||
verify_discovery_single(@endpoint, to_match_1_0) | |||
end | |||
rescue ProtocolError => why | |||
Util.log('Error attempting to use stored discovery information: ' + | |||
why.message) | |||
Util.log('Attempting discovery to verify endpoint') | |||
else | |||
return @endpoint | |||
end | |||
end | |||
# Either no endpoint was supplied or OpenID 1.x verification | |||
# of the information that's in the message failed on that | |||
# endpoint. | |||
discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0]) | |||
end | |||
# Given an endpoint object created from the information in an | |||
# OpenID response, perform discovery and verify the discovery | |||
# results, returning the matching endpoint that is the result of | |||
# doing that discovery. | |||
def discover_and_verify(claimed_id, to_match_endpoints) | |||
Util.log("Performing discovery on #{claimed_id}") | |||
_, services = OpenID.discover(claimed_id) | |||
if services.length == 0 | |||
# XXX: this might want to be something other than | |||
# ProtocolError. In Python, it's DiscoveryFailure | |||
raise ProtocolError, ("No OpenID information found at "\ | |||
"#{claimed_id}") | |||
end | |||
verify_discovered_services(claimed_id, services, to_match_endpoints) | |||
end | |||
def verify_discovered_services(claimed_id, services, to_match_endpoints) | |||
# Search the services resulting from discovery to find one | |||
# that matches the information from the assertion | |||
failure_messages = [] | |||
for endpoint in services | |||
for to_match_endpoint in to_match_endpoints | |||
begin | |||
verify_discovery_single(endpoint, to_match_endpoint) | |||
rescue ProtocolError => why | |||
failure_messages << why.message | |||
else | |||
# It matches, so discover verification has | |||
# succeeded. Return this endpoint. | |||
@endpoint = endpoint | |||
return | |||
end | |||
end | |||
end | |||
Util.log("Discovery verification failure for #{claimed_id}") | |||
failure_messages.each do |failure_message| | |||
Util.log(" * Endpoint mismatch: " + failure_message) | |||
end | |||
# XXX: is DiscoveryFailure in Python OpenID | |||
raise ProtocolError, ("No matching endpoint found after "\ | |||
"discovering #{claimed_id}") | |||
end | |||
def verify_discovery_single(endpoint, to_match) | |||
# Every type URI that's in the to_match endpoint has to be | |||
# present in the discovered endpoint. | |||
for type_uri in to_match.type_uris | |||
if !endpoint.uses_extension(type_uri) | |||
raise TypeURIMismatch.new(type_uri, endpoint) | |||
end | |||
end | |||
# Fragments do not influence discovery, so we can't compare a | |||
# claimed identifier with a fragment to discovered information. | |||
defragged_claimed_id = | |||
case Yadis::XRI.identifier_scheme(endpoint.claimed_id) | |||
when :xri | |||
endpoint.claimed_id | |||
when :uri | |||
begin | |||
parsed = URI.parse(endpoint.claimed_id) | |||
rescue URI::InvalidURIError | |||
endpoint.claimed_id | |||
else | |||
parsed.fragment = nil | |||
parsed.to_s | |||
end | |||
else | |||
raise StandardError, 'Not reached' | |||
end | |||
if defragged_claimed_id != endpoint.claimed_id | |||
raise ProtocolError, ("Claimed ID does not match (different "\ | |||
"subjects!), Expected "\ | |||
"#{defragged_claimed_id}, got "\ | |||
"#{endpoint.claimed_id}") | |||
end | |||
if to_match.get_local_id != endpoint.get_local_id | |||
raise ProtocolError, ("local_id mismatch. Expected "\ | |||
"#{to_match.get_local_id}, got "\ | |||
"#{endpoint.get_local_id}") | |||
end | |||
# If the server URL is nil, this must be an OpenID 1 | |||
# response, because op_endpoint is a required parameter in | |||
# OpenID 2. In that case, we don't actually care what the | |||
# discovered server_url is, because signature checking or | |||
# check_auth should take care of that check for us. | |||
if to_match.server_url.nil? | |||
if to_match.preferred_namespace != OPENID1_NS | |||
raise StandardError, | |||
"The code calling this must ensure that OpenID 2 "\ | |||
"responses have a non-none `openid.op_endpoint' and "\ | |||
"that it is set as the `server_url' attribute of the "\ | |||
"`to_match' endpoint." | |||
end | |||
elsif to_match.server_url != endpoint.server_url | |||
raise ProtocolError, ("OP Endpoint mismatch. Expected"\ | |||
"#{to_match.server_url}, got "\ | |||
"#{endpoint.server_url}") | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -1,148 +0,0 @@ | |||
module OpenID | |||
class Consumer | |||
# Code returned when either the of the | |||
# OpenID::OpenIDConsumer.begin_auth or OpenID::OpenIDConsumer.complete_auth | |||
# methods return successfully. | |||
SUCCESS = :success | |||
# Code OpenID::OpenIDConsumer.complete_auth | |||
# returns when the value it received indicated an invalid login. | |||
FAILURE = :failure | |||
# Code returned by OpenIDConsumer.complete_auth when the user | |||
# cancels the operation from the server. | |||
CANCEL = :cancel | |||
# Code returned by OpenID::OpenIDConsumer.complete_auth when the | |||
# OpenIDConsumer instance is in immediate mode and ther server sends back a | |||
# URL for the user to login with. | |||
SETUP_NEEDED = :setup_needed | |||
module Response | |||
attr_reader :endpoint | |||
def status | |||
self.class::STATUS | |||
end | |||
# The identity URL that has been authenticated; the Claimed Identifier. | |||
# See also display_identifier. | |||
def identity_url | |||
@endpoint ? @endpoint.claimed_id : nil | |||
end | |||
# The display identifier is related to the Claimed Identifier, but the | |||
# two are not always identical. The display identifier is something the | |||
# user should recognize as what they entered, whereas the response's | |||
# claimed identifier (in the identity_url attribute) may have extra | |||
# information for better persistence. | |||
# | |||
# URLs will be stripped of their fragments for display. XRIs will | |||
# display the human-readable identifier (i-name) instead of the | |||
# persistent identifier (i-number). | |||
# | |||
# Use the display identifier in your user interface. Use identity_url | |||
# for querying your database or authorization server, or other | |||
# identifier equality comparisons. | |||
def display_identifier | |||
@endpoint ? @endpoint.display_identifier : nil | |||
end | |||
end | |||
# A successful acknowledgement from the OpenID server that the | |||
# supplied URL is, indeed controlled by the requesting agent. | |||
class SuccessResponse | |||
include Response | |||
STATUS = SUCCESS | |||
attr_reader :message, :signed_fields | |||
def initialize(endpoint, message, signed_fields) | |||
# Don't use :endpoint=, because endpoint should never be nil | |||
# for a successfull transaction. | |||
@endpoint = endpoint | |||
@identity_url = endpoint.claimed_id | |||
@message = message | |||
@signed_fields = signed_fields | |||
end | |||
# Was this authentication response an OpenID 1 authentication | |||
# response? | |||
def is_openid1 | |||
@message.is_openid1 | |||
end | |||
# Return whether a particular key is signed, regardless of its | |||
# namespace alias | |||
def signed?(ns_uri, ns_key) | |||
@signed_fields.member?(@message.get_key(ns_uri, ns_key)) | |||
end | |||
# Return the specified signed field if available, otherwise | |||
# return default | |||
def get_signed(ns_uri, ns_key, default=nil) | |||
if singed?(ns_uri, ns_key) | |||
return @message.get_arg(ns_uri, ns_key, default) | |||
else | |||
return default | |||
end | |||
end | |||
# Get signed arguments from the response message. Return a dict | |||
# of all arguments in the specified namespace. If any of the | |||
# arguments are not signed, return nil. | |||
def get_signed_ns(ns_uri) | |||
msg_args = @message.get_args(ns_uri) | |||
msg_args.each_key do |key| | |||
if !signed?(ns_uri, key) | |||
return nil | |||
end | |||
end | |||
return msg_args | |||
end | |||
# Return response arguments in the specified namespace. | |||
# If require_signed is true and the arguments are not signed, | |||
# return nil. | |||
def extension_response(namespace_uri, require_signed) | |||
if require_signed | |||
get_signed_ns(namespace_uri) | |||
else | |||
@message.get_args(namespace_uri) | |||
end | |||
end | |||
end | |||
class FailureResponse | |||
include Response | |||
STATUS = FAILURE | |||
attr_reader :message, :contact, :reference | |||
def initialize(endpoint, message, contact=nil, reference=nil) | |||
@endpoint = endpoint | |||
@message = message | |||
@contact = contact | |||
@reference = reference | |||
end | |||
end | |||
class CancelResponse | |||
include Response | |||
STATUS = CANCEL | |||
def initialize(endpoint) | |||
@endpoint = endpoint | |||
end | |||
end | |||
class SetupNeededResponse | |||
include Response | |||
STATUS = SETUP_NEEDED | |||
def initialize(endpoint, setup_url) | |||
@endpoint = endpoint | |||
@setup_url = setup_url | |||
end | |||
end | |||
end | |||
end |
@@ -1,97 +0,0 @@ | |||
require "openid/util" | |||
require "digest/sha1" | |||
require "digest/sha2" | |||
begin | |||
require "digest/hmac" | |||
rescue LoadError | |||
require "hmac/sha1" | |||
require "hmac/sha2" | |||
end | |||
module OpenID | |||
# This module contains everything needed to perform low-level | |||
# cryptograph and data manipulation tasks. | |||
module CryptUtil | |||
# Generate a random number, doing a little extra work to make it | |||
# more likely that it's suitable for cryptography. If your system | |||
# doesn't have /dev/urandom then this number is not | |||
# cryptographically safe. See | |||
# <http://www.cosine.org/2007/08/07/security-ruby-kernel-rand/> | |||
# for more information. max is the largest possible value of such | |||
# a random number, where the result will be less than max. | |||
def CryptUtil.rand(max) | |||
Kernel.srand() | |||
return Kernel.rand(max) | |||
end | |||
def CryptUtil.sha1(text) | |||
return Digest::SHA1.digest(text) | |||
end | |||
def CryptUtil.hmac_sha1(key, text) | |||
if Digest.const_defined? :HMAC | |||
Digest::HMAC.new(key,Digest::SHA1).update(text).digest | |||
else | |||
return HMAC::SHA1.digest(key, text) | |||
end | |||
end | |||
def CryptUtil.sha256(text) | |||
return Digest::SHA256.digest(text) | |||
end | |||
def CryptUtil.hmac_sha256(key, text) | |||
if Digest.const_defined? :HMAC | |||
Digest::HMAC.new(key,Digest::SHA256).update(text).digest | |||
else | |||
return HMAC::SHA256.digest(key, text) | |||
end | |||
end | |||
# Generate a random string of the given length, composed of the | |||
# specified characters. If chars is nil, generate a string | |||
# composed of characters in the range 0..255. | |||
def CryptUtil.random_string(length, chars=nil) | |||
s = "" | |||
unless chars.nil? | |||
length.times { s << chars[rand(chars.length)] } | |||
else | |||
length.times { s << rand(256).chr } | |||
end | |||
return s | |||
end | |||
# Convert a number to its binary representation; return a string | |||
# of bytes. | |||
def CryptUtil.num_to_binary(n) | |||
bits = n.to_s(2) | |||
prepend = (8 - bits.length % 8) | |||
bits = ('0' * prepend) + bits | |||
return [bits].pack('B*') | |||
end | |||
# Convert a string of bytes into a number. | |||
def CryptUtil.binary_to_num(s) | |||
# taken from openid-ruby 0.0.1 | |||
s = "\000" * (4 - (s.length % 4)) + s | |||
num = 0 | |||
s.unpack('N*').each do |x| | |||
num <<= 32 | |||
num |= x | |||
end | |||
return num | |||
end | |||
# Encode a number as a base64-encoded byte string. | |||
def CryptUtil.num_to_base64(l) | |||
return OpenID::Util.to_base64(num_to_binary(l)) | |||
end | |||
# Decode a base64 byte string to a number. | |||
def CryptUtil.base64_to_num(s) | |||
return binary_to_num(OpenID::Util.from_base64(s)) | |||
end | |||
end | |||
end |
@@ -1,89 +0,0 @@ | |||
require "openid/util" | |||
require "openid/cryptutil" | |||
module OpenID | |||
# Encapsulates a Diffie-Hellman key exchange. This class is used | |||
# internally by both the consumer and server objects. | |||
# | |||
# Read more about Diffie-Hellman on wikipedia: | |||
# http://en.wikipedia.org/wiki/Diffie-Hellman | |||
class DiffieHellman | |||
# From the OpenID specification | |||
@@default_mod = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443 | |||
@@default_gen = 2 | |||
attr_reader :modulus, :generator, :public | |||
# A new DiffieHellman object, using the modulus and generator from | |||
# the OpenID specification | |||
def DiffieHellman.from_defaults | |||
DiffieHellman.new(@@default_mod, @@default_gen) | |||
end | |||
def initialize(modulus=nil, generator=nil, priv=nil) | |||
@modulus = modulus.nil? ? @@default_mod : modulus | |||
@generator = generator.nil? ? @@default_gen : generator | |||
set_private(priv.nil? ? OpenID::CryptUtil.rand(@modulus-2) + 1 : priv) | |||
end | |||
def get_shared_secret(composite) | |||
DiffieHellman.powermod(composite, @private, @modulus) | |||
end | |||
def xor_secret(algorithm, composite, secret) | |||
dh_shared = get_shared_secret(composite) | |||
packed_dh_shared = OpenID::CryptUtil.num_to_binary(dh_shared) | |||
hashed_dh_shared = algorithm.call(packed_dh_shared) | |||
return DiffieHellman.strxor(secret, hashed_dh_shared) | |||
end | |||
def using_default_values? | |||
@generator == @@default_gen && @modulus == @@default_mod | |||
end | |||
private | |||
def set_private(priv) | |||
@private = priv | |||
@public = DiffieHellman.powermod(@generator, @private, @modulus) | |||
end | |||
def DiffieHellman.strxor(s, t) | |||
if s.length != t.length | |||
raise ArgumentError, "strxor: lengths don't match. " + | |||
"Inputs were #{s.inspect} and #{t.inspect}" | |||
end | |||
if String.method_defined? :bytes | |||
s.bytes.zip(t.bytes).map{|sb,tb| sb^tb}.pack('C*') | |||
else | |||
indices = 0...(s.length) | |||
chrs = indices.collect {|i| (s[i]^t[i]).chr} | |||
chrs.join("") | |||
end | |||
end | |||
# This code is taken from this post: | |||
# <http://blade.nagaokaut.ac.jp/cgi-bin/scat.\rb/ruby/ruby-talk/19098> | |||
# by Eric Lee Green. | |||
def DiffieHellman.powermod(x, n, q) | |||
counter=0 | |||
n_p=n | |||
y_p=1 | |||
z_p=x | |||
while n_p != 0 | |||
if n_p[0]==1 | |||
y_p=(y_p*z_p) % q | |||
end | |||
n_p = n_p >> 1 | |||
z_p = (z_p * z_p) % q | |||
counter += 1 | |||
end | |||
return y_p | |||
end | |||
end | |||
end |
@@ -1,39 +0,0 @@ | |||
require 'openid/message' | |||
module OpenID | |||
# An interface for OpenID extensions. | |||
class Extension < Object | |||
def initialize | |||
@ns_uri = nil | |||
@ns_alias = nil | |||
end | |||
# Get the string arguments that should be added to an OpenID | |||
# message for this extension. | |||
def get_extension_args | |||
raise NotImplementedError | |||
end | |||
# Add the arguments from this extension to the provided | |||
# message, or create a new message containing only those | |||
# arguments. Returns the message with added extension args. | |||
def to_message(message = nil) | |||
if message.nil? | |||
# warnings.warn('Passing None to Extension.toMessage is deprecated. ' | |||
# 'Creating a message assuming you want OpenID 2.', | |||
# DeprecationWarning, stacklevel=2) | |||
Message.new(OPENID2_NS) | |||
end | |||
message = Message.new if message.nil? | |||
implicit = message.is_openid1() | |||
message.namespaces.add_alias(@ns_uri, @ns_alias, implicit) | |||
# XXX python ignores keyerror if m.ns.getAlias(uri) == alias | |||
message.update_args(@ns_uri, get_extension_args) | |||
return message | |||
end | |||
end | |||
end |
@@ -1,516 +0,0 @@ | |||
# Implements the OpenID attribute exchange specification, version 1.0 | |||
require 'openid/extension' | |||
require 'openid/trustroot' | |||
require 'openid/message' | |||
module OpenID | |||
module AX | |||
UNLIMITED_VALUES = "unlimited" | |||
MINIMUM_SUPPORTED_ALIAS_LENGTH = 32 | |||
# check alias for invalid characters, raise AXError if found | |||
def self.check_alias(name) | |||
if name.match(/(,|\.)/) | |||
raise Error, ("Alias #{name.inspect} must not contain a "\ | |||
"comma or period.") | |||
end | |||
end | |||
# Raised when data does not comply with AX 1.0 specification | |||
class Error < ArgumentError | |||
end | |||
# Abstract class containing common code for attribute exchange messages | |||
class AXMessage < Extension | |||
attr_accessor :ns_alias, :mode, :ns_uri | |||
NS_URI = 'http://openid.net/srv/ax/1.0' | |||
def initialize | |||
@ns_alias = 'ax' | |||
@ns_uri = NS_URI | |||
@mode = nil | |||
end | |||
protected | |||
# 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'] | |||
if actual_mode != @mode | |||
raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}" | |||
end | |||
end | |||
def new_args | |||
{'mode' => @mode} | |||
end | |||
end | |||
# Represents a single attribute in an attribute exchange | |||
# request. This should be added to an Request object in order to | |||
# request the attribute. | |||
# | |||
# @ivar required: Whether the attribute will be marked as required | |||
# when presented to the subject of the attribute exchange | |||
# request. | |||
# @type required: bool | |||
# | |||
# @ivar count: How many values of this type to request from the | |||
# subject. Defaults to one. | |||
# @type count: int | |||
# | |||
# @ivar type_uri: The identifier that determines what the attribute | |||
# represents and how it is serialized. For example, one type URI | |||
# representing dates could represent a Unix timestamp in base 10 | |||
# and another could represent a human-readable string. | |||
# @type type_uri: str | |||
# | |||
# @ivar ns_alias: The name that should be given to this alias in the | |||
# request. If it is not supplied, a generic name will be | |||
# assigned. For example, if you want to call a Unix timestamp | |||
# value 'tstamp', set its alias to that value. If two attributes | |||
# in the same message request to use the same alias, the request | |||
# will fail to be generated. | |||
# @type alias: str or NoneType | |||
class AttrInfo < Object | |||
attr_reader :type_uri, :count, :ns_alias | |||
attr_accessor :required | |||
def initialize(type_uri, ns_alias=nil, required=false, count=1) | |||
@type_uri = type_uri | |||
@count = count | |||
@required = required | |||
@ns_alias = ns_alias | |||
end | |||
def wants_unlimited_values? | |||
@count == UNLIMITED_VALUES | |||
end | |||
end | |||
# Given a namespace mapping and a string containing a | |||
# comma-separated list of namespace aliases, return a list of type | |||
# URIs that correspond to those aliases. | |||
# namespace_map: OpenID::NamespaceMap | |||
def self.to_type_uris(namespace_map, alias_list_s) | |||
return [] if alias_list_s.nil? | |||
alias_list_s.split(',').inject([]) {|uris, name| | |||
type_uri = namespace_map.get_namespace_uri(name) | |||
raise IndexError, "No type defined for attribute name #{name.inspect}" if type_uri.nil? | |||
uris << type_uri | |||
} | |||
end | |||
# An attribute exchange 'fetch_request' message. This message is | |||
# sent by a relying party when it wishes to obtain attributes about | |||
# the subject of an OpenID authentication request. | |||
class FetchRequest < AXMessage | |||
attr_reader :requested_attributes | |||
attr_accessor :update_url | |||
def initialize(update_url = nil) | |||
super() | |||
@mode = 'fetch_request' | |||
@requested_attributes = {} | |||
@update_url = update_url | |||
end | |||
# Add an attribute to this attribute exchange request. | |||
# attribute: AttrInfo, the attribute being requested | |||
# Raises IndexError if the requested attribute is already present | |||
# in this request. | |||
def add(attribute) | |||
if @requested_attributes[attribute.type_uri] | |||
raise IndexError, "The attribute #{attribute.type_uri} has already been requested" | |||
end | |||
@requested_attributes[attribute.type_uri] = attribute | |||
end | |||
# Get the serialized form of this attribute fetch request. | |||
# returns a hash of the arguments | |||
def get_extension_args | |||
aliases = NamespaceMap.new | |||
required = [] | |||
if_available = [] | |||
ax_args = new_args | |||
@requested_attributes.each{|type_uri, attribute| | |||
if attribute.ns_alias | |||
name = aliases.add_alias(type_uri, attribute.ns_alias) | |||
else | |||
name = aliases.add(type_uri) | |||
end | |||
if attribute.required | |||
required << name | |||
else | |||
if_available << name | |||
end | |||
if attribute.count != 1 | |||
ax_args["count.#{name}"] = attribute.count.to_s | |||
end | |||
ax_args["type.#{name}"] = type_uri | |||
} | |||
unless required.empty? | |||
ax_args['required'] = required.join(',') | |||
end | |||
unless if_available.empty? | |||
ax_args['if_available'] = if_available.join(',') | |||
end | |||
return ax_args | |||
end | |||
# Get the type URIs for all attributes that have been marked | |||
# as required. | |||
def get_required_attrs | |||
@requested_attributes.inject([]) {|required, (type_uri, attribute)| | |||
if attribute.required | |||
required << type_uri | |||
else | |||
required | |||
end | |||
} | |||
end | |||
# Extract a FetchRequest from an OpenID message | |||
# message: OpenID::Message | |||
# return a FetchRequest or nil if AX arguments are not present | |||
def self.from_openid_request(oidreq) | |||
message = oidreq.message | |||
ax_args = message.get_args(NS_URI) | |||
return nil if ax_args == {} | |||
req = new | |||
req.parse_extension_args(ax_args) | |||
if req.update_url | |||
realm = message.get_arg(OPENID_NS, 'realm', | |||
message.get_arg(OPENID_NS, 'return_to')) | |||
if realm.nil? or realm.empty? | |||
raise Error, "Cannot validate update_url #{req.update_url.inspect} against absent realm" | |||
end | |||
tr = TrustRoot::TrustRoot.parse(realm) | |||
unless tr.validate_url(req.update_url) | |||
raise Error, "Update URL #{req.update_url.inspect} failed validation against realm #{realm.inspect}" | |||
end | |||
end | |||
return req | |||
end | |||
def parse_extension_args(ax_args) | |||
check_mode(ax_args) | |||
aliases = NamespaceMap.new | |||
ax_args.each{|k,v| | |||
if k.index('type.') == 0 | |||
name = k[5..-1] | |||
type_uri = v | |||
aliases.add_alias(type_uri, name) | |||
count_key = 'count.'+name | |||
count_s = ax_args[count_key] | |||
count = 1 | |||
if count_s | |||
if count_s == UNLIMITED_VALUES | |||
count = count_s | |||
else | |||
count = count_s.to_i | |||
if count <= 0 | |||
raise Error, "Invalid value for count #{count_key.inspect}: #{count_s.inspect}" | |||
end | |||
end | |||
end | |||
add(AttrInfo.new(type_uri, name, false, count)) | |||
end | |||
} | |||
required = AX.to_type_uris(aliases, ax_args['required']) | |||
required.each{|type_uri| | |||
@requested_attributes[type_uri].required = true | |||
} | |||
if_available = AX.to_type_uris(aliases, ax_args['if_available']) | |||
all_type_uris = required + if_available | |||
aliases.namespace_uris.each{|type_uri| | |||
unless all_type_uris.member? type_uri | |||
raise Error, "Type URI #{type_uri.inspect} was in the request but not present in 'required' or 'if_available'" | |||
end | |||
} | |||
@update_url = ax_args['update_url'] | |||
end | |||
# return the list of AttrInfo objects contained in the FetchRequest | |||
def attributes | |||
@requested_attributes.values | |||
end | |||
# return the list of requested attribute type URIs | |||
def requested_types | |||
@requested_attributes.keys | |||
end | |||
def member?(type_uri) | |||
! @requested_attributes[type_uri].nil? | |||
end | |||
end | |||
# Abstract class that implements a message that has attribute | |||
# keys and values. It contains the common code between | |||
# fetch_response and store_request. | |||
class KeyValueMessage < AXMessage | |||
attr_reader :data | |||
def initialize | |||
super() | |||
@mode = nil | |||
@data = {} | |||
@data.default = [] | |||
end | |||
# Add a single value for the given attribute type to the | |||
# message. If there are already values specified for this type, | |||
# this value will be sent in addition to the values already | |||
# specified. | |||
def add_value(type_uri, value) | |||
@data[type_uri] = @data[type_uri] << value | |||
end | |||
# Set the values for the given attribute type. This replaces | |||
# any values that have already been set for this attribute. | |||
def set_values(type_uri, values) | |||
@data[type_uri] = values | |||
end | |||
# Get the extension arguments for the key/value pairs | |||
# contained in this message. | |||
def _get_extension_kv_args(aliases = nil) | |||
aliases = NamespaceMap.new if aliases.nil? | |||
ax_args = new_args | |||
@data.each{|type_uri, values| | |||
name = aliases.add(type_uri) | |||
ax_args['type.'+name] = type_uri | |||
ax_args['count.'+name] = values.size.to_s | |||
values.each_with_index{|value, i| | |||
key = "value.#{name}.#{i+1}" | |||
ax_args[key] = value | |||
} | |||
} | |||
return ax_args | |||
end | |||
# Parse attribute exchange key/value arguments into this object. | |||
def parse_extension_args(ax_args) | |||
check_mode(ax_args) | |||
aliases = NamespaceMap.new | |||
ax_args.each{|k, v| | |||
if k.index('type.') == 0 | |||
type_uri = v | |||
name = k[5..-1] | |||
AX.check_alias(name) | |||
aliases.add_alias(type_uri,name) | |||
end | |||
} | |||
aliases.each{|type_uri, name| | |||
count_s = ax_args['count.'+name] | |||
count = count_s.to_i | |||
if count_s.nil? | |||
value = ax_args['value.'+name] | |||
if value.nil? | |||
raise IndexError, "Missing #{'value.'+name} in FetchResponse" | |||
elsif value.empty? | |||
values = [] | |||
else | |||
values = [value] | |||
end | |||
elsif count_s.to_i == 0 | |||
values = [] | |||
else | |||
values = (1..count).inject([]){|l,i| | |||
key = "value.#{name}.#{i}" | |||
v = ax_args[key] | |||
raise IndexError, "Missing #{key} in FetchResponse" if v.nil? | |||
l << v | |||
} | |||
end | |||
@data[type_uri] = values | |||
} | |||
end | |||
# Get a single value for an attribute. If no value was sent | |||
# for this attribute, use the supplied default. If there is more | |||
# than one value for this attribute, this method will fail. | |||
def get_single(type_uri, default = nil) | |||
values = @data[type_uri] | |||
return default if values.empty? | |||
if values.size != 1 | |||
raise Error, "More than one value present for #{type_uri.inspect}" | |||
else | |||
return values[0] | |||
end | |||
end | |||
# retrieve the list of values for this attribute | |||
def get(type_uri) | |||
@data[type_uri] | |||
end | |||
# retrieve the list of values for this attribute | |||
def [](type_uri) | |||
@data[type_uri] | |||
end | |||
# get the number of responses for this attribute | |||
def count(type_uri) | |||
@data[type_uri].size | |||
end | |||
end | |||
# A fetch_response attribute exchange message | |||
class FetchResponse < KeyValueMessage | |||
attr_reader :update_url | |||
def initialize(update_url = nil) | |||
super() | |||
@mode = 'fetch_response' | |||
@update_url = update_url | |||
end | |||
# Serialize this object into arguments in the attribute | |||
# exchange namespace | |||
# Takes an optional FetchRequest. If specified, the response will be | |||
# 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 | |||
# Validate the data in the context of the request (the | |||
# same attributes should be present in each, and the | |||
# counts in the response must be no more than the counts | |||
# in the request) | |||
@data.keys.each{|type_uri| | |||
unless request.member? type_uri | |||
raise IndexError, "Response attribute not present in request: #{type_uri.inspect}" | |||
end | |||
} | |||
request.attributes.each{|attr_info| | |||
# 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) | |||
else | |||
aliases.add_alias(attr_info.type_uri, attr_info.ns_alias) | |||
end | |||
values = @data[attr_info.type_uri] | |||
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 | |||
} | |||
end | |||
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) | |||
kv_args['type.' + name] = attr_info.type_uri | |||
kv_args['count.' + name] = '0' | |||
} | |||
update_url = (request and request.update_url or @update_url) | |||
ax_args['update_url'] = update_url unless update_url.nil? | |||
ax_args.update(kv_args) | |||
return ax_args | |||
end | |||
def parse_extension_args(ax_args) | |||
super | |||
@update_url = ax_args['update_url'] | |||
end | |||
# Construct a FetchResponse object from an OpenID library | |||
# SuccessResponse object. | |||
def self.from_success_response(success_response, signed=true) | |||
obj = self.new | |||
if signed | |||
ax_args = success_response.get_signed_ns(obj.ns_uri) | |||
else | |||
ax_args = success_response.message.get_args(obj.ns_uri) | |||
end | |||
begin | |||
obj.parse_extension_args(ax_args) | |||
return obj | |||
rescue Error => e | |||
return nil | |||
end | |||
end | |||
end | |||
# A store request attribute exchange message representation | |||
class StoreRequest < KeyValueMessage | |||
def initialize | |||
super | |||
@mode = 'store_request' | |||
end | |||
def get_extension_args(aliases=nil) | |||
ax_args = new_args | |||
kv_args = _get_extension_kv_args(aliases) | |||
ax_args.update(kv_args) | |||
return ax_args | |||
end | |||
end | |||
# An indication that the store request was processed along with | |||
# this OpenID transaction. | |||
class StoreResponse < AXMessage | |||
SUCCESS_MODE = 'store_response_success' | |||
FAILURE_MODE = 'store_response_failure' | |||
attr_reader :error_message | |||
def initialize(succeeded = true, error_message = nil) | |||
super() | |||
if succeeded and error_message | |||
raise Error, "Error message included in a success response" | |||
end | |||
if succeeded | |||
@mode = SUCCESS_MODE | |||
else | |||
@mode = FAILURE_MODE | |||
end | |||
@error_message = error_message | |||
end | |||
def succeeded? | |||
@mode == SUCCESS_MODE | |||
end | |||
def get_extension_args | |||
ax_args = new_args | |||
if !succeeded? and error_message | |||
ax_args['error'] = @error_message | |||
end | |||
return ax_args | |||
end | |||
end | |||
end | |||
end |
@@ -1,179 +0,0 @@ | |||
# An implementation of the OpenID Provider Authentication Policy | |||
# Extension 1.0 | |||
# see: http://openid.net/specs/ | |||
require 'openid/extension' | |||
module OpenID | |||
module PAPE | |||
NS_URI = "http://specs.openid.net/extensions/pape/1.0" | |||
AUTH_MULTI_FACTOR_PHYSICAL = | |||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical' | |||
AUTH_MULTI_FACTOR = | |||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor' | |||
AUTH_PHISHING_RESISTANT = | |||
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant' | |||
TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/ | |||
# A Provider Authentication Policy request, sent from a relying | |||
# party to a provider | |||
class Request < Extension | |||
attr_accessor :preferred_auth_policies, :max_auth_age, :ns_alias, :ns_uri | |||
def initialize(preferred_auth_policies=[], max_auth_age=nil) | |||
@ns_alias = 'pape' | |||
@ns_uri = NS_URI | |||
@preferred_auth_policies = preferred_auth_policies | |||
@max_auth_age = max_auth_age | |||
end | |||
# Add an acceptable authentication policy URI to this request | |||
# This method is intended to be used by the relying party to add | |||
# acceptable authentication types to the request. | |||
def add_policy_uri(policy_uri) | |||
unless @preferred_auth_policies.member? policy_uri | |||
@preferred_auth_policies << policy_uri | |||
end | |||
end | |||
def get_extension_args | |||
ns_args = { | |||
'preferred_auth_policies' => @preferred_auth_policies.join(' ') | |||
} | |||
ns_args['max_auth_age'] = @max_auth_age.to_s if @max_auth_age | |||
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) | |||
pape_req = new | |||
args = oid_req.message.get_args(NS_URI) | |||
if args == {} | |||
return nil | |||
end | |||
pape_req.parse_extension_args(args) | |||
return pape_req | |||
end | |||
# Set the state of this request to be that expressed in these | |||
# PAPE arguments | |||
def parse_extension_args(args) | |||
@preferred_auth_policies = [] | |||
policies_str = args['preferred_auth_policies'] | |||
if policies_str | |||
policies_str.split(' ').each{|uri| | |||
add_policy_uri(uri) | |||
} | |||
end | |||
max_auth_age_str = args['max_auth_age'] | |||
if max_auth_age_str | |||
@max_auth_age = max_auth_age_str.to_i | |||
else | |||
@max_auth_age = nil | |||
end | |||
end | |||
# Given a list of authentication policy URIs that a provider | |||
# supports, this method returns the subset of those types | |||
# that are preferred by the relying party. | |||
def preferred_types(supported_types) | |||
@preferred_auth_policies.select{|uri| supported_types.member? uri} | |||
end | |||
end | |||
# A Provider Authentication Policy response, sent from a provider | |||
# to a relying party | |||
class Response < Extension | |||
attr_accessor :ns_alias, :auth_policies, :auth_time, :nist_auth_level | |||
def initialize(auth_policies=[], auth_time=nil, nist_auth_level=nil) | |||
@ns_alias = 'pape' | |||
@ns_uri = NS_URI | |||
@auth_policies = auth_policies | |||
@auth_time = auth_time | |||
@nist_auth_level = nist_auth_level | |||
end | |||
# Add a policy URI to the response | |||
# see http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies | |||
def add_policy_uri(policy_uri) | |||
@auth_policies << policy_uri unless @auth_policies.member?(policy_uri) | |||
end | |||
# Create a Response object from an OpenID::Consumer::SuccessResponse | |||
def self.from_success_response(success_response) | |||
args = success_response.get_signed_ns(NS_URI) | |||
return nil if args.nil? | |||
pape_resp = new | |||
pape_resp.parse_extension_args(args) | |||
return pape_resp | |||
end | |||
# parse the provider authentication policy arguments into the | |||
# internal state of this object | |||
# if strict is specified, raise an exception when bad data is | |||
# encountered | |||
def parse_extension_args(args, strict=false) | |||
policies_str = args['auth_policies'] | |||
if policies_str and policies_str != 'none' | |||
@auth_policies = policies_str.split(' ') | |||
end | |||
nist_level_str = args['nist_auth_level'] | |||
if nist_level_str | |||
# special handling of zero to handle to_i behavior | |||
if nist_level_str.strip == '0' | |||
nist_level = 0 | |||
else | |||
nist_level = nist_level_str.to_i | |||
# if it's zero here we have a bad value | |||
if nist_level == 0 | |||
nist_level = nil | |||
end | |||
end | |||
if nist_level and nist_level >= 0 and nist_level < 5 | |||
@nist_auth_level = nist_level | |||
elsif strict | |||
raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{nist_level_str.inspect}" | |||
end | |||
end | |||
auth_time_str = args['auth_time'] | |||
if auth_time_str | |||
# validate time string | |||
if auth_time_str =~ TIME_VALIDATOR | |||
@auth_time = auth_time_str | |||
elsif strict | |||
raise ArgumentError, "auth_time must be in RFC3339 format" | |||
end | |||
end | |||
end | |||
def get_extension_args | |||
ns_args = {} | |||
if @auth_policies.empty? | |||
ns_args['auth_policies'] = 'none' | |||
else | |||
ns_args['auth_policies'] = @auth_policies.join(' ') | |||
end | |||
if @nist_auth_level | |||
unless (0..4).member? @nist_auth_level | |||
raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{@nist_auth_level.inspect}" | |||
end | |||
ns_args['nist_auth_level'] = @nist_auth_level.to_s | |||
end | |||
if @auth_time | |||
unless @auth_time =~ TIME_VALIDATOR | |||
raise ArgumentError, "auth_time must be in RFC3339 format" | |||
end | |||
ns_args['auth_time'] = @auth_time | |||
end | |||
return ns_args | |||
end | |||
end | |||
end | |||
end |
@@ -1,277 +0,0 @@ | |||
require 'openid/extension' | |||
require 'openid/util' | |||
require 'openid/message' | |||
module OpenID | |||
module SReg | |||
DATA_FIELDS = { | |||
'fullname'=>'Full Name', | |||
'nickname'=>'Nickname', | |||
'dob'=>'Date of Birth', | |||
'email'=>'E-mail Address', | |||
'gender'=>'Gender', | |||
'postcode'=>'Postal Code', | |||
'country'=>'Country', | |||
'language'=>'Language', | |||
'timezone'=>'Time Zone', | |||
} | |||
NS_URI_1_0 = 'http://openid.net/sreg/1.0' | |||
NS_URI_1_1 = 'http://openid.net/extensions/sreg/1.1' | |||
NS_URI = NS_URI_1_1 | |||
begin | |||
Message.register_namespace_alias(NS_URI_1_1, 'sreg') | |||
rescue NamespaceAliasRegistrationError => e | |||
Util.log(e) | |||
end | |||
# raise ArgumentError if fieldname is not in the defined sreg fields | |||
def OpenID.check_sreg_field_name(fieldname) | |||
unless DATA_FIELDS.member? fieldname | |||
raise ArgumentError, "#{fieldname} is not a defined simple registration field" | |||
end | |||
end | |||
# Does the given endpoint advertise support for simple registration? | |||
def OpenID.supports_sreg?(endpoint) | |||
endpoint.uses_extension(NS_URI_1_1) || endpoint.uses_extension(NS_URI_1_0) | |||
end | |||
# Extract the simple registration namespace URI from the given | |||
# OpenID message. Handles OpenID 1 and 2, as well as both sreg | |||
# namespace URIs found in the wild, as well as missing namespace | |||
# definitions (for OpenID 1) | |||
def OpenID.get_sreg_ns(message) | |||
[NS_URI_1_1, NS_URI_1_0].each{|ns| | |||
if message.namespaces.get_alias(ns) | |||
return ns | |||
end | |||
} | |||
# try to add an alias, since we didn't find one | |||
ns = NS_URI_1_1 | |||
begin | |||
message.namespaces.add_alias(ns, 'sreg') | |||
rescue IndexError | |||
raise NamespaceError | |||
end | |||
return ns | |||
end | |||
# The simple registration namespace was not found and could not | |||
# be created using the expected name (there's another extension | |||
# using the name 'sreg') | |||
# | |||
# This is not <em>illegal</em>, for OpenID 2, although it probably | |||
# indicates a problem, since it's not expected that other extensions | |||
# will re-use the alias that is in use for OpenID 1. | |||
# | |||
# If this is an OpenID 1 request, then there is no recourse. This | |||
# should not happen unless some code has modified the namespaces for | |||
# the message that is being processed. | |||
class NamespaceError < ArgumentError | |||
end | |||
# An object to hold the state of a simple registration request. | |||
class Request < Extension | |||
attr_reader :optional, :required, :ns_uri | |||
attr_accessor :policy_url | |||
def initialize(required = nil, optional = nil, policy_url = nil, ns_uri = NS_URI) | |||
super() | |||
@policy_url = policy_url | |||
@ns_uri = ns_uri | |||
@ns_alias = 'sreg' | |||
@required = [] | |||
@optional = [] | |||
if required | |||
request_fields(required, true, true) | |||
end | |||
if optional | |||
request_fields(optional, false, true) | |||
end | |||
end | |||
# Create a simple registration request that contains the | |||
# fields that were requested in the OpenID request with the | |||
# given arguments | |||
# Takes an OpenID::CheckIDRequest, returns an OpenID::Sreg::Request | |||
# return nil if the extension was not requested. | |||
def self.from_openid_request(request) | |||
# Since we're going to mess with namespace URI mapping, don't | |||
# mutate the object that was passed in. | |||
message = request.message.copy | |||
ns_uri = OpenID::get_sreg_ns(message) | |||
args = message.get_args(ns_uri) | |||
return nil if args == {} | |||
req = new(nil,nil,nil,ns_uri) | |||
req.parse_extension_args(args) | |||
return req | |||
end | |||
# Parse the unqualified simple registration request | |||
# parameters and add them to this object. | |||
# | |||
# This method is essentially the inverse of | |||
# getExtensionArgs. This method restores the serialized simple | |||
# registration request fields. | |||
# | |||
# If you are extracting arguments from a standard OpenID | |||
# checkid_* request, you probably want to use fromOpenIDRequest, | |||
# which will extract the sreg namespace and arguments from the | |||
# OpenID request. This method is intended for cases where the | |||
# OpenID server needs more control over how the arguments are | |||
# parsed than that method provides. | |||
def parse_extension_args(args, strict = false) | |||
required_items = args['required'] | |||
unless required_items.nil? or required_items.empty? | |||
required_items.split(',').each{|field_name| | |||
begin | |||
request_field(field_name, true, strict) | |||
rescue ArgumentError | |||
raise if strict | |||
end | |||
} | |||
end | |||
optional_items = args['optional'] | |||
unless optional_items.nil? or optional_items.empty? | |||
optional_items.split(',').each{|field_name| | |||
begin | |||
request_field(field_name, false, strict) | |||
rescue ArgumentError | |||
raise if strict | |||
end | |||
} | |||
end | |||
@policy_url = args['policy_url'] | |||
end | |||
# A list of all of the simple registration fields that were | |||
# requested, whether they were required or optional. | |||
def all_requested_fields | |||
@required + @optional | |||
end | |||
# Have any simple registration fields been requested? | |||
def were_fields_requested? | |||
!all_requested_fields.empty? | |||
end | |||
# Request the specified field from the OpenID user | |||
# field_name: the unqualified simple registration field name | |||
# required: whether the given field should be presented | |||
# to the user as being a required to successfully complete | |||
# the request | |||
# strict: whether to raise an exception when a field is | |||
# added to a request more than once | |||
# Raises ArgumentError if the field_name is not a simple registration | |||
# field, or if strict is set and a field is added more than once | |||
def request_field(field_name, required=false, strict=false) | |||
OpenID::check_sreg_field_name(field_name) | |||
if strict | |||
if (@required + @optional).member? field_name | |||
raise ArgumentError, 'That field has already been requested' | |||
end | |||
else | |||
return if @required.member? field_name | |||
if @optional.member? field_name | |||
if required | |||
@optional.delete field_name | |||
else | |||
return | |||
end | |||
end | |||
end | |||
if required | |||
@required << field_name | |||
else | |||
@optional << field_name | |||
end | |||
end | |||
# Add the given list of fields to the request. | |||
def request_fields(field_names, required = false, strict = false) | |||
raise ArgumentError unless field_names.respond_to?(:each) and | |||
field_names[0].is_a?(String) | |||
field_names.each{|fn|request_field(fn, required, strict)} | |||
end | |||
# Get a hash of unqualified simple registration arguments | |||
# representing this request. | |||
# This method is essentially the inverse of parse_extension_args. | |||
# This method serializes the simple registration request fields. | |||
def get_extension_args | |||
args = {} | |||
args['required'] = @required.join(',') unless @required.empty? | |||
args['optional'] = @optional.join(',') unless @optional.empty? | |||
args['policy_url'] = @policy_url unless @policy_url.nil? | |||
return args | |||
end | |||
def member?(field_name) | |||
all_requested_fields.member?(field_name) | |||
end | |||
end | |||
# Represents the data returned in a simple registration response | |||
# inside of an OpenID id_res response. This object will be | |||
# created by the OpenID server, added to the id_res response | |||
# object, and then extracted from the id_res message by the Consumer. | |||
class Response < Extension | |||
attr_reader :ns_uri, :data | |||
def initialize(data = {}, ns_uri=NS_URI) | |||
@ns_alias = 'sreg' | |||
@data = data | |||
@ns_uri = ns_uri | |||
end | |||
# Take a Request and a hash of simple registration | |||
# values and create a Response object containing that data. | |||
def self.extract_response(request, data) | |||
arf = request.all_requested_fields | |||
resp_data = data.reject{|k,v| !arf.member?(k) || v.nil? } | |||
new(resp_data, request.ns_uri) | |||
end | |||
# Create an Response object from an | |||
# OpenID::Consumer::SuccessResponse from consumer.complete | |||
# If you set the signed_only parameter to false, unsigned data from | |||
# the id_res message from the server will be processed. | |||
def self.from_success_response(success_response, signed_only = true) | |||
ns_uri = OpenID::get_sreg_ns(success_response.message) | |||
if signed_only | |||
args = success_response.get_signed_ns(ns_uri) | |||
return nil if args.nil? # No signed args, so fail | |||
else | |||
args = success_response.message.get_args(ns_uri) | |||
end | |||
args.reject!{|k,v| !DATA_FIELDS.member?(k) } | |||
new(args, ns_uri) | |||
end | |||
# Get the fields to put in the simple registration namespace | |||
# when adding them to an id_res message. | |||
def get_extension_args | |||
return @data | |||
end | |||
# Read-only hashlike interface. | |||
# Raises an exception if the field name is bad | |||
def [](field_name) | |||
OpenID::check_sreg_field_name(field_name) | |||
data[field_name] | |||
end | |||
def empty? | |||
@data.empty? | |||
end | |||
# XXX is there more to a hashlike interface I should add? | |||
end | |||
end | |||
end | |||
@@ -1,11 +0,0 @@ | |||
class String | |||
def starts_with?(other) | |||
head = self[0, other.length] | |||
head == other | |||
end | |||
def ends_with?(other) | |||
tail = self[-1 * other.length, other.length] | |||
tail == other | |||
end | |||
end |
@@ -1,238 +0,0 @@ | |||
require 'net/http' | |||
require 'openid' | |||
require 'openid/util' | |||
begin | |||
require 'net/https' | |||
rescue LoadError | |||
OpenID::Util.log('WARNING: no SSL support found. Will not be able ' + | |||
'to fetch HTTPS URLs!') | |||
require 'net/http' | |||
end | |||
MAX_RESPONSE_KB = 1024 | |||
module Net | |||
class HTTP | |||
def post_connection_check(hostname) | |||
check_common_name = true | |||
cert = @socket.io.peer_cert | |||
cert.extensions.each { |ext| | |||
next if ext.oid != "subjectAltName" | |||
ext.value.split(/,\s+/).each{ |general_name| | |||
if /\ADNS:(.*)/ =~ general_name | |||
check_common_name = false | |||
reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") | |||
return true if /\A#{reg}\z/i =~ hostname | |||
elsif /\AIP Address:(.*)/ =~ general_name | |||
check_common_name = false | |||
return true if $1 == hostname | |||
end | |||
} | |||
} | |||
if check_common_name | |||
cert.subject.to_a.each{ |oid, value| | |||
if oid == "CN" | |||
reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") | |||
return true if /\A#{reg}\z/i =~ hostname | |||
end | |||
} | |||
end | |||
raise OpenSSL::SSL::SSLError, "hostname does not match" | |||
end | |||
end | |||
end | |||
module OpenID | |||
# Our HTTPResponse class extends Net::HTTPResponse with an additional | |||
# method, final_url. | |||
class HTTPResponse | |||
attr_accessor :final_url | |||
attr_accessor :_response | |||
def self._from_net_response(response, final_url, headers=nil) | |||
me = self.new | |||
me._response = response | |||
me.final_url = final_url | |||
return me | |||
end | |||
def method_missing(method, *args) | |||
@_response.send(method, *args) | |||
end | |||
def body=(s) | |||
@_response.instance_variable_set('@body', s) | |||
# XXX Hack to work around ruby's HTTP library behavior. @body | |||
# is only returned if it has been read from the response | |||
# object's socket, but since we're not using a socket in this | |||
# case, we need to set the @read flag to true to avoid a bug in | |||
# Net::HTTPResponse.stream_check when @socket is nil. | |||
@_response.instance_variable_set('@read', true) | |||
end | |||
end | |||
class FetchingError < OpenIDError | |||
end | |||
class HTTPRedirectLimitReached < FetchingError | |||
end | |||
class SSLFetchingError < FetchingError | |||
end | |||
@fetcher = nil | |||
def self.fetch(url, body=nil, headers=nil, | |||
redirect_limit=StandardFetcher::REDIRECT_LIMIT) | |||
return fetcher.fetch(url, body, headers, redirect_limit) | |||
end | |||
def self.fetcher | |||
if @fetcher.nil? | |||
@fetcher = StandardFetcher.new | |||
end | |||
return @fetcher | |||
end | |||
def self.fetcher=(fetcher) | |||
@fetcher = fetcher | |||
end | |||
# Set the default fetcher to use the HTTP proxy defined in the environment | |||
# variable 'http_proxy'. | |||
def self.fetcher_use_env_http_proxy | |||
proxy_string = ENV['http_proxy'] | |||
return unless proxy_string | |||
proxy_uri = URI.parse(proxy_string) | |||
@fetcher = StandardFetcher.new(proxy_uri.host, proxy_uri.port, | |||
proxy_uri.user, proxy_uri.password) | |||
end | |||
class StandardFetcher | |||
USER_AGENT = "ruby-openid/#{OpenID::VERSION} (#{RUBY_PLATFORM})" | |||
REDIRECT_LIMIT = 5 | |||
TIMEOUT = 60 | |||
attr_accessor :ca_file | |||
attr_accessor :timeout | |||
# I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy. | |||
def initialize(proxy_addr=nil, proxy_port=nil, | |||
proxy_user=nil, proxy_pass=nil) | |||
@ca_file = nil | |||
@proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass) | |||
@timeout = TIMEOUT | |||
end | |||
def supports_ssl?(conn) | |||
return conn.respond_to?(:use_ssl=) | |||
end | |||
def make_http(uri) | |||
http = @proxy.new(uri.host, uri.port) | |||
http.read_timeout = @timeout | |||
http.open_timeout = @timeout | |||
return http | |||
end | |||
def set_verified(conn, verify) | |||
if verify | |||
conn.verify_mode = OpenSSL::SSL::VERIFY_PEER | |||
else | |||
conn.verify_mode = OpenSSL::SSL::VERIFY_NONE | |||
end | |||
end | |||
def make_connection(uri) | |||
conn = make_http(uri) | |||
if !conn.is_a?(Net::HTTP) | |||
raise RuntimeError, sprintf("Expected Net::HTTP object from make_http; got %s", | |||
conn.class) | |||
end | |||
if uri.scheme == 'https' | |||
if supports_ssl?(conn) | |||
conn.use_ssl = true | |||
if @ca_file | |||
set_verified(conn, true) | |||
conn.ca_file = @ca_file | |||
else | |||
Util.log("WARNING: making https request to #{uri} without verifying " + | |||
"server certificate; no CA path was specified.") | |||
set_verified(conn, false) | |||
end | |||
else | |||
raise RuntimeError, "SSL support not found; cannot fetch #{uri}" | |||
end | |||
end | |||
return conn | |||
end | |||
def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) | |||
unparsed_url = url.dup | |||
url = URI::parse(url) | |||
if url.nil? | |||
raise FetchingError, "Invalid URL: #{unparsed_url}" | |||
end | |||
headers ||= {} | |||
headers['User-agent'] ||= USER_AGENT | |||
begin | |||
conn = make_connection(url) | |||
response = nil | |||
response = conn.start { | |||
# Check the certificate against the URL's hostname | |||
if supports_ssl?(conn) and conn.use_ssl? | |||
conn.post_connection_check(url.host) | |||
end | |||
if body.nil? | |||
conn.request_get(url.request_uri, headers) | |||
else | |||
headers["Content-type"] ||= "application/x-www-form-urlencoded" | |||
conn.request_post(url.request_uri, body, headers) | |||
end | |||
} | |||
rescue RuntimeError => why | |||
raise why | |||
rescue OpenSSL::SSL::SSLError => why | |||
raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{why}" | |||
rescue FetchingError => why | |||
raise why | |||
rescue Exception => why | |||
# Things we've caught here include a Timeout::Error, which descends | |||
# from SignalException. | |||
raise FetchingError, "Error fetching #{url}: #{why}" | |||
end | |||
case response | |||
when Net::HTTPRedirection | |||
if redirect_limit <= 0 | |||
raise HTTPRedirectLimitReached.new( | |||
"Too many redirects, not fetching #{response['location']}") | |||
end | |||
begin | |||
return fetch(response['location'], body, headers, redirect_limit - 1) | |||
rescue HTTPRedirectLimitReached => e | |||
raise e | |||
rescue FetchingError => why | |||
raise FetchingError, "Error encountered in redirect from #{url}: #{why}" | |||
end | |||
else | |||
return HTTPResponse._from_net_response(response, unparsed_url) | |||
end | |||
end | |||
end | |||
end |
@@ -1,136 +0,0 @@ | |||
module OpenID | |||
class KVFormError < Exception | |||
end | |||
module Util | |||
def Util.seq_to_kv(seq, strict=false) | |||
# Represent a sequence of pairs of strings as newline-terminated | |||
# key:value pairs. The pairs are generated in the order given. | |||
# | |||
# @param seq: The pairs | |||
# | |||
# returns a string representation of the sequence | |||
err = lambda { |msg| | |||
msg = "seq_to_kv warning: #{msg}: #{seq.inspect}" | |||
if strict | |||
raise KVFormError, msg | |||
else | |||
Util.log(msg) | |||
end | |||
} | |||
lines = [] | |||
seq.each { |k, v| | |||
if !k.is_a?(String) | |||
err.call("Converting key to string: #{k.inspect}") | |||
k = k.to_s | |||
end | |||
if !k.index("\n").nil? | |||
raise KVFormError, "Invalid input for seq_to_kv: key contains newline: #{k.inspect}" | |||
end | |||
if !k.index(":").nil? | |||
raise KVFormError, "Invalid input for seq_to_kv: key contains colon: #{k.inspect}" | |||
end | |||
if k.strip() != k | |||
err.call("Key has whitespace at beginning or end: #{k.inspect}") | |||
end | |||
if !v.is_a?(String) | |||
err.call("Converting value to string: #{v.inspect}") | |||
v = v.to_s | |||
end | |||
if !v.index("\n").nil? | |||
raise KVFormError, "Invalid input for seq_to_kv: value contains newline: #{v.inspect}" | |||
end | |||
if v.strip() != v | |||
err.call("Value has whitespace at beginning or end: #{v.inspect}") | |||
end | |||
lines << k + ":" + v + "\n" | |||
} | |||
return lines.join("") | |||
end | |||
def Util.kv_to_seq(data, strict=false) | |||
# After one parse, seq_to_kv and kv_to_seq are inverses, with no | |||
# warnings: | |||
# | |||
# seq = kv_to_seq(s) | |||
# seq_to_kv(kv_to_seq(seq)) == seq | |||
err = lambda { |msg| | |||
msg = "kv_to_seq warning: #{msg}: #{data.inspect}" | |||
if strict | |||
raise KVFormError, msg | |||
else | |||
Util.log(msg) | |||
end | |||
} | |||
lines = data.split("\n") | |||
if data.length == 0 | |||
return [] | |||
end | |||
if data[-1].chr != "\n" | |||
err.call("Does not end in a newline") | |||
# We don't expect the last element of lines to be an empty | |||
# string because split() doesn't behave that way. | |||
end | |||
pairs = [] | |||
line_num = 0 | |||
lines.each { |line| | |||
line_num += 1 | |||
# Ignore blank lines | |||
if line.strip() == "" | |||
next | |||
end | |||
pair = line.split(':', 2) | |||
if pair.length == 2 | |||
k, v = pair | |||
k_s = k.strip() | |||
if k_s != k | |||
msg = "In line #{line_num}, ignoring leading or trailing whitespace in key #{k.inspect}" | |||
err.call(msg) | |||
end | |||
if k_s.length == 0 | |||
err.call("In line #{line_num}, got empty key") | |||
end | |||
v_s = v.strip() | |||
if v_s != v | |||
msg = "In line #{line_num}, ignoring leading or trailing whitespace in value #{v.inspect}" | |||
err.call(msg) | |||
end | |||
pairs << [k_s, v_s] | |||
else | |||
err.call("Line #{line_num} does not contain a colon") | |||
end | |||
} | |||
return pairs | |||
end | |||
def Util.dict_to_kv(d) | |||
return seq_to_kv(d.entries.sort) | |||
end | |||
def Util.kv_to_dict(s) | |||
seq = kv_to_seq(s) | |||
return Hash[*seq.flatten] | |||
end | |||
end | |||
end |
@@ -1,58 +0,0 @@ | |||
require "openid/message" | |||
require "openid/fetchers" | |||
module OpenID | |||
# Exception that is raised when the server returns a 400 response | |||
# code to a direct request. | |||
class ServerError < OpenIDError | |||
attr_reader :error_text, :error_code, :message | |||
def initialize(error_text, error_code, message) | |||
super(error_text) | |||
@error_text = error_text | |||
@error_code = error_code | |||
@message = message | |||
end | |||
def self.from_message(msg) | |||
error_text = msg.get_arg(OPENID_NS, 'error', | |||
'<no error message supplied>') | |||
error_code = msg.get_arg(OPENID_NS, 'error_code') | |||
return self.new(error_text, error_code, msg) | |||
end | |||
end | |||
class KVPostNetworkError < OpenIDError | |||
end | |||
class HTTPStatusError < OpenIDError | |||
end | |||
class Message | |||
def self.from_http_response(response, server_url) | |||
msg = self.from_kvform(response.body) | |||
case response.code.to_i | |||
when 200 | |||
return msg | |||
when 206 | |||
return msg | |||
when 400 | |||
raise ServerError.from_message(msg) | |||
else | |||
error_message = "bad status code from server #{server_url}: "\ | |||
"#{response.code}" | |||
raise HTTPStatusError.new(error_message) | |||
end | |||
end | |||
end | |||
# Send the message to the server via HTTP POST and receive and parse | |||
# a response in KV Form | |||
def self.make_kv_post(request_message, server_url) | |||
begin | |||
http_response = self.fetch(server_url, request_message.to_url_encoded) | |||
rescue Exception | |||
raise KVPostNetworkError.new("Unable to contact OpenID server: #{$!.to_s}") | |||
end | |||
return Message.from_http_response(http_response, server_url) | |||
end | |||
end |
@@ -1,553 +0,0 @@ | |||
require 'openid/util' | |||
require 'openid/kvform' | |||
module OpenID | |||
IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select' | |||
# URI for Simple Registration extension, the only commonly deployed | |||
# OpenID 1.x extension, and so a special case. | |||
SREG_URI = 'http://openid.net/sreg/1.0' | |||
# The OpenID 1.x namespace URIs | |||
OPENID1_NS = 'http://openid.net/signon/1.0' | |||
OPENID11_NS = 'http://openid.net/signon/1.1' | |||
OPENID1_NAMESPACES = [OPENID1_NS, OPENID11_NS] | |||
# The OpenID 2.0 namespace URI | |||
OPENID2_NS = 'http://specs.openid.net/auth/2.0' | |||
# The namespace consisting of pairs with keys that are prefixed with | |||
# "openid." but not in another namespace. | |||
NULL_NAMESPACE = :null_namespace | |||
# The null namespace, when it is an allowed OpenID namespace | |||
OPENID_NS = :openid_namespace | |||
# The top-level namespace, excluding all pairs with keys that start | |||
# with "openid." | |||
BARE_NS = :bare_namespace | |||
# Limit, in bytes, of identity provider and return_to URLs, | |||
# including response payload. See OpenID 1.1 specification, | |||
# Appendix D. | |||
OPENID1_URL_LIMIT = 2047 | |||
# All OpenID protocol fields. Used to check namespace aliases. | |||
OPENID_PROTOCOL_FIELDS = [ | |||
'ns', 'mode', 'error', 'return_to', | |||
'contact', 'reference', 'signed', | |||
'assoc_type', 'session_type', | |||
'dh_modulus', 'dh_gen', | |||
'dh_consumer_public', 'claimed_id', | |||
'identity', 'realm', 'invalidate_handle', | |||
'op_endpoint', 'response_nonce', 'sig', | |||
'assoc_handle', 'trust_root', 'openid', | |||
] | |||
# Sentinel used for Message implementation to indicate that getArg | |||
# should raise an exception instead of returning a default. | |||
NO_DEFAULT = :no_default | |||
# Raised if the generic OpenID namespace is accessed when there | |||
# is no OpenID namespace set for this message. | |||
class UndefinedOpenIDNamespace < Exception; end | |||
# Raised when an alias or namespace URI has already been registered. | |||
class NamespaceAliasRegistrationError < Exception; end | |||
# Raised if openid.ns is not a recognized value. | |||
# See Message class variable @@allowed_openid_namespaces | |||
class InvalidOpenIDNamespace < Exception; end | |||
class Message | |||
attr_reader :namespaces | |||
# Raised when key lookup fails | |||
class KeyNotFound < IndexError ; end | |||
# Namespace / alias registration map. See | |||
# register_namespace_alias. | |||
@@registered_aliases = {} | |||
# Registers a (namespace URI, alias) mapping in a global namespace | |||
# alias map. Raises NamespaceAliasRegistrationError if either the | |||
# namespace URI or alias has already been registered with a | |||
# different value. This function is required if you want to use a | |||
# namespace with an OpenID 1 message. | |||
def Message.register_namespace_alias(namespace_uri, alias_) | |||
if @@registered_aliases[alias_] == namespace_uri | |||
return | |||
end | |||
if @@registered_aliases.values.include?(namespace_uri) | |||
raise NamespaceAliasRegistrationError, | |||
'Namespace uri #{namespace_uri} already registered' | |||
end | |||
if @@registered_aliases.member?(alias_) | |||
raise NamespaceAliasRegistrationError, | |||
'Alias #{alias_} already registered' | |||
end | |||
@@registered_aliases[alias_] = namespace_uri | |||
end | |||
@@allowed_openid_namespaces = [OPENID1_NS, OPENID2_NS, OPENID11_NS] | |||
# Raises InvalidNamespaceError if you try to instantiate a Message | |||
# with a namespace not in the above allowed list | |||
def initialize(openid_namespace=nil) | |||
@args = {} | |||
@namespaces = NamespaceMap.new | |||
if openid_namespace | |||
implicit = OPENID1_NAMESPACES.member? openid_namespace | |||
self.set_openid_namespace(openid_namespace, implicit) | |||
else | |||
@openid_ns_uri = nil | |||
end | |||
end | |||
# Construct a Message containing a set of POST arguments. | |||
# Raises InvalidNamespaceError if you try to instantiate a Message | |||
# with a namespace not in the above allowed list | |||
def Message.from_post_args(args) | |||
m = Message.new | |||
openid_args = {} | |||
args.each do |key,value| | |||
if value.is_a?(Array) | |||
raise ArgumentError, "Query dict must have one value for each key, " + | |||
"not lists of values. Query is #{args.inspect}" | |||
end | |||
prefix, rest = key.split('.', 2) | |||
if prefix != 'openid' or rest.nil? | |||
m.set_arg(BARE_NS, key, value) | |||
else | |||
openid_args[rest] = value | |||
end | |||
end | |||
m._from_openid_args(openid_args) | |||
return m | |||
end | |||
# Construct a Message from a parsed KVForm message. | |||
# Raises InvalidNamespaceError if you try to instantiate a Message | |||
# with a namespace not in the above allowed list | |||
def Message.from_openid_args(openid_args) | |||
m = Message.new | |||
m._from_openid_args(openid_args) | |||
return m | |||
end | |||
# Raises InvalidNamespaceError if you try to instantiate a Message | |||
# with a namespace not in the above allowed list | |||
def _from_openid_args(openid_args) | |||
ns_args = [] | |||
# resolve namespaces | |||
openid_args.each { |rest, value| | |||
ns_alias, ns_key = rest.split('.', 2) | |||
if ns_key.nil? | |||
ns_alias = NULL_NAMESPACE | |||
ns_key = rest | |||
end | |||
if ns_alias == 'ns' | |||
@namespaces.add_alias(value, ns_key) | |||
elsif ns_alias == NULL_NAMESPACE and ns_key == 'ns' | |||
set_openid_namespace(value, false) | |||
else | |||
ns_args << [ns_alias, ns_key, value] | |||
end | |||
} | |||
# implicitly set an OpenID 1 namespace | |||
unless get_openid_namespace | |||
set_openid_namespace(OPENID1_NS, true) | |||
end | |||
# put the pairs into the appropriate namespaces | |||
ns_args.each { |ns_alias, ns_key, value| | |||
ns_uri = @namespaces.get_namespace_uri(ns_alias) | |||
unless ns_uri | |||
ns_uri = _get_default_namespace(ns_alias) | |||
unless ns_uri | |||
ns_uri = get_openid_namespace | |||
ns_key = "#{ns_alias}.#{ns_key}" | |||
else | |||
@namespaces.add_alias(ns_uri, ns_alias, true) | |||
end | |||
end | |||
self.set_arg(ns_uri, ns_key, value) | |||
} | |||
end | |||
def _get_default_namespace(mystery_alias) | |||
# only try to map an alias to a default if it's an | |||
# OpenID 1.x namespace | |||
if is_openid1 | |||
@@registered_aliases[mystery_alias] | |||
end | |||
end | |||
def set_openid_namespace(openid_ns_uri, implicit) | |||
if !@@allowed_openid_namespaces.include?(openid_ns_uri) | |||
raise InvalidOpenIDNamespace, "Invalid null namespace: #{openid_ns_uri}" | |||
end | |||
@namespaces.add_alias(openid_ns_uri, NULL_NAMESPACE, implicit) | |||
@openid_ns_uri = openid_ns_uri | |||
end | |||
def get_openid_namespace | |||
return @openid_ns_uri | |||
end | |||
def is_openid1 | |||
return OPENID1_NAMESPACES.member?(@openid_ns_uri) | |||
end | |||
def is_openid2 | |||
return @openid_ns_uri == OPENID2_NS | |||
end | |||
# Create a message from a KVForm string | |||
def Message.from_kvform(kvform_string) | |||
return Message.from_openid_args(Util.kv_to_dict(kvform_string)) | |||
end | |||
def copy | |||
return Marshal.load(Marshal.dump(self)) | |||
end | |||
# Return all arguments with "openid." in from of namespaced arguments. | |||
def to_post_args | |||
args = {} | |||
# add namespace defs to the output | |||
@namespaces.each { |ns_uri, ns_alias| | |||
if @namespaces.implicit?(ns_uri) | |||
next | |||
end | |||
if ns_alias == NULL_NAMESPACE | |||
ns_key = 'openid.ns' | |||
else | |||
ns_key = 'openid.ns.' + ns_alias | |||
end | |||
args[ns_key] = ns_uri | |||
} | |||
@args.each { |k, value| | |||
ns_uri, ns_key = k | |||
key = get_key(ns_uri, ns_key) | |||
args[key] = value | |||
} | |||
return args | |||
end | |||
# Return all namespaced arguments, failing if any non-namespaced arguments | |||
# exist. | |||
def to_args | |||
post_args = self.to_post_args | |||
kvargs = {} | |||
post_args.each { |k,v| | |||
if !k.starts_with?('openid.') | |||
raise ArgumentError, "This message can only be encoded as a POST, because it contains arguments that are not prefixed with 'openid.'" | |||
else | |||
kvargs[k[7..-1]] = v | |||
end | |||
} | |||
return kvargs | |||
end | |||
# Generate HTML form markup that contains the values in this | |||
# message, to be HTTP POSTed as x-www-form-urlencoded UTF-8. | |||
def to_form_markup(action_url, form_tag_attrs=nil, submit_text='Continue') | |||
form_tag_attr_map = {} | |||
if form_tag_attrs | |||
form_tag_attrs.each { |name, attr| | |||
form_tag_attr_map[name] = attr | |||
} | |||
end | |||
form_tag_attr_map['action'] = action_url | |||
form_tag_attr_map['method'] = 'post' | |||
form_tag_attr_map['accept-charset'] = 'UTF-8' | |||
form_tag_attr_map['enctype'] = 'application/x-www-form-urlencoded' | |||
markup = "<form " | |||
form_tag_attr_map.each { |k, v| | |||
markup += " #{k}=\"#{v}\"" | |||
} | |||
markup += ">\n" | |||
to_post_args.each { |k,v| | |||
markup += "<input type='hidden' name='#{k}' value='#{v}' />\n" | |||
} | |||
markup += "<input type='submit' value='#{submit_text}' />\n" | |||
markup += "\n</form>" | |||
return markup | |||
end | |||
# Generate a GET URL with the paramters in this message attacked as | |||
# query parameters. | |||
def to_url(base_url) | |||
return Util.append_args(base_url, self.to_post_args) | |||
end | |||
# Generate a KVForm string that contains the parameters in this message. | |||
# This will fail is the message contains arguments outside of the | |||
# "openid." prefix. | |||
def to_kvform | |||
return Util.dict_to_kv(to_args) | |||
end | |||
# Generate an x-www-urlencoded string. | |||
def to_url_encoded | |||
args = to_post_args.map.sort | |||
return Util.urlencode(args) | |||
end | |||
# Convert an input value into the internally used values of this obejct. | |||
def _fix_ns(namespace) | |||
if namespace == OPENID_NS | |||
unless @openid_ns_uri | |||
raise UndefinedOpenIDNamespace, 'OpenID namespace not set' | |||
else | |||
namespace = @openid_ns_uri | |||
end | |||
end | |||
if namespace == BARE_NS | |||
return namespace | |||
end | |||
if !namespace.is_a?(String) | |||
raise ArgumentError, ("Namespace must be BARE_NS, OPENID_NS or "\ | |||
"a string. Got #{namespace.inspect}") | |||
end | |||
if namespace.index(':').nil? | |||
msg = ("OpenID 2.0 namespace identifiers SHOULD be URIs. "\ | |||
"Got #{namespace.inspect}") | |||
Util.log(msg) | |||
if namespace == 'sreg' | |||
msg = "Using #{SREG_URI} instead of \"sreg\" as namespace" | |||
Util.log(msg) | |||
return SREG_URI | |||
end | |||
end | |||
return namespace | |||
end | |||
def has_key?(namespace, ns_key) | |||
namespace = _fix_ns(namespace) | |||
return @args.member?([namespace, ns_key]) | |||
end | |||
# Get the key for a particular namespaced argument | |||
def get_key(namespace, ns_key) | |||
namespace = _fix_ns(namespace) | |||
return ns_key if namespace == BARE_NS | |||
ns_alias = @namespaces.get_alias(namespace) | |||
# no alias is defined, so no key can exist | |||
return nil if ns_alias.nil? | |||
if ns_alias == NULL_NAMESPACE | |||
tail = ns_key | |||
else | |||
tail = "#{ns_alias}.#{ns_key}" | |||
end | |||
return 'openid.' + tail | |||
end | |||
# Get a value for a namespaced key. | |||
def get_arg(namespace, key, default=nil) | |||
namespace = _fix_ns(namespace) | |||
@args.fetch([namespace, key]) { | |||
if default == NO_DEFAULT | |||
raise KeyNotFound, "<#{namespace}>#{key} not in this message" | |||
else | |||
default | |||
end | |||
} | |||
end | |||
# Get the arguments that are defined for this namespace URI. | |||
def get_args(namespace) | |||
namespace = _fix_ns(namespace) | |||
args = {} | |||
@args.each { |k,v| | |||
pair_ns, ns_key = k | |||
args[ns_key] = v if pair_ns == namespace | |||
} | |||
return args | |||
end | |||
# Set multiple key/value pairs in one call. | |||
def update_args(namespace, updates) | |||
namespace = _fix_ns(namespace) | |||
updates.each {|k,v| set_arg(namespace, k, v)} | |||
end | |||
# Set a single argument in this namespace | |||
def set_arg(namespace, key, value) | |||
namespace = _fix_ns(namespace) | |||
@args[[namespace, key].freeze] = value | |||
if namespace != BARE_NS | |||
@namespaces.add(namespace) | |||
end | |||
end | |||
# Remove a single argument from this namespace. | |||
def del_arg(namespace, key) | |||
namespace = _fix_ns(namespace) | |||
_key = [namespace, key] | |||
@args.delete(_key) | |||
end | |||
def ==(other) | |||
other.is_a?(self.class) && @args == other.instance_eval { @args } | |||
end | |||
def get_aliased_arg(aliased_key, default=nil) | |||
if aliased_key == 'ns' | |||
return get_openid_namespace() | |||
end | |||
ns_alias, key = aliased_key.split('.', 2) | |||
if ns_alias == 'ns' | |||
uri = @namespaces.get_namespace_uri(key) | |||
if uri.nil? and default == NO_DEFAULT | |||
raise KeyNotFound, "Namespace #{key} not defined when looking "\ | |||
"for #{aliased_key}" | |||
else | |||
return (uri.nil? ? default : uri) | |||
end | |||
end | |||
if key.nil? | |||
key = aliased_key | |||
ns = nil | |||
else | |||
ns = @namespaces.get_namespace_uri(ns_alias) | |||
end | |||
if ns.nil? | |||
key = aliased_key | |||
ns = get_openid_namespace | |||
end | |||
return get_arg(ns, key, default) | |||
end | |||
end | |||
# Maintains a bidirectional map between namespace URIs and aliases. | |||
class NamespaceMap | |||
def initialize | |||
@alias_to_namespace = {} | |||
@namespace_to_alias = {} | |||
@implicit_namespaces = [] | |||
end | |||
def get_alias(namespace_uri) | |||
@namespace_to_alias[namespace_uri] | |||
end | |||
def get_namespace_uri(namespace_alias) | |||
@alias_to_namespace[namespace_alias] | |||
end | |||
# Add an alias from this namespace URI to the alias. | |||
def add_alias(namespace_uri, desired_alias, implicit=false) | |||
# Check that desired_alias is not an openid protocol field as | |||
# per the spec. | |||
Util.assert(!OPENID_PROTOCOL_FIELDS.include?(desired_alias), | |||
"#{desired_alias} is not an allowed namespace alias") | |||
# check that there is not a namespace already defined for the | |||
# desired alias | |||
current_namespace_uri = @alias_to_namespace.fetch(desired_alias, nil) | |||
if current_namespace_uri and current_namespace_uri != namespace_uri | |||
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. #{current_namespace_uri} is already mapped to alias #{desired_alias}" | |||
end | |||
# Check that desired_alias does not contain a period as per the | |||
# spec. | |||
if desired_alias.is_a?(String) | |||
Util.assert(desired_alias.index('.').nil?, | |||
"#{desired_alias} must not contain a dot") | |||
end | |||
# check that there is not already a (different) alias for this | |||
# namespace URI. | |||
_alias = @namespace_to_alias[namespace_uri] | |||
if _alias and _alias != desired_alias | |||
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. It is already mapped to alias #{_alias}" | |||
end | |||
@alias_to_namespace[desired_alias] = namespace_uri | |||
@namespace_to_alias[namespace_uri] = desired_alias | |||
@implicit_namespaces << namespace_uri if implicit | |||
return desired_alias | |||
end | |||
# Add this namespace URI to the mapping, without caring what alias | |||
# it ends up with. | |||
def add(namespace_uri) | |||
# see if this namepace is already mapped to an alias | |||
_alias = @namespace_to_alias[namespace_uri] | |||
return _alias if _alias | |||
# Fall back to generating a numberical alias | |||
i = 0 | |||
while true | |||
_alias = 'ext' + i.to_s | |||
begin | |||
add_alias(namespace_uri, _alias) | |||
rescue IndexError | |||
i += 1 | |||
else | |||
return _alias | |||
end | |||
end | |||
raise StandardError, 'Unreachable' | |||
end | |||
def member?(namespace_uri) | |||
@namespace_to_alias.has_key?(namespace_uri) | |||
end | |||
def each | |||
@namespace_to_alias.each {|k,v| yield k,v} | |||
end | |||
def namespace_uris | |||
# Return an iterator over the namespace URIs | |||
return @namespace_to_alias.keys() | |||
end | |||
def implicit?(namespace_uri) | |||
return @implicit_namespaces.member?(namespace_uri) | |||
end | |||
def aliases | |||
# Return an iterator over the aliases | |||
return @alias_to_namespace.keys() | |||
end | |||
end | |||
end |
@@ -1,8 +0,0 @@ | |||
require 'openid/util' | |||
module OpenID | |||
# An error in the OpenID protocol | |||
class ProtocolError < OpenIDError | |||
end | |||
end |
@@ -1,271 +0,0 @@ | |||
require 'fileutils' | |||
require 'pathname' | |||
require 'tempfile' | |||
require 'openid/util' | |||
require 'openid/store/interface' | |||
require 'openid/association' | |||
module OpenID | |||
module Store | |||
class Filesystem < Interface | |||
@@FILENAME_ALLOWED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-".split("") | |||
# 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') | |||
self.ensure_dir(@nonce_dir) | |||
self.ensure_dir(@association_dir) | |||
self.ensure_dir(@temp_dir) | |||
end | |||
# Create a unique filename for a given server url and handle. The | |||
# filename that is returned will contain the domain name from the | |||
# server URL for ease of human inspection of the data dir. | |||
def get_association_filename(server_url, handle) | |||
unless server_url.index('://') | |||
raise ArgumentError, "Bad server URL: #{server_url}" | |||
end | |||
proto, rest = server_url.split('://', 2) | |||
domain = filename_escape(rest.split('/',2)[0]) | |||
url_hash = safe64(server_url) | |||
if handle | |||
handle_hash = safe64(handle) | |||
else | |||
handle_hash = '' | |||
end | |||
filename = [proto,domain,url_hash,handle_hash].join('-') | |||
@association_dir.join(filename) | |||
end | |||
# Store an association in the assoc directory | |||
def store_association(server_url, association) | |||
assoc_s = association.serialize | |||
filename = get_association_filename(server_url, association.handle) | |||
f, tmp = mktemp | |||
begin | |||
begin | |||
f.write(assoc_s) | |||
f.fsync | |||
ensure | |||
f.close | |||
end | |||
begin | |||
File.rename(tmp, filename) | |||
rescue Errno::EEXIST | |||
begin | |||
File.unlink(filename) | |||
rescue Errno::ENOENT | |||
# do nothing | |||
end | |||
File.rename(tmp, filename) | |||
end | |||
rescue | |||
self.remove_if_present(tmp) | |||
raise | |||
end | |||
end | |||
# Retrieve an association | |||
def get_association(server_url, handle=nil) | |||
# the filename with empty handle is the prefix for the associations | |||
# for a given server url | |||
filename = get_association_filename(server_url, handle) | |||
if handle | |||
return _get_association(filename) | |||
end | |||
assoc_filenames = Dir.glob(filename.to_s + '*') | |||
assocs = assoc_filenames.collect do |f| | |||
_get_association(f) | |||
end | |||
assocs = assocs.find_all { |a| not a.nil? } | |||
assocs = assocs.sort_by { |a| a.issued } | |||
return nil if assocs.empty? | |||
return assocs[-1] | |||
end | |||
def _get_association(filename) | |||
begin | |||
assoc_file = File.open(filename, "r") | |||
rescue Errno::ENOENT | |||
return nil | |||
else | |||
begin | |||
assoc_s = assoc_file.read | |||
ensure | |||
assoc_file.close | |||
end | |||
begin | |||
association = Association.deserialize(assoc_s) | |||
rescue | |||
self.remove_if_present(filename) | |||
return nil | |||
end | |||
# clean up expired associations | |||
if association.expires_in == 0 | |||
self.remove_if_present(filename) | |||
return nil | |||
else | |||
return association | |||
end | |||
end | |||
end | |||
# Remove an association if it exists, otherwise do nothing. | |||
def remove_association(server_url, handle) | |||
assoc = get_association(server_url, handle) | |||
if assoc.nil? | |||
return false | |||
else | |||
filename = get_association_filename(server_url, handle) | |||
return self.remove_if_present(filename) | |||
end | |||
end | |||
# Return whether the nonce is valid | |||
def use_nonce(server_url, timestamp, salt) | |||
return false if (timestamp - Time.now.to_i).abs > Nonce.skew | |||
if server_url and !server_url.empty? | |||
proto, rest = server_url.split('://',2) | |||
else | |||
proto, rest = '','' | |||
end | |||
raise "Bad server URL" unless proto && rest | |||
domain = filename_escape(rest.split('/',2)[0]) | |||
url_hash = safe64(server_url) | |||
salt_hash = safe64(salt) | |||
nonce_fn = '%08x-%s-%s-%s-%s'%[timestamp, proto, domain, url_hash, salt_hash] | |||
filename = @nonce_dir.join(nonce_fn) | |||
begin | |||
fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0200) | |||
fd.close | |||
return true | |||
rescue Errno::EEXIST | |||
return false | |||
end | |||
end | |||
# Remove expired entries from the database. This is potentially expensive, | |||
# so only run when it is acceptable to take time. | |||
def cleanup | |||
cleanup_associations | |||
cleanup_nonces | |||
end | |||
def cleanup_associations | |||
association_filenames = Dir[@association_dir.join("*").to_s] | |||
count = 0 | |||
association_filenames.each do |af| | |||
begin | |||
f = File.open(af, 'r') | |||
rescue Errno::ENOENT | |||
next | |||
else | |||
begin | |||
assoc_s = f.read | |||
ensure | |||
f.close | |||
end | |||
begin | |||
association = OpenID::Association.deserialize(assoc_s) | |||
rescue StandardError | |||
self.remove_if_present(af) | |||
next | |||
else | |||
if association.expires_in == 0 | |||
self.remove_if_present(af) | |||
count += 1 | |||
end | |||
end | |||
end | |||
end | |||
return count | |||
end | |||
def cleanup_nonces | |||
nonces = Dir[@nonce_dir.join("*").to_s] | |||
now = Time.now.to_i | |||
count = 0 | |||
nonces.each do |filename| | |||
nonce = filename.split('/')[-1] | |||
timestamp = nonce.split('-', 2)[0].to_i(16) | |||
nonce_age = (timestamp - now).abs | |||
if nonce_age > Nonce.skew | |||
self.remove_if_present(filename) | |||
count += 1 | |||
end | |||
end | |||
return count | |||
end | |||
protected | |||
# Create a temporary file and return the File object and filename. | |||
def mktemp | |||
f = Tempfile.new('tmp', @temp_dir) | |||
[f, f.path] | |||
end | |||
# create a safe filename from a url | |||
def filename_escape(s) | |||
s = '' if s.nil? | |||
filename_chunks = [] | |||
s.split('').each do |c| | |||
if @@FILENAME_ALLOWED.index(c) | |||
filename_chunks << c | |||
else | |||
filename_chunks << sprintf("_%02X", c[0]) | |||
end | |||
end | |||
filename_chunks.join("") | |||
end | |||
def safe64(s) | |||
s = OpenID::CryptUtil.sha1(s) | |||
s = OpenID::Util.to_base64(s) | |||
s.gsub!('+', '_') | |||
s.gsub!('/', '.') | |||
s.gsub!('=', '') | |||
return s | |||
end | |||
# remove file if present in filesystem | |||
def remove_if_present(filename) | |||
begin | |||
File.unlink(filename) | |||
rescue Errno::ENOENT | |||
return false | |||
end | |||
return true | |||
end | |||
# ensure that a path exists | |||
def ensure_dir(dir_name) | |||
FileUtils::mkdir_p(dir_name) | |||
end | |||
end | |||
end | |||
end | |||