diff --git a/activesp.gemspec b/activesp.gemspec index 8f2fb28..616090b 100644 --- a/activesp.gemspec +++ b/activesp.gemspec @@ -12,8 +12,9 @@ Gem::Specification.new do |s| s.files += Dir['lib/**/*.rb'] # s.bindir = "bin" # s.executables.push(*(Dir['bin/*.rb'])) - s.add_dependency('savon-xaop', '= 0.7.2.7') - s.add_dependency('nokogiri') + s.add_dependency('savon', '>= 0.9.8') + s.add_dependency('curb') + s.add_dependency('httpi', '= 0.9.4') # s.rdoc_options << '--exclude' << 'ext' << '--main' << 'README' # s.extra_rdoc_files = ["README"] s.has_rdoc = false diff --git a/lib/activesp.rb b/lib/activesp.rb index 6c12fa3..e768358 100644 --- a/lib/activesp.rb +++ b/lib/activesp.rb @@ -50,6 +50,9 @@ module ActiveSP require 'activesp/ghost_field' require 'activesp/user' require 'activesp/group' +require 'activesp/user_group_proxy' require 'activesp/role' require 'activesp/permission_set' require 'activesp/file' +require 'activesp/site_template' +require 'activesp/list_template' diff --git a/lib/activesp/base.rb b/lib/activesp/base.rb index 0be8700..e90a3f6 100644 --- a/lib/activesp/base.rb +++ b/lib/activesp/base.rb @@ -31,6 +31,16 @@ class Base extend Caching extend Associations + # @private + def relative_url(site_or_list = @list ? @list.site.connection.root : connection.root) + reference_url = site_or_list.url + reference_url += "/" unless reference_url[-1, 1] == "/" + url = self.url + reference_url = reference_url.sub(/\Ahttps?:\/\/[^\/]+/, "") + url = url.sub(/\Ahttps?:\/\/[^\/]+/, "") + url[reference_url.length..-1] + end + # Returns a key that can be used to retrieve this object later on using {Connection#find_by_key} # @return [String] def key @@ -85,9 +95,11 @@ def attribute_type(name) # @return [Integer, Float, String, Time, Boolean, Base] The assigned value # @raise [ArgumentError] Raised when this object does not have an attribute by the given name or if the attribute by the given name is read-only def set_attribute(name, value) - has_attribute?(name) and field = attribute_type(name) and internal_attribute_types[name] or raise ArgumentError, "#{self} has no field by the name #{name}" - !field.ReadOnly or raise ArgumentError, "field #{name} of #{self} is read-only" - current_attributes[name] = type_check_attribute(field, value) + set_attribute_internal(name, value, false) + end + + def set_attribute!(name, value) + set_attribute_internal(name, value, true) end # Provides convenient getters and setters for attributes. Note that no name mangling @@ -134,6 +146,16 @@ def changed_attributes changes end + def set_attribute_internal(name, value, override_restrictions) + has_attribute?(name) and field = attribute_type_internal(name, override_restrictions) or raise ArgumentError, "#{self} has no field by the name #{name}" + override_restrictions or !field.ReadOnly or raise ArgumentError, "field #{name} of #{self} is read-only" + current_attributes[name] = type_check_attribute(field, value, override_restrictions) + end + + def attribute_type_internal(name, override_restrictions) + attribute_type(name) + end + end end diff --git a/lib/activesp/connection.rb b/lib/activesp/connection.rb index 3504407..e48da61 100644 --- a/lib/activesp/connection.rb +++ b/lib/activesp/connection.rb @@ -24,12 +24,71 @@ # OTHER DEALINGS IN THE SOFTWARE. require 'savon' +require 'activesp/wasabi_authentication' require 'net/ntlm_http' -Savon::Request.logger.level = Logger::ERROR +Savon.configure do |config| + config.log = false +end + +HTTPI.log = false + +HTTPI.adapter = :curb + +class Savon::SOAP::Fault + + def error_code + Integer(((to_hash[:fault] || {})[:detail] || {})[:errorcode] || 0) + end + + def error_string + ((to_hash[:fault] || {})[:detail] || {})[:errorstring] + end + +end -Savon::Response.error_handler do |soap_fault| - soap_fault[:detail][:errorstring] +class HTTPI::Auth::Config + + # Accessor for the GSSNEGOTIATE auth credentials. + def gssnegotiate(*args) + return @gssnegotiate if args.empty? + + self.type = :gssnegotiate + @gssnegotiate = args.flatten.compact + end + + # Returns whether to use GSSNEGOTIATE auth. + def gssnegotiate? + type == :gssnegotiate + end + +end + +class HTTPI::Adapter::Curb + + def setup_client(request) + basic_setup request + setup_http_auth request if request.auth.http? + setup_ssl_auth request.auth.ssl if request.auth.ssl? + setup_ntlm_auth request if request.auth.ntlm? + setup_gssnegotiate_auth request if request.auth.gssnegotiate? + end + + def setup_gssnegotiate_auth(request) + client.username, client.password = *request.auth.credentials + client.http_auth_types = request.auth.type + end + +end + +# This is because setting the cookie causes problems on SP 2011 +class Savon::Client + +private + + def set_cookie(headers) + end + end module ActiveSP @@ -43,7 +102,7 @@ class Connection # @private # TODO: create profile - attr_reader :login, :password, :auth_type, :root_url, :trace + attr_reader :login, :password, :auth_type, :root_url, :trace, :user_group_proxy # @param [Hash] options The connection options # @option options [String] :root The URL of the root site @@ -57,6 +116,7 @@ def initialize(options = {}) @password = options.delete(:password) @auth_type = options.delete(:auth_type) || :ntlm @trace = options.delete(:trace) + @user_group_proxy = options.delete(:user_group_proxy) options.empty? or raise ArgumentError, "unknown options #{options.keys.map { |k| k.inspect }.join(", ")}" cache = nil configure_persistent_cache { |c| cache ||= c } @@ -95,6 +155,15 @@ def find_by_key(key) else ActiveSP::ContentType.new(parent, nil, trail[1]) end + when ?M + case type[1] + when ?S + site_template(trail[0]) + when ?L + list_template(trail[0].to_i) + else + raise "not yet #{key.inspect}" + end else raise "not yet #{key.inspect}" end @@ -107,54 +176,56 @@ def find_by_key(key) # @param [String] url The URL to fetch # @return [String] The content fetched from the URL def fetch(url) - # TODO: support HTTPS too - @open_params ||= begin - u = URL(@root_url) - [u.host, u.port] - end - Net::HTTP.start(*@open_params) do |http| - request = Net::HTTP::Get.new(URL(url).full_path.gsub(/ /, "%20")) - if @login - case auth_type - when :ntlm - request.ntlm_auth(@login, @password) - when :basic - request.basic_auth(@login, @password) - else - raise ArgumentError, "Unknown authentication type #{auth_type.inspect}" - end + url = "#{protocol}://#{open_params.join(':')}#{url.gsub(/ /, "%20")}" unless /\Ahttp:\/\// === url + request = HTTPI::Request.new(url) + if login + case auth_type + when :ntlm + request.auth.ntlm(login, password) + when :basic + request.auth.basic(login, password) + when :digest + request.auth.digest(login, password) + when :gss_negotiate + request.auth.gssnegotiate(login, password) + else + raise ArgumentError, "Unknown authentication type #{auth_type.inspect}" end - response = http.request(request) - # if Net::HTTPFound === response - # response = fetch(response["location"]) - # end - # response end + HTTPI.get(request) end def head(url) - # TODO: support HTTPS too + url = "#{protocol}://#{open_params.join(':')}#{url.gsub(/ /, "%20")}" unless /\Ahttp:\/\// === url + request = HTTPI::Request.new(url) + if login + case auth_type + when :ntlm + request.auth.ntlm(login, password) + when :basic + request.auth.basic(login, password) + when :digest + request.auth.digest(login, password) + when :gss_negotiate + request.auth.gssnegotiate(login, password) + else + raise ArgumentError, "Unknown authentication type #{auth_type.inspect}" + end + end + HTTPI.head(request).headers + end + + def open_params @open_params ||= begin u = URL(@root_url) [u.host, u.port] end - Net::HTTP.start(*@open_params) do |http| - request = Net::HTTP::Head.new(URL(url).full_path.gsub(/ /, "%20")) - if @login - case auth_type - when :ntlm - request.ntlm_auth(@login, @password) - when :basic - request.basic_auth(@login, @password) - else - raise ArgumentError, "Unknown authentication type #{auth_type.inspect}" - end - end - response = http.request(request) - # if Net::HTTPFound === response - # response = fetch(response["location"]) - # end - # response + end + + def protocol + @protocol ||= begin + u = URL(@root_url) + u.protocol end end diff --git a/lib/activesp/content_type.rb b/lib/activesp/content_type.rb index dbe68d5..13de926 100644 --- a/lib/activesp/content_type.rb +++ b/lib/activesp/content_type.rb @@ -106,7 +106,7 @@ def fields_by_name # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, false) end # @private @@ -121,9 +121,9 @@ def to_s def data if @list - call("Lists", "get_list_content_type", "listName" => @list.id, "contentTypeId" => @id).xpath("//sp:ContentType", NS).first + call("Lists", "GetListContentType", "listName" => @list.id, "contentTypeId" => @id).xpath("//sp:ContentType", NS).first else - call("Webs", "get_content_type", "contentTypeId" => @id).xpath("//sp:ContentType", NS).first + call("Webs", "GetContentType", "contentTypeId" => @id).xpath("//sp:ContentType", NS).first end end cache :data diff --git a/lib/activesp/errors.rb b/lib/activesp/errors.rb index 1ec803f..9a199fd 100644 --- a/lib/activesp/errors.rb +++ b/lib/activesp/errors.rb @@ -1,8 +1,14 @@ module ActiveSP + class NotFound < Exception + end + class AccessDenied < Exception end + class PermissionDenied < Exception + end + class AlreadyExists < Exception def initialize(msg, &object_blk) diff --git a/lib/activesp/field.rb b/lib/activesp/field.rb index 3cb4cd8..5164898 100644 --- a/lib/activesp/field.rb +++ b/lib/activesp/field.rb @@ -30,6 +30,7 @@ class Field < Base include InSite extend Caching include Util + extend Util # @private attr_reader :ID, :Name, :internal_type @@ -78,7 +79,7 @@ def ReadOnly # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, false) end # @private @@ -89,6 +90,15 @@ def to_s # @private alias inspect to_s + def self.check_attributes_for_creation(site, attributes) + name = attributes.delete("Name") or raise ArgumentError, "wrong type for Name attribute" + name = name.to_s + type = attributes.delete("internal_type") or raise ArgumentError, "wrong type for Name attribute" + %[DateTime XMLDateTime Computed Text Guid ContentTypeId URL Integer Counter Attachments ModStat Number Bool File Note User InternalUser UserMulti Choice MultiChoice Lookup LookupMulti ThreadIndex].include?(type) or raise ArgumentError, "illegal value for internal_type attribute" + attributes = type_check_attributes_for_creation(internal_attribute_types, attributes, false).merge("Name" => name, "Type" => type) + untype_cast_attributes(site, nil, internal_attribute_types, attributes, false) + end + private def list_for_lookup @@ -107,7 +117,7 @@ def original_attributes @original_attributes ||= type_cast_attributes(@site, nil, internal_attribute_types, @attributes_before_type_cast.merge("List" => list_for_lookup, "Type" => self.Type, "internal_type" => internal_type)) end - def internal_attribute_types + def self.internal_attribute_types @@internal_attribute_types ||= { "AllowDeletion" => GhostField.new("AllowDeletion", "Bool", false, true), "AppendOnly" => GhostField.new("AppendOnly", "Bool", false, true), @@ -116,23 +126,24 @@ def internal_attribute_types "CanToggleHidden" => GhostField.new("CanToggleHidden", "Bool", false, true), "ClassInfo" => GhostField.new("ClassInfo", "Text", false, true), "ColName" => GhostField.new("ColName", "Text", false, true), - "Description" => GhostField.new("Description", "Text", false, true), + "Description" => GhostField.new("Description", "Text", false, false), "Dir" => GhostField.new("Dir", "Text", false, true), "DisplaceOnUpgrade" => GhostField.new("DisplaceOnUpgrade", "Bool", false, true), "DisplayImage" => GhostField.new("DisplayImage", "Text", false, true), - "DisplayName" => GhostField.new("DisplayName", "Text", false, true), + "DisplayName" => GhostField.new("DisplayName", "Text", false, false), "DisplayNameSrcField" => GhostField.new("DisplayNameSrcField", "Text", false, true), "DisplaySize" => GhostField.new("DisplaySize", "Integer", false, true), "ExceptionImage" => GhostField.new("ExceptionImage", "Text", false, true), "FieldRef" => GhostField.new("FieldRef", "Text", false, true), "FillInChoice" => GhostField.new("FillInChoice", "Bool", false, true), - "Filterable" => GhostField.new("Filterable", "Bool", false, true), - "Format" => GhostField.new("Format", "Bool", false, true), - "FromBaseType" => GhostField.new("FromBaseType", "Bool", false, true), + "Filterable" => GhostField.new("Filterable", "Bool", false, false), + "FilterableNoRecurrence" => GhostField.new("FilterableNoRecurrence", "Bool", false, false), + "Format" => GhostField.new("Format", "Bool", false, false), + "FromBaseType" => GhostField.new("FromBaseType", "Bool", false, false), "Group" => GhostField.new("Group", "Text", false, true), "HeaderImage" => GhostField.new("HeaderImage", "Text", false, true), "Height" => GhostField.new("Height", "Integer", false, true), - "Hidden" => GhostField.new("Hidden", "Bool", false, true), + "Hidden" => GhostField.new("Hidden", "Bool", false, false), "ID" => GhostField.new("ID", "Text", false, true), "IMEMode" => GhostField.new("IMEMode", "Text", false, true), "internal_type" => GhostField.new("internal_type", "Text", false, true), @@ -140,15 +151,15 @@ def internal_attribute_types "JoinColName" => GhostField.new("JoinColName", "Text", false, true), "JoinRowOrdinal" => GhostField.new("JoinRowOrdinal", "Integer", false, true), "JoinType" => GhostField.new("JoinType", "Text", false, true), - "List" => GhostField.new("List", "ListReference", false, true), + "List" => GhostField.new("List", "ListReference", false, false), "Max" => GhostField.new("Max", "Integer", false, true), "MaxLength" => GhostField.new("MaxLength", "Integer", false, true), "Min" => GhostField.new("Min", "Integer", false, true), "Mult" => GhostField.new("Mult", "Bool", false, true), "Name" => GhostField.new("Name", "Text", false, true), - "Node" => GhostField.new("Node", "Text", false, true), + "Node" => GhostField.new("Node", "Text", false, false), "NoEditFormBreak" => GhostField.new("NoEditFormBreak", "Bool", false, true), - "NumLines" => GhostField.new("NumLines", "Text", false, true), + "NumLines" => GhostField.new("NumLines", "Text", false, false), "Percentage" => GhostField.new("Percentage", "Bool", false, true), "PIAttribute" => GhostField.new("PIAttribute", "Text", false, true), "PITarget" => GhostField.new("PITarget", "Text", false, true), @@ -156,20 +167,20 @@ def internal_attribute_types "PrimaryKey" => GhostField.new("PrimaryKey", "Bool", false, true), "PrimaryPIAttribute" => GhostField.new("PrimaryPIAttribute", "Text", false, true), "PrimaryPITarget" => GhostField.new("PrimaryPITarget", "Text", false, true), - "ReadOnly" => GhostField.new("ReadOnly", "Bool", false, true), + "ReadOnly" => GhostField.new("ReadOnly", "Bool", false, false), "ReadOnlyEnforced" => GhostField.new("ReadOnlyEnforced", "Bool", false, true), "RenderXMLUsingPattern" => GhostField.new("ReadOnly", "Bool", false, true), - "Required" => GhostField.new("Required", "Bool", false, true), + "Required" => GhostField.new("Required", "Bool", false, false), "RestrictedMode" => GhostField.new("RestrictedMode", "Bool", false, true), "RichText" => GhostField.new("RichText", "Bool", false, true), "RichTextMode" => GhostField.new("RichTextMode", "Text", false, true), "RowOrdinal" => GhostField.new("RowOrdinal", "Integer", false, true), - "Sealed" => GhostField.new("Sealed", "Bool", false, true), - "ShowInDisplayForm" => GhostField.new("ShowInDisplayForm", "Bool", false, true), - "ShowInListSettings" => GhostField.new("ShowInListSettings", "Bool", false, true), - "ShowInFileDlg" => GhostField.new("ShowInFileDlg", "Bool", false, true), + "Sealed" => GhostField.new("Sealed", "Bool", false, false), + "ShowInDisplayForm" => GhostField.new("ShowInDisplayForm", "Bool", false, false), + "ShowInListSettings" => GhostField.new("ShowInListSettings", "Bool", false, false), + "ShowInFileDlg" => GhostField.new("ShowInFileDlg", "Bool", false, false), "ShowInVersionHistory" => GhostField.new("ShowInVersionHistory", "Bool", false, true), - "Sortable" => GhostField.new("Sortable", "Bool", false, true), + "Sortable" => GhostField.new("Sortable", "Bool", false, false), "SourceID" => GhostField.new("SourceID", "Text", false, true), "StaticName" => GhostField.new("StaticName", "Text", false, true), "StorageTZ" => GhostField.new("StorageTZ", "Bool", false, true), @@ -177,9 +188,9 @@ def internal_attribute_types "Title" => GhostField.new("Title", "Text", false, true), "Type" => GhostField.new("Type", "Text", false, true), "SetAs" => GhostField.new("SetAs", "Text", false, true), - "ShowField" => GhostField.new("ShowField", "Text", false, true), - "ShowInEditForm" => GhostField.new("ShowInEditForm", "Bool", false, true), - "ShowInNewForm" => GhostField.new("ShowInNewForm", "Bool", false, true), + "ShowField" => GhostField.new("ShowField", "Text", false, false), + "ShowInEditForm" => GhostField.new("ShowInEditForm", "Bool", false, false), + "ShowInNewForm" => GhostField.new("ShowInNewForm", "Bool", false, false), "UnlimitedLengthInDocumentLibrary" => GhostField.new("UnlimitedLengthInDocumentLibrary", "Bool", false, true), "Version" => GhostField.new("Version", "Integer", false, true), "Width" => GhostField.new("Width", "Integer", false, true), @@ -188,6 +199,14 @@ def internal_attribute_types } end + def internal_attribute_types + self.class.internal_attribute_types + end + end end + +__END__ + +Reference of attributes for fields: http://msdn.microsoft.com/en-us/library/aa543225.aspx diff --git a/lib/activesp/file.rb b/lib/activesp/file.rb index d1717c5..86196b4 100644 --- a/lib/activesp/file.rb +++ b/lib/activesp/file.rb @@ -54,7 +54,7 @@ def content_size def destroy if @destroyable - result = call("Lists", "delete_attachment", "listName" => @item.list.id, "listItemID" => @item.ID, "url" => @url) + result = call("Lists", "DeleteAttachment", "listName" => @item.list.id, "listItemID" => @item.ID, "url" => @url) if delete_result = result.xpath("//sp:DeleteAttachmentResponse", NS).first @item.clear_cache_for(:attachment_urls) self diff --git a/lib/activesp/folder.rb b/lib/activesp/folder.rb index dec9c9d..f942946 100644 --- a/lib/activesp/folder.rb +++ b/lib/activesp/folder.rb @@ -52,10 +52,17 @@ def each_folder(options = {}, &blk) def create(parameters = {}) @object.create_folder(parameters) end + def create!(parameters = {}) + @object.create_folder!(parameters) + end end def create_folder(parameters = {}) - @list.create_folder(parameters.merge(:folder => absolute_url)) + @list.create_folder(parameters.merge(:folder => absolute_url, :folder_object => self)) + end + + def create_folder!(parameters = {}) + @list.create_folder!(parameters.merge(:folder => absolute_url, :folder_object => self)) end def each_document(options = {}, &blk) @@ -65,12 +72,19 @@ def each_document(options = {}, &blk) def create(parameters = {}) @object.create_document(parameters) end + def create!(parameters = {}) + @object.create_document!(parameters) + end end def create_document(parameters = {}) @list.create_document(parameters.merge(:folder => absolute_url, :folder_object => self)) end + def create_document!(parameters = {}) + @list.create_document!(parameters.merge(:folder => absolute_url, :folder_object => self)) + end + # Returns the item with the given name # @param [String] name # @return [Item] diff --git a/lib/activesp/group.rb b/lib/activesp/group.rb index f4199a4..ba94132 100644 --- a/lib/activesp/group.rb +++ b/lib/activesp/group.rb @@ -47,7 +47,7 @@ def key # Returns the list of users in this group # @return [User] def users - call("UserGroup", "get_user_collection_from_group", "groupName" => @name).xpath("//spdir:User", NS).map do |row| + call("UserGroup", "GetUserCollectionFromGroup", "groupName" => @name).xpath("//spdir:User", NS).map do |row| attributes = clean_attributes(row.attributes) User.new(@site, attributes["LoginName"]) end @@ -64,7 +64,7 @@ def is_role? # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, false) end # @private @@ -78,7 +78,7 @@ def to_s private def data - call("UserGroup", "get_group_info", "groupName" => @name).xpath("//spdir:Group", NS).first + call("UserGroup", "GetGroupInfo", "groupName" => @name).xpath("//spdir:Group", NS).first end cache :data diff --git a/lib/activesp/item.rb b/lib/activesp/item.rb index 0830a13..e969e21 100644 --- a/lib/activesp/item.rb +++ b/lib/activesp/item.rb @@ -113,8 +113,8 @@ def key # @return [Array] def attachment_urls @list.when_list do - result = call("Lists", "get_attachment_collection", "listName" => @list.id, "listItemID" => @id) - return result.xpath("//sp:Attachment", NS).map { |att| att.text } + result = call("Lists", "GetAttachmentCollection", "listName" => @list.id, "listItemID" => @id) + return result.xpath("//sp:Attachment", NS).map { |att| att.text.gsub(/ /, "%20") } end @list.when_document_library { raise TypeError, "a document library does not support attachments" } @list.raise_on_unknown_type @@ -122,7 +122,6 @@ def attachment_urls cache :attachment_urls, :dup => :always # Yields each attachment as a ActiveSP::File object. - # def each_attachment attachment_urls.each { |url| yield ActiveSP::File.new(self, url, true) } end @@ -132,7 +131,7 @@ def add_attachment(parameters = {}) parameters = parameters.dup content = parameters.delete(:content) or raise ArgumentError, "Specify the content in the :content parameter" file_name = parameters.delete(:file_name) or raise ArgumentError, "Specify the file name in the :file_name parameter" - result = call("Lists", "add_attachment", "listName" => @list.ID, "listItemID" => self.ID, "fileName" => file_name, "attachment" => Base64.encode64(content.to_s)) + result = call("Lists", "AddAttachment", "listName" => @list.ID, "listItemID" => self.ID, "fileName" => file_name, "attachment" => Base64.encode64(content.to_s)) add_result = result.xpath("//sp:AddAttachmentResult", NS).first if add_result clear_cache_for(:attachment_urls) @@ -182,20 +181,20 @@ def content_type cache :content_type # def versions - # call("Versions", "get_versions", "fileName" => attributes["ServerUrl"]) + # call("Versions", "GetVersions", "fileName" => attributes["ServerUrl"]) # end # See {Base#save} # @return [self] def save - update_attributes_internal(untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes)) + update_attributes_internal(untype_cast_attributes(@site, nil, @list.fields_by_name, changed_attributes, true)) self end def check_out @list.when_list { raise TypeError, "cannot check out list items; they would disappear" } @list.raise_on_unknown_type - result = call("Lists", "check_out_file", "pageUrl" => absolute_url, "checkoutToLocal" => false) + result = call("Lists", "CheckOutFile", "pageUrl" => absolute_url, "checkoutToLocal" => false) checkout_result = result.xpath("//sp:CheckOutFileResult", NS).first.text if checkout_result == "true" self @@ -215,7 +214,7 @@ def check_in(options = {}) if type == :minor && !@list.attribute("EnableMinorVersion") raise TypeError, "this list does not support minor versions" end - result = call("Lists", "check_in_file", "pageUrl" => absolute_url, "comment" => comment, "CheckinType" => checkin_type) + result = call("Lists", "CheckInFile", "pageUrl" => absolute_url, "comment" => comment, "CheckinType" => checkin_type) checkin_result = result.xpath("//sp:CheckInFileResult", NS).first.text if checkin_result == "true" self @@ -230,7 +229,7 @@ def check_in(options = {}) def cancel_checkout @list.when_list { raise TypeError, "cannot undo check-out for list items because you can't check them out" } @list.raise_on_unknown_type - result = call("Lists", "undo_check_out", "pageUrl" => absolute_url) + result = call("Lists", "UndoCheckOut", "pageUrl" => absolute_url) cancel_result = result.xpath("//sp:UndoCheckOutResult", NS).first.text if cancel_result == "true" self @@ -246,6 +245,13 @@ def update_attributes(attributes) save end + def update_attributes!(attributes) + attributes.each do |k, v| + set_attribute!(k, v) + end + save + end + def destroy updates = Builder::XmlMarkup.new.Batch("OnError" => "Continue", "ListVersion" => 1) do |xml| xml.Method("ID" => 1, "Cmd" => "Delete") do @@ -256,7 +262,7 @@ def destroy end end end - result = call("Lists", "update_list_items", "listName" => @list.id, "updates" => updates) + result = call("Lists", "UpdateListItems", "listName" => @list.id, "updates" => updates) create_result = result.xpath("//sp:Result", NS).first error_code = create_result.xpath("./sp:ErrorCode", NS).first.text.to_i(0) if error_code == 0 @@ -298,6 +304,7 @@ def raw_attributes @list.__each_item(query_options, "query" => query) do |attributes| return attributes end + raise ActiveSP::NotFound, "Not found" end cache :raw_attributes @@ -330,15 +337,19 @@ def update_attributes_internal(attributes) updates = Builder::XmlMarkup.new.Batch("OnError" => "Continue", "ListVersion" => @list.attribute("Version")) do |xml| xml.Method("ID" => 1, "Cmd" => "Update") do xml.Field(self.ID, "Name" => "ID") - construct_xml_for_update_list_items(xml, @list.fields_by_name, attributes) + construct_xml_for_update_list_items(xml, @list, @list.fields_by_name, attributes) if file_ref xml.Field(base_name, "Name" => "BaseName") - xml.Field(file_ref, "Name" => "FileRef") + xml.Field(file_ref, "Name" => "FileRef") + else + @list.when_document_library do + xml.Field(URI.unescape(original_attributes["EncodedAbsUrl"]), "Name" => "FileRef") + end end xml.Field("1", "Name" => "FSObjType") if is_folder? end end - result = call("Lists", "update_list_items", "listName" => @list.id, "updates" => updates) + result = call("Lists", "UpdateListItems", "listName" => @list.id, "updates" => updates) create_result = result.xpath("//sp:Result", NS).first error_code = create_result.xpath("./sp:ErrorCode", NS).first.text.to_i(0) if error_code == 0 @@ -346,9 +357,23 @@ def update_attributes_internal(attributes) @attributes_before_type_cast = clean_item_attributes(row.attributes) reload else - message = create_result.xpath("./sp:ErrorText", NS).first - message &&= message.text - raise "cannot update item, error code = #{error_code}, error description = #{message}" + if error_code == 0x80004005 + raise ActiveSP::AccessDenied, "access denied" + elsif error_code == 0x80070005 + raise ActiveSP::PermissionDenied, "permission denied" + else + message = create_result.xpath("./sp:ErrorText", NS).first + message &&= message.text + raise "cannot update item, error code = #{error_code}, error description = #{message}" + end + end + end + + def attribute_type_internal(name, override_restrictions) + if override_restrictions + @list.fields_by_name[name] + else + attribute_type(name) end end diff --git a/lib/activesp/list.rb b/lib/activesp/list.rb index 16665a4..48a3cd0 100644 --- a/lib/activesp/list.rb +++ b/lib/activesp/list.rb @@ -42,32 +42,36 @@ class List < Base persistent { |site, id, *a| [site.connection, [:list, id]] } # @private def initialize(site, id, title = nil, attributes_before_type_cast1 = nil, attributes_before_type_cast2 = nil) - @site, @id = site, id + @site, @id = site, id.upcase @Title = title if title # This testing for emptiness of RootFolder is necessary because it is empty # in bulk calls. - @attributes_before_type_cast1 = attributes_before_type_cast1 if attributes_before_type_cast1 && attributes_before_type_cast1["RootFolder"] != "" - @attributes_before_type_cast2 = attributes_before_type_cast2 if attributes_before_type_cast2 && attributes_before_type_cast2["RootFolder"] != "" + @attributes_before_type_cast1 = attributes_before_type_cast1 if attributes_before_type_cast1 + @attributes_before_type_cast2 = attributes_before_type_cast2 if attributes_before_type_cast2 + end + + def RootFolder + if attributes_before_type_cast1["RootFolder"] == "" + clear_cache_for("attributes_before_type_cast1") + end + attributes_before_type_cast1["RootFolder"] end # The URL of the list # @return [String] def url - URL(@site.url).join(attributes["RootFolder"]).to_s - # # Dirty. Used to use RootFolder, but if you get the data from the bulk calls, RootFolder is the empty - # # string rather than what it should be. That's what you get with web services as an afterthought I guess. - # view_url = ::File.dirname(attributes["DefaultViewUrl"]) - # result = URL(@site.url).join(view_url).to_s - # if ::File.basename(result) == "Forms" and dir = ::File.dirname(result) and dir.length > @site.url.length - # result = dir - # end - # result + URL(@site.url).join(self.RootFolder).to_s end cache :url # @private - def relative_url - @site.relative_url(url) + def relative_url(site = @site.connection.root) + reference_url = site.url + reference_url += "/" unless reference_url[-1, 1] == "/" + url = self.url + reference_url = reference_url.sub(/\Ahttps?:\/\/[^\/]+/, "") + url = url.sub(/\Ahttps?:\/\/[^\/]+/, "") + url[reference_url.length..-1] end # See {Base#key} @@ -131,6 +135,9 @@ def each_document(parameters = {}, &blk) def create(parameters = {}) @object.create_document(parameters) end + def create!(parameters = {}) + @object.create_document!(parameters) + end end def each_folder(parameters = {}, &blk) @@ -148,6 +155,9 @@ def each_folder(parameters = {}, &blk) def create(parameters = {}) @object.create_folder(parameters) end + def create!(parameters = {}) + @object.create_folder!(parameters) + end end # Returns the item with the given name or nil if there is no item with the given name @@ -176,15 +186,25 @@ def create_document(parameters = {}) raise_on_unknown_type end + def create_document!(parameters = {}) + create_document(parameters.merge(:override_restrictions => true)) + end + def create_folder(parameters = {}) name = parameters.delete("FileLeafRef") or raise ArgumentError, "Specify the folder name in the 'FileLeafRef' parameter" create_list_item(parameters.merge(:folder_name => name)) end + def create_folder!(parameters = {}) + create_folder(parameters.merge(:override_restrictions => true)) + end + def changes_since_token(token, options = {}) options = options.dup no_preload = options.delete(:no_preload) + row_limit = (r_l = options.delete(:row_limit)) ? {'rowLimit' => r_l.to_s} : {} + only_attrs = options.delete(:only_attrs) options.empty? or raise ArgumentError, "unknown options #{options.keys.map { |k| k.inspect }.join(", ")}" if no_preload @@ -195,14 +215,21 @@ def changes_since_token(token, options = {}) view_fields = Builder::XmlMarkup.new.ViewFields end if token - result = call("Lists", "get_list_item_changes_since_token", "listName" => @id, 'queryOptions' => '', 'changeToken' => token, 'viewFields' => view_fields) + result = call("Lists", "GetListItemChangesSinceToken", {"listName" => @id, 'queryOptions' => '', 'changeToken' => token, 'viewFields' => view_fields}.merge(row_limit)) else - result = call("Lists", "get_list_item_changes_since_token", "listName" => @id, 'queryOptions' => '', 'viewFields' => view_fields) + result = call("Lists", "GetListItemChangesSinceToken", {"listName" => @id, 'queryOptions' => '', 'viewFields' => view_fields}.merge(row_limit)) end updates = [] result.xpath("//z:row", NS).each do |row| attributes = clean_item_attributes(row.attributes) - updates << construct_item(:unset, attributes, no_preload ? nil : attributes) + all_attrs = if only_attrs + only_attrs.each_with_object({}) do |a, h| + h[a] = attributes[a] if attributes.has_key?(a) + end + else + attributes + end + updates << construct_item(:unset, attributes, no_preload ? nil : all_attrs) end deletes = [] result.xpath("//sp:Changes/sp:Id", NS).each do |row| @@ -234,14 +261,45 @@ def field(id) fields.find { |f| f.ID == id } end + def create_field(attributes) + # TODO: remove this in time + add_to_view = attributes.delete(:add_to_view) + parameters = ActiveSP::Field.check_attributes_for_creation(@site, attributes) + fields = Builder::XmlMarkup.new.Fields do |xml| + xml.Method({ "ID" => "1" }.merge(add_to_view ? { "AddToView" => add_to_view } : {})) do + xml.Field(parameters) + end + end + result = call("Lists", "UpdateList", "listName" => self.id, "newFields" => fields) + method = result.xpath("//sp:Method", NS).first + if error_text = method.xpath("./sp:ErrorText", NS).first + error_code = method.xpath("./sp:ErrorCode", NS).first.text + raise ArgumentError.new("#{error_code} : #{error_text.text.to_s}") + else + field = method.xpath("./sp:Field", NS).first + attributes = clean_attributes(field.attributes) + result = Field.new(self, attributes["ID"].downcase, attributes["StaticName"], attributes["Type"], @site.field(attributes["ID"].downcase), attributes) + if @fields + @fields << result + clear_cache_for(:fields_by_name) + end + result + end + end + def content_types - result = call("Lists", "get_list_content_types", "listName" => @id) + result = call("Lists", "GetListContentTypes", "listName" => @id) result.xpath("//sp:ContentType", NS).map do |content_type| ContentType.new(@site, self, content_type["ID"], content_type["Name"], content_type["Description"], content_type["Version"], content_type["Group"]) end end cache :content_types, :dup => :always + def content_types_by_name + content_types.inject({}) { |h, t| h[t.Name] = t ; h } + end + cache :content_types_by_name, :dup => :always + # @private def content_type(id) content_types.find { |t| t.id == id } @@ -256,10 +314,18 @@ def permission_set end cache :permission_set + def update_attributes(attributes) + attributes.each do |k, v| + set_attribute(k, v) + end + save + end + # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + update_attributes_internal(untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, true)) + self end # @private @@ -291,10 +357,10 @@ def __each_item(query_options, query) get_list_items("", query_options, query) do |attributes| yield attributes end - rescue Savon::SOAPFault => e + rescue Savon::SOAP::Fault => e # This is where it gets ugly... Apparently there is a limit to the number of columns # you can retrieve with this operation. Joy! - if e.message[/lookup column threshold/] + if /lookup column threshold/ === e.error_string fields = self.fields.map { |f| f.Name } split_factor = 2 begin @@ -318,8 +384,8 @@ def __each_item(query_options, query) end yield attrs end - rescue Savon::SOAPFault => e - if e.message[/lookup column threshold/] + rescue Savon::SOAP::Fault => e + if /lookup column threshold/ === e.error_string split_factor += 1 retry else @@ -335,10 +401,14 @@ def ==(object) ::ActiveSP::List === object && self.ID == object.ID end + def quick_attributes + type_cast_attributes(@site, nil, internal_attribute_types, attributes_before_type_cast1) + end + private def data1 - call("Lists", "get_list", "listName" => @id).xpath("//sp:List", NS).first + call("Lists", "GetList", "listName" => @id).xpath("//sp:List", NS).first end cache :data1 @@ -348,7 +418,7 @@ def attributes_before_type_cast1 cache :attributes_before_type_cast1 def data2 - call("SiteData", "get_list", "strListName" => @id) + call("SiteData", "GetList", "strListName" => @id) end cache :data2 @@ -363,6 +433,7 @@ def attributes_before_type_cast2 cache :attributes_before_type_cast2 def original_attributes + self.RootFolder attrs = attributes_before_type_cast1.merge(attributes_before_type_cast2).merge("BaseType" => attributes_before_type_cast1["BaseType"]) type_cast_attributes(@site, nil, internal_attribute_types, attrs) end @@ -372,7 +443,7 @@ def internal_attribute_types @@internal_attribute_types ||= { "AllowAnonymousAccess" => GhostField.new("AllowAnonymousAccess", "Bool", false, true, "Allow Anonymous Access?"), "AllowDeletion" => GhostField.new("AllowDeletion", "Bool", false, true, "Allow Deletion?"), - "AllowMultiResponses" => GhostField.new("AllowMultiResponses", "Bool", false, true, "Allow Multiple Responses?"), + "AllowMultiResponses" => GhostField.new("AllowMultiResponses", "Bool", false, false, "Allow Multiple Responses?"), "AnonymousPermMask" => GhostField.new("AnonymousPermMask", "Integer", false, true, "Anonymous Permission Mask"), "AnonymousViewListItems" => GhostField.new("AnonymousViewListItems", "Bool", false, true, "Anonymous Can View List Items?"), "Author" => GhostField.new("Author", "InternalUser", false, true), @@ -381,22 +452,22 @@ def internal_attribute_types "Created" => GhostField.new("Created", "StandardDateTime", false, true, "Created"), "DefaultViewUrl" => GhostField.new("DefaultViewUrl", "Text", false, true, "Default View Url"), "Description" => GhostField.new("Description", "Text", false, false), - "Direction" => GhostField.new("Direction", "Text", false, true), + "Direction" => GhostField.new("Direction", "Text", false, false), "DocTemplateUrl" => GhostField.new("DocTemplateUrl", "Text", false, true, "Document Template URL"), "EmailAlias" => GhostField.new("EmailAlias", "Text", false, true, "Email Alias"), "EmailInsertsFolder" => GhostField.new("EmailInsertsFolder", "Text", false, true, "Email Inserts Folder"), - "EnableAssignedToEmail" => GhostField.new("EnableAssignedToEmail", "Bool", false, true, "Enable Assign to Email?"), - "EnableAttachments" => GhostField.new("EnableAttachments", "Bool", false, true, "Enable Attachments?"), + "EnableAssignedToEmail" => GhostField.new("EnableAssignedToEmail", "Bool", false, false, "Enable Assign to Email?"), + "EnableAttachments" => GhostField.new("EnableAttachments", "Bool", false, false, "Enable Attachments?"), "EnableMinorVersion" => GhostField.new("EnableMinorVersion", "Bool", false, true, "Enable Minor Versions?"), - "EnableModeration" => GhostField.new("EnableModeration", "Bool", false, true, "Enable Moderation?"), - "EnableVersioning" => GhostField.new("EnableVersioning", "Bool", false, true, "Enable Versioning?"), + "EnableModeration" => GhostField.new("EnableModeration", "Bool", false, false, "Enable Moderation?"), + "EnableVersioning" => GhostField.new("EnableVersioning", "Bool", false, false, "Enable Versioning?"), "EventSinkAssembly" => GhostField.new("EventSinkAssembly", "Text", false, true, "Event Sink Assembly"), "EventSinkClass" => GhostField.new("EventSinkClass", "Text", false, true, "Event Sink Class"), "EventSinkData" => GhostField.new("EventSinkData", "Text", false, true, "Event Sink Data"), "FeatureId" => GhostField.new("FeatureId", "Text", false, true, "Feature ID"), "Flags" => GhostField.new("Flags", "Integer", false, true), "HasUniqueScopes" => GhostField.new("HasUniqueScopes", "Bool", false, true, "Has Unique Scopes?"), - "Hidden" => GhostField.new("Hidden", "Bool", false, true), + "Hidden" => GhostField.new("Hidden", "Bool", false, false), "ID" => GhostField.new("ID", "Text", false, true), "ImageUrl" => GhostField.new("ImageUrl", "Text", false, true, "Image URL"), "InheritedSecurity" => GhostField.new("InheritedSecurity", "Bool", false, true, "Has Inherited Security?"), @@ -409,9 +480,10 @@ def internal_attribute_types "MajorWithMinorVersionsLimit" => GhostField.new("MajorWithMinorVersionsLimit", "Integer", false, true, "Major With Minor Versions Limit"), "MobileDefaultViewUrl" => GhostField.new("MobileDefaultViewUrl", "Text", false, true, "Mobile Default View URL"), "Modified" => GhostField.new("Modified", "StandardDateTime", false, true), - "MultipleDataList" => GhostField.new("MultipleDataList", "Bool", false, true, "Is Multiple Data List?"), + "MultipleDataList" => GhostField.new("MultipleDataList", "Bool", false, false, "Is Multiple Data List?"), "Name" => GhostField.new("Name", "Text", false, true), - "Ordered" => GhostField.new("Ordered", "Bool", false, true), + "OnQuickLaunch" => GhostField.new("OnQuickLaunch", "Bool", false, false), + "Ordered" => GhostField.new("Ordered", "Bool", false, false), "Permissions" => GhostField.new("Permissions", "Text", false, true), "ReadSecurity" => GhostField.new("ReadSecurity", "Integer", false, true, "Read Security"), "RequireCheckout" => GhostField.new("RequireCheckout", "Bool", false, true, "Requires Checkout?"), @@ -419,9 +491,9 @@ def internal_attribute_types "ScopeId" => GhostField.new("ScopeId", "Text", false, true, "Scope ID"), "SendToLocation" => GhostField.new("SendToLocation", "Text", false, true, "Send To Location"), "ServerTemplate" => GhostField.new("ServerTemplate", "Text", false, true, "Server Template"), - "ShowUser" => GhostField.new("ShowUser", "Bool", false, true, "Shows User?"), + "ShowUser" => GhostField.new("ShowUser", "Bool", false, false, "Shows User?"), "ThumbnailSize" => GhostField.new("ThumbnailSize", "Integer", false, true, "Thumbnail Size"), - "Title" => GhostField.new("Title", "Text", false, true), + "Title" => GhostField.new("Title", "Text", false, false), "ValidSecurityInfo" => GhostField.new("ValidSecurityInfo", "Bool", false, true, "Has Valid Security Info?"), "Version" => GhostField.new("Version", "Integer", false, true), "WebFullUrl" => GhostField.new("WebFullUrl", "Text", false, true, "Full Web URL"), @@ -434,7 +506,7 @@ def internal_attribute_types end def permissions - result = call("Permissions", "get_permission_collection", "objectName" => @id, "objectType" => "List") + result = call("Permissions", "GetPermissionCollection", "objectName" => @id, "objectType" => "List") rootsite = @site.rootsite result.xpath("//spdir:Permission", NS).map do |row| accessor = row["MemberIsUser"][/true/i] ? User.new(rootsite, row["UserLogin"]) : Group.new(rootsite, row["GroupName"]) @@ -443,8 +515,10 @@ def permissions end cache :permissions, :dup => :always - def get_list_items(view_fields, query_options, query) - result = call("Lists", "get_list_items", { "listName" => @id, "viewFields" => view_fields, "queryOptions" => query_options }.merge(query)) + def get_list_items(view_fields, query_options, query, options = {}) + options = options.dup + row_limit = (r_l = options.delete(:row_limit)) ? {'rowLimit' => r_l.to_s} : {} + result = call("Lists", "GetListItems", {"listName" => @id, "viewFields" => view_fields, "queryOptions" => query_options}.merge(query).merge(row_limit)) result.xpath("//z:row", NS).each do |row| yield clean_item_attributes(row.attributes) end @@ -467,17 +541,18 @@ def create_library_document(parameters) folder = parameters.delete(:folder) folder_object = parameters.delete(:folder_object) overwrite = parameters.delete(:overwrite) + override_restrictions = parameters.delete(:override_restrictions) file_name = parameters.delete("FileLeafRef") or raise ArgumentError, "Specify the file name in the 'FileLeafRef' parameter" if !overwrite object = __item(file_name, :folder => folder_object) raise ActiveSP::AlreadyExists.new("document with file name #{file_name.inspect} already exists") { object } if object end destination_urls = Builder::XmlMarkup.new.wsdl(:string, URI.escape(::File.join(folder || url, file_name))) - parameters = type_check_attributes_for_creation(fields_by_name, parameters) - attributes = untype_cast_attributes(@site, self, fields_by_name, parameters) + parameters = type_check_attributes_for_creation(fields_by_name, parameters, override_restrictions) + attributes = untype_cast_attributes(@site, self, fields_by_name, parameters, override_restrictions) fields = construct_xml_for_copy_into_items(fields_by_name, attributes) source_url = escape_xml(file_name) - result = call("Copy", "copy_into_items", "DestinationUrls" => destination_urls, "Stream" => Base64.encode64(content.to_s), "SourceUrl" => source_url, "Fields" => fields) + result = call("Copy", "CopyIntoItems", "DestinationUrls" => destination_urls, "Stream" => Base64.encode64(content.to_s), "SourceUrl" => source_url, "Fields" => fields) copy_result = result.xpath("//sp:CopyResult", NS).first error_code = copy_result["ErrorCode"] if error_code != "Success" @@ -492,21 +567,23 @@ def create_list_item(parameters) folder = parameters.delete(:folder) folder_object = parameters.delete(:folder_object) folder_name = parameters.delete(:folder_name) - parameters = type_check_attributes_for_creation(fields_by_name, parameters) - attributes = untype_cast_attributes(@site, self, fields_by_name, parameters) - updates = Builder::XmlMarkup.new.Batch("OnError" => "Continue", "ListVersion" => 1) do |xml| + override_restrictions = parameters.delete(:override_restrictions) + parameters = type_check_attributes_for_creation(fields_by_name, parameters, override_restrictions) + attributes = untype_cast_attributes(@site, self, fields_by_name, parameters, override_restrictions) + folder_attributes = !folder_name && folder ? { "RootFolder" => folder } : {} + updates = Builder::XmlMarkup.new.Batch(folder_attributes.merge("OnError" => "Continue")) do |xml| xml.Method("ID" => 1, "Cmd" => "New") do xml.Field("New", "Name" => "ID") - construct_xml_for_update_list_items(xml, fields_by_name, attributes) + construct_xml_for_update_list_items(xml, self, fields_by_name, attributes) if folder_name xml.Field(::File.join(folder || url, folder_name), "Name" => "FileRef") xml.Field(1, "Name" => "FSObjType") else - xml.Field(::File.join(folder, Time.now.strftime("%Y%m%d%H%M%S-#{rand(16**3).to_s(16)}")), "Name" => "FileRef") if folder + xml.Field(0, "Name" => "FSObjType") end end end - result = call("Lists", "update_list_items", "listName" => self.id, "updates" => updates) + result = call("Lists", "UpdateListItems", "listName" => self.id, "updates" => updates) create_result = result.xpath("//sp:Result", NS).first error_text = create_result.xpath("./sp:ErrorText", NS).first if !error_text @@ -516,15 +593,21 @@ def create_list_item(parameters) error_code = create_result.xpath("./sp:ErrorCode", NS).first error_code &&= error_code.text.to_s if error_code == "0x8107090d" - raise ActiveSP::AlreadyExists.new(error_text.text.to_s) { item(folder_name) } + raise ActiveSP::AlreadyExists.new(error_text.text.to_s) { (folder_object || self).item(folder_name) } else # Make it look like the error came from soap # Alternatively we could wrap all the soap faults maybe - raise Savon::SOAPFault.new(error_text.text.to_s, error_code) + raise ArgumentError.new(error_text.text.to_s) end end end + def update_attributes_internal(attributes) + properties = Builder::XmlMarkup.new.List(attributes) + call("Lists", "UpdateList", "listName" => self.id, "listProperties" => properties) + reload + end + end end diff --git a/lib/activesp/list_template.rb b/lib/activesp/list_template.rb new file mode 100644 index 0000000..a947d51 --- /dev/null +++ b/lib/activesp/list_template.rb @@ -0,0 +1,84 @@ +# Copyright (c) 2010 XAOP bvba +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +module ActiveSP + + class ListTemplate < Base + + extend Caching + extend PersistentCaching + include Util + + attr_reader :Type + + persistent { |connection, type, *a| [connection, [:template, type]] } + # @private + def initialize(connection, type, attributes_before_type_cast) + @connection, @Type, @attributes_before_type_cast = connection, type, attributes_before_type_cast + end + + def key + encode_key("ML", [@Type]) + end + + # @private + def to_s + "#" + end + + # @private + alias inspect to_s + + private + + def original_attributes + @original_attributes ||= type_cast_attributes(@site, nil, internal_attribute_types, @attributes_before_type_cast) + end + + def internal_attribute_types + @@internal_attribute_types ||= { + "BaseType" => GhostField.new("BaseType", "Text", false, true), + "Description" => GhostField.new("Description", "Text", false, true), + "DisplayName" => GhostField.new("DisplayName", "Text", false, true), + "DocumentTemplate" => GhostField.new("DocumentTemplate", "Integer", false, true), + "DontSaveInTemplate" => GhostField.new("DontSaveInTemplate", "Bool", false, true), + "FeatureId" => GhostField.new("FeatureId", "Text", false, true), + "HiddenList" => GhostField.new("HiddenList", "Bool", false, true), + "Image" => GhostField.new("Image", "Text", false, true), + "FolderCreation" => GhostField.new("FolderCreation", "Bool", false, true), + "Hidden" => GhostField.new("Hidden", "Bool", false, true), + "OnQuickLaunch" => GhostField.new("OnQuickLaunch", "Bool", false, true), + "Name" => GhostField.new("Name", "Text", false, true), + "Sequence" => GhostField.new("Sequence", "Integer", false, true), + "SecurityBits" => GhostField.new("SecurityBits", "Text", false, true), + "Type" => GhostField.new("Type", "Integer", false, true), + "UseRootFolderForNavigation" => GhostField.new("UseRootFolderForNavigation", "Bool", false, true), + "VersioningEnabled" => GhostField.new("VersioningEnabled", "Bool", false, true) + } + end + + end + +end diff --git a/lib/activesp/role.rb b/lib/activesp/role.rb index 81e7cc9..aece959 100644 --- a/lib/activesp/role.rb +++ b/lib/activesp/role.rb @@ -47,7 +47,7 @@ def key # Returns the list of users in this role # @return [User] def users - call("UserGroup", "get_user_collection_from_role", "roleName" => @name).xpath("//spdir:User", NS).map do |row| + call("UserGroup", "GetUserCollectionFromRole", "roleName" => @name).xpath("//spdir:User", NS).map do |row| attributes = clean_attributes(row.attributes) User.new(@site, attributes["LoginName"]) end @@ -57,7 +57,7 @@ def users # Returns the list of groups in this role # @return [Group] def groups - call("UserGroup", "get_group_collection_from_role", "roleName" => @name).xpath("//spdir:Group", NS).map do |row| + call("UserGroup", "GetGroupCollectionFromRole", "roleName" => @name).xpath("//spdir:Group", NS).map do |row| attributes = clean_attributes(row.attributes) Group.new(@site, attributes["Name"]) end @@ -74,7 +74,7 @@ def is_role? # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, false) end # @private @@ -88,7 +88,7 @@ def to_s private def data - call("UserGroup", "get_role_info", "roleName" => @name).xpath("//spdir:Role", NS).first + call("UserGroup", "GetRoleInfo", "roleName" => @name).xpath("//spdir:Role", NS).first end cache :data diff --git a/lib/activesp/root.rb b/lib/activesp/root.rb index 88b1679..edfc421 100644 --- a/lib/activesp/root.rb +++ b/lib/activesp/root.rb @@ -28,8 +28,10 @@ module ActiveSP # @private NS = { "sp" => "http://schemas.microsoft.com/sharepoint/soap/", + "SP" => "http://schemas.microsoft.com/sharepoint/", "z" => "#RowsetSchema", - "spdir" => "http://schemas.microsoft.com/sharepoint/soap/directory/" + "spdir" => "http://schemas.microsoft.com/sharepoint/soap/directory/", + "meet" => "http://schemas.microsoft.com/sharepoint/soap/meetings/" } module Root @@ -46,9 +48,9 @@ def root # Returns the list of users in the system # @return [Array] def users - root.send(:call, "UserGroup", "get_user_collection_from_site").xpath("//spdir:User", NS).map do |row| + root.send(:call, "UserGroup", "GetUserCollectionFromSite").xpath("//spdir:User", NS).map do |row| attributes = clean_attributes(row.attributes) - User.new(root, attributes["LoginName"]) + User.new(root, attributes["LoginName"], attributes) end end cache :users, :dup => :always @@ -90,6 +92,30 @@ def roles end cache :roles, :dup => :always + def site_templates + root.send(:call, "Sites", "GetSiteTemplates", "LCID" => 1033).xpath("//sp:Template", NS).map do |row| + attributes = clean_attributes(row.attributes) + ActiveSP::SiteTemplate.new(root, attributes["Name"], attributes) + end + end + cache :site_templates, :dup => :always + + def site_template(name) + site_templates.find { |template| template.Name == name } + end + + def list_templates + result = root.send(:call, "Webs", "GetListTemplates") + result.xpath("//SP:ListTemplate", NS).map do |row| + attributes = clean_attributes(row.attributes) + ActiveSP::ListTemplate.new(root, attributes["Type"].to_i, attributes) + end + end + + def list_template(type) + list_templates.find { |template| template.Type == type } + end + private def users_by_login diff --git a/lib/activesp/site.rb b/lib/activesp/site.rb index 7b7c05e..60cc17d 100644 --- a/lib/activesp/site.rb +++ b/lib/activesp/site.rb @@ -33,7 +33,7 @@ class Site < Base # The URL of this site # @return [String] - attr_reader :url + attr_reader :url # TODO: deprecate this in favor of Url # @private attr_reader :connection @@ -44,9 +44,12 @@ def initialize(connection, url, depth = 0) @services = {} end - # @private - def relative_url(url = @url) - url[@connection.root_url.rindex("/") + 1..-1] + def Url + @url + end + + def Name + ::File.basename(@url) end # Returns the containing site, or nil if this is the root site @@ -74,46 +77,50 @@ def is_root_site? # See {Base#key} # @return [String] def key # This documentation is not ideal. The ideal doesn't work out of the box - encode_key("S", [@url[@connection.root_url.length + 1..-1], @depth]) + encode_key("S", [@url[@connection.root_url.sub(/\/\z/, "").length + 1..-1], @depth]) end - # Returns the list of sites below this site. Does not recurse - # @return [Array] - def sites - result = call("Webs", "get_web_collection") - result.xpath("//sp:Web", NS).map { |web| Site.new(connection, web["Url"].to_s, @depth + 1) } + def each_site(&blk) + __sites.each(&blk) + end + association :sites do + def create(attributes) + @object.create_site(attributes) + end end - cache :sites, :dup => :always # Returns the site with the given name. This name is what appears in the URL as name and is immutable. Return nil # if such a site does not exist # @param [String] name The name if the site # @return [Site] def site(name) - result = call("Webs", "get_web", "webUrl" => ::File.join(@url, name)) + result = call("Webs", "GetWeb", "webUrl" => ::File.join(@url, name)) Site.new(connection, result.xpath("//sp:Web", NS).first["Url"].to_s, @depth + 1) - rescue Savon::SOAPFault + rescue Savon::SOAP::Fault nil end - # Returns the list if lists in this sute. Does not recurse - # @return [Array] - def lists - result1 = call("Lists", "get_list_collection") - result2 = call("SiteData", "get_list_collection") - result2_by_id = {} - result2.xpath("//sp:_sList", NS).each do |element| - data = {} - element.children.each do |ch| - data[ch.name] = ch.inner_text - end - result2_by_id[data["InternalName"]] = data - end - result1.xpath("//sp:List", NS).select { |list| list["Title"] != "User Information List" }.map do |list| - List.new(self, list["ID"].to_s, list["Title"].to_s, clean_attributes(list.attributes), result2_by_id[list["ID"].to_s]) + def create_site(attributes) + template = attributes.delete("Template") + ActiveSP::SiteTemplate === template or raise ArgumentError, "wrong type for Template attribute" + title = attributes.delete("Title") + title or raise ArgumentError, "wrong type for Title attribute" + title = title.to_s + lcid = attributes.delete("Language") + Integer === lcid or raise ArgumentError, "wrong type for Language attribute" + parameters = type_check_attributes_for_creation(fields_by_name, attributes, false) + result = call("Meetings", "CreateWorkspace", "title" => title, "templateName" => template.Name, "lcid" => lcid) + Site.new(connection, result.xpath("//meet:CreateWorkspace", NS).first["Url"].to_s, @depth + 1) + end + + def each_list(&blk) + __lists.each(&blk) + end + association :lists do + def create(attributes) + @object.create_list(attributes) end end - cache :lists, :dup => :always # Returns the list with the given name. The name is what appears in the URL as name and is immutable. Returns nil # if such a list does not exist @@ -123,6 +130,19 @@ def list(name) lists.find { |list| ::File.basename(list.url) == name } end + def create_list(attributes) + template = attributes.delete("ServerTemplate") + ActiveSP::ListTemplate === template or raise ArgumentError, "wrong type for ServerTemplate attribute" + title = attributes.delete("Title") + title or raise ArgumentError, "wrong type for Title attribute" + title = title.to_s + description = attributes.delete("Description").to_s + parameters = type_check_attributes_for_creation(fields_by_name, attributes, false) + result = call("Lists", "AddList", "listName" => title, "description" => description, "templateID" => template.Type) + list = result.xpath("//sp:List", NS).first + List.new(self, list["ID"].to_s, list["Title"].to_s, clean_attributes(list.attributes)) + end + # Returns the site or list with the given name, or nil if it does not exist # @param [String] name The name of the site or list # @return [Site, List] @@ -134,13 +154,18 @@ def /(name) # containing sites as they are automatically inherited # @return [Array] def content_types - result = call("Webs", "get_content_types", "listName" => @id) + result = call("Webs", "GetContentTypes", "listName" => @id) result.xpath("//sp:ContentType", NS).map do |content_type| supersite && supersite.content_type(content_type["ID"]) || ContentType.new(self, nil, content_type["ID"], content_type["Name"], content_type["Description"], content_type["Version"], content_type["Group"]) end end cache :content_types, :dup => :always + def content_types_by_name + content_types.inject({}) { |h, t| h[t.Name] = t ; h } + end + cache :content_types_by_name, :dup => :always + # @private def content_type(id) content_types.find { |t| t.id == id } @@ -161,7 +186,7 @@ def permission_set # Returns the list of fields for this site. This includes fields inherited from containing sites # @return [Array] def fields - call("Webs", "get_columns").xpath("//sp:Field", NS).map do |field| + call("Webs", "GetColumns").xpath("//sp:Field", NS).map do |field| attributes = clean_attributes(field.attributes) supersite && supersite.field(attributes["ID"].downcase) || Field.new(self, attributes["ID"].downcase, attributes["StaticName"], attributes["Type"], nil, attributes) if attributes["ID"] && attributes["StaticName"] end.compact @@ -180,19 +205,33 @@ def field(id) fields.find { |f| f.ID == id } end + def update_attributes(attributes) + attributes.each do |k, v| + set_attribute(k, v) + end + save + end + # See {Base#save} # @return [void] def save - p untype_cast_attributes(self, nil, internal_attribute_types, changed_attributes) + update_attributes_internal(untype_cast_attributes(self, nil, internal_attribute_types, changed_attributes, false)) + self end def accessible? data true - rescue Savon::HTTPError + rescue Savon::HTTP::Error false end + def destroy + call("Dws", "DeleteDws") + supersite.__unregister_site(self) + self + end + # @private def to_s "#" @@ -201,6 +240,19 @@ def to_s # @private alias inspect to_s + #private + def __unregister_site(site) + p [:__unregister_site, self] + @__sites.delete(site) if @__sites + end + + def quick_attributes + { + "Name" => self.Name, + "Url" => self.Url + } + end + private def call(service, m, *args, &blk) @@ -218,10 +270,10 @@ def service(name) def data # Looks like you can't call this as a non-admin. To investigate further - call("SiteData", "get_web") - rescue Savon::HTTPError + call("SiteData", "GetWeb") + rescue Savon::HTTP::Error # This can fail when you don't have access to this site - call("Webs", "get_web", "webUrl" => ".") + call("Webs", "GetWeb", "webUrl" => ".") end cache :data @@ -231,10 +283,10 @@ def attributes_before_type_cast element.children.each do |ch| result[ch.name] = ch.inner_text end - result + result.merge("Url" => @url, "Name" => self.Name) else element = data.xpath("//sp:Web", NS).first - clean_attributes(element.attributes) + clean_attributes(element.attributes).merge("Url" => @url, "Name" => self.Name) end end cache :attributes_before_type_cast @@ -256,8 +308,10 @@ def internal_attribute_types "Language" => GhostField.new("Language", "Integer", false, true), "LastModified" => GhostField.new("LastModified", "XMLDateTime", false, true, "Modified"), "LastModifiedForceRecrawl" => GhostField.new("LastModifiedForceRecrawl", "XMLDateTime", false, true, "Last Modified Force Recrawl"), + "Name" => GhostField.new("Name", "Text", false, true), "Permissions" => GhostField.new("Permissions", "Text", false, true), - "Title" => GhostField.new("Title", "Text", false, true), + "Title" => GhostField.new("Title", "Text", false, false), + "Url" => GhostField.new("Url", "Text", false, true), "UsedInAutocat" => GhostField.new("UsedInAutocat", "Bool", false, true, "Used in Autocat?"), "ValidSecurityInfo" => GhostField.new("ValidSecurityInfo", "Bool", false, true, "Has Valid Security Info?"), "WebID" => GhostField.new("WebID", "Text", false, true, "Web ID") @@ -265,7 +319,7 @@ def internal_attribute_types end def permissions - result = call("Permissions", "get_permission_collection", "objectName" => ::File.basename(@url), "objectType" => "Web") + result = call("Permissions", "GetPermissionCollection", "objectName" => ::File.basename(@url), "objectType" => "Web") result.xpath("//spdir:Permission", NS).map do |row| accessor = row["MemberIsUser"][/true/i] ? User.new(rootsite, row["UserLogin"]) : Group.new(rootsite, row["GroupName"]) { :mask => Integer(row["Mask"]), :accessor => accessor } @@ -273,20 +327,58 @@ def permissions end cache :permissions, :dup => :always + def update_attributes_internal(attributes) + call("Dws", "RenameDws", "title" => attributes["Title"]) + reload + end + + def __sites + result = call("Webs", "GetWebCollection") + result.xpath("//sp:Web", NS).map { |web| Site.new(connection, web["Url"].to_s, @depth + 1) } + end + cache :__sites, :dup => :always + + def __lists + result1 = call("Lists", "GetListCollection") + result2 = call("SiteData", "GetListCollection") + result2_by_id = {} + result2.xpath("//sp:_sList", NS).each do |element| + data = {} + element.children.each do |ch| + data[ch.name] = ch.inner_text + end + result2_by_id[data["InternalName"]] = data + end + result1.xpath("//sp:List", NS).select { |list| list["Title"] != "User Information List" }.map do |list| + List.new(self, list["ID"].to_s, list["Title"].to_s, clean_attributes(list.attributes), result2_by_id[list["ID"].to_s]) + end + end + cache :__lists, :dup => :always + # @private class Service def initialize(site, name) @site, @name = site, name - @client = Savon::Client.new(::File.join(URI.escape(site.url), "_vti_bin", name + ".asmx?WSDL")) - if site.connection.login - case site.connection.auth_type - when :ntlm - @client.request.ntlm_auth(site.connection.login, site.connection.password) - when :basic - @client.request.basic_auth(site.connection.login, site.connection.password) - else - raise ArgumentError, "Unknown authentication type #{site.connection.auth_type.inspect}" + @client = Savon::Client.new do |wsdl, http| + wsdl.document = ::File.join(URI.escape(site.url), "_vti_bin", name + ".asmx?WSDL") + if site.connection.login + auth_type = site.connection.auth_type + login = site.connection.login + password = site.connection.password + wsdl.authenticate(:method => auth_type, :usename => login, :password => password) + case auth_type + when :ntlm + http.auth.ntlm(login, password) + when :basic + http.auth.basic(login, password) + when :digest + http.auth.digest(login, password) + when :gss_negotiate + http.auth.gssnegotiate(login, password) + else + raise ArgumentError, "Unknown authentication type #{site.connection.auth_type.inspect}" + end end end end @@ -296,15 +388,19 @@ def call(m, *args) if Hash === args[-1] body = args.pop end - @client.send(m, *args) do |soap| + @client.request(:wsdl, m.snakecase, *args) do |soap| if body - soap.body = body.inject({}) { |h, (k, v)| h["wsdl:#{k}"] = v ; h } + soap.body = body.map do |k, v| + Builder::XmlMarkup.new.wsdl(k.to_sym) { |e| e << v.to_s } + end.join end yield soap if block_given? end - rescue Savon::SOAPFault => e + rescue Savon::SOAP::Fault => e if e.error_code == 0x80004005 - raise AccessDenied, "access denied" + raise ActiveSP::AccessDenied, "access denied" + elsif e.error_code == 0x80070005 + raise ActiveSP::PermissionDenied, "permission denied" else raise e end diff --git a/lib/activesp/site_template.rb b/lib/activesp/site_template.rb new file mode 100644 index 0000000..de2f533 --- /dev/null +++ b/lib/activesp/site_template.rb @@ -0,0 +1,79 @@ +# Copyright (c) 2010 XAOP bvba +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +module ActiveSP + + class SiteTemplate < Base + + extend Caching + extend PersistentCaching + include Util + + attr_reader :Name + + persistent { |connection, name, *a| [connection, [:template, name]] } + # @private + def initialize(connection, name, attributes_before_type_cast) + @connection, @Name, @attributes_before_type_cast = connection, name, attributes_before_type_cast + end + + def key + encode_key("MS", [@Name]) + end + + # @private + def to_s + "#" + end + + # @private + alias inspect to_s + + private + + def original_attributes + @original_attributes ||= type_cast_attributes(@site, nil, internal_attribute_types, @attributes_before_type_cast) + end + + def internal_attribute_types + @@internal_attribute_types ||= { + "Description" => GhostField.new("Description", "Text", false, true), + "DisplayCategory" => GhostField.new("DisplayCategory", "Text", false, true), + "HasProvisionClass" => GhostField.new("HasProvisionClass", "Bool", false, true), + "ID" => GhostField.new("ID", "Integer", false, true), + "ImageUrl" => GhostField.new("ImageUrl", "Text", false, true), + "IsCustom" => GhostField.new("IsCustom", "Bool", false, true), + "IsHidden" => GhostField.new("IsHidden", "Bool", false, true), + "IsRootWebOnly" => GhostField.new("IsRootWebOnly", "Bool", false, true), + "IsSubWebOnly" => GhostField.new("IsSubWebOnly", "Bool", false, true), + "IsUnique" => GhostField.new("IsUnique", "Bool", false, true), + "Name" => GhostField.new("Name", "Text", false, true), + "Title" => GhostField.new("Title", "Text", false, true) + } + end + + end + +end diff --git a/lib/activesp/user.rb b/lib/activesp/user.rb index 0e6e251..bb9a498 100644 --- a/lib/activesp/user.rb +++ b/lib/activesp/user.rb @@ -51,7 +51,7 @@ def key # See {Base#save} # @return [void] def save - p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes) + p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes, false) end # @private @@ -65,7 +65,7 @@ def to_s private def data - call("UserGroup", "get_user_info", "userLoginName" => @login_name).xpath("//spdir:User", NS).first + call("UserGroup", "GetUserInfo", "userLoginName" => @login_name).xpath("//spdir:User", NS).first end cache :data diff --git a/lib/activesp/user_group_proxy.rb b/lib/activesp/user_group_proxy.rb new file mode 100644 index 0000000..381b131 --- /dev/null +++ b/lib/activesp/user_group_proxy.rb @@ -0,0 +1,44 @@ +# Copyright (c) 2010 XAOP bvba +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +module ActiveSP + + class UserGroupProxy + + def initialize(blk) + @blk = blk + end + + def __user_group + @__user_group ||= @blk.call + end + + def method_missing(m, *a, &b) + __user_group.send(m, *a, &b) + end + + end + +end diff --git a/lib/activesp/util.rb b/lib/activesp/util.rb index 14c95f3..89618a0 100644 --- a/lib/activesp/util.rb +++ b/lib/activesp/util.rb @@ -58,7 +58,7 @@ def type_cast_attributes(site, list, fields, attributes) else v = Time.xmlschema(v.sub(/ /, "T")) end - when "Computed", "Text", "Guid", "ContentTypeId", "URL" + when "Computed", "Text", "Guid", "ContentTypeId", "URL", "Calculated" when "Integer", "Counter", "Attachments" v = v && v != "" ? Integer(v) : nil when "ModStat" # 0 @@ -79,7 +79,7 @@ def type_cast_attributes(site, list, fields, attributes) v = create_user_or_group_by_name(site, v) when "UserMulti" d = split_multi(v) - v = (0...(d.length / 4)).map { |i| create_user_or_group(site, d[4 * i + 2]) } + v = (0...(d.length / 4)).map { |i| create_user_or_group_by_name(site, d[4 * i + 2]) } when "Choice" # For some reason there is no encoding here @@ -101,9 +101,18 @@ def type_cast_attributes(site, list, fields, attributes) else v = (0...(d.length / 4)).map { |i| d[4 * i + 2] } end - + when "TaxonomyFieldType" + d = split_multi(v) + # TODO: lookup translated values in metadata store? + v = d[2] + when "TaxonomyFieldTypeMulti" + d = split_multi(v) + # TODO: lookup translated values in metadata store? + v = (0...(d.length / 4)).map { |i| d[4 * i + 2] } + when "ThreadIndex" + else - # raise NotImplementedError, "don't know type #{field.type.inspect} for #{k}=#{v.inspect}" + # raise NotImplementedError, "don't know type #{field.internal_type.inspect} for #{k}=#{v.inspect}" # Note: can't print self if it needs the attributes to be loaded, so just display the class # warn "don't know type #{field.internal_type.inspect} for #{k}=#{v.inspect} on #{self.class}" end @@ -121,7 +130,14 @@ def type_cast_attributes(site, list, fields, attributes) result end + # TODO: check if this is still needed def create_user_or_group(site, entry) + with_user_proxy(site) do + create_user_or_group_no_proxy(site, entry) + end + end + + def create_user_or_group_no_proxy(site, entry) if entry[/\\/] User.new(site.connection.root, entry) else @@ -130,26 +146,46 @@ def create_user_or_group(site, entry) end def create_user_or_group_by_name(site, name) + with_user_proxy(site) do + create_user_or_group_by_name_no_proxy(site, name) + end + end + + def create_user_or_group_by_name_no_proxy(site, name) if /\A\d+\z/ === name - create_user_or_group_by_id(site, name) + create_user_or_group_by_id_no_proxy(site, name) else if user = site.connection.users.find { |u| u.attribute("Name") === name } - User.new(site.connection.root, user.attribute("LoginName")) - elsif group = site.connection.groups.find { |g| g.attribute("Name") === name } - Group.new(site.connection.root, name) + user + elsif group = site.connection.group(name) + group end end end def create_user_or_group_by_id(site, id) + with_user_proxy(site) do + create_user_or_group_by_id_no_proxy(site, id) + end + end + + def create_user_or_group_by_id_no_proxy(site, id) if user = site.connection.users.find { |u| u.attribute("ID") === id } - User.new(site.connection.root, user.attribute("LoginName")) + user elsif group = site.connection.groups.find { |g| g.attribute("ID") === id } - Group.new(site.connection.root, group.attribute("Name")) + group + end + end + + def with_user_proxy(site, &blk) + if site.connection.user_group_proxy + ::ActiveSP::UserGroupProxy.new(blk) + else + blk.call end end - def type_check_attribute(field, value) + def type_check_attribute(field, value, override_restrictions) case field.internal_type when "Text", "File", "Note", "URL", "Choice" value.to_s @@ -168,6 +204,8 @@ def type_check_attribute(field, value) if field.List if ::ActiveSP::Item === value && value.list == field.List value + elsif nil == value + nil else raise ArgumentError, "wrong type for #{field.Name} attribute" end @@ -218,16 +256,43 @@ def type_check_attribute(field, value) end when "ContentTypeId" value + when "ThreadIndex" + if /\A0x([A-F0-9]+)\z/ === value + value + else + raise ArgumentError, "wrong value for #{field.Name} attribute" + end + when "Computed" + # ContentType is Computed in SP 2011 + if override_restrictions || field.Name == "ContentType" + value.to_s + else + raise "not yet #{field.Name}:#{field.internal_type}" + end + when "ListReference" + ActiveSP::List === value and value or raise ArgumentError, "wrong type for #{field.Name} attribute" + when "TaxonomyFieldType" + if value + d = split_multi(value) + # TODO: lookup translated values in metadata store? + d[2] + end + when "TaxonomyFieldTypeMulti" + if value + d = split_multi(value) + # TODO: lookup translated values in metadata store? + (0...(d.length / 4)).map { |i| d[4 * i + 2] } + end else raise "not yet #{field.Name}:#{field.internal_type}" end end - def type_check_attributes_for_creation(fields, attributes) + def type_check_attributes_for_creation(fields, attributes, override_restrictions) attributes.inject({}) do |h, (k, v)| if field = fields[k] - if !field.ReadOnly || field.Name == "ContentType" - h[k] = type_check_attribute(field, v) + if override_restrictions || !field.ReadOnly || field.Name == "ContentType" + h[k] = type_check_attribute(field, v, override_restrictions) h else raise ArgumentError, "field #{field.Name} is read-only" @@ -238,7 +303,7 @@ def type_check_attributes_for_creation(fields, attributes) end end - def untype_cast_attributes(site, list, fields, attributes) + def untype_cast_attributes(site, list, fields, attributes, override_restrictions) attributes.inject({}) do |h, (k, v)| if field = fields[k] case field.internal_type @@ -262,6 +327,27 @@ def untype_cast_attributes(site, list, fields, attributes) when "LookupMulti" v = v.map { |i| i.ID }.join(";#;#") when "ContentTypeId" + when "ThreadIndex" + when "Computed" + if override_restrictions || k == "ContentType" + v = v.to_s + else + raise "don't know type #{field.internal_type.inspect} for #{k}=#{v.inspect} on self" + end + when "ListReference" + v = v.ID + when "TaxonomyFieldType" + if v + d = split_multi(v) + # TODO: lookup translated values in metadata store? + v =d[2] + end + when "TaxonomyFieldTypeMulti" + if v + d = split_multi(v) + # TODO: lookup translated values in metadata store? + v = (0...(d.length / 4)).map { |i| d[4 * i + 2] } + end else raise "don't know type #{field.internal_type.inspect} for #{k}=#{v.inspect} on self" end @@ -287,10 +373,16 @@ def construct_xml_for_copy_into_items(fields, attributes) end.join("") end - def construct_xml_for_update_list_items(xml, fields, attributes) + def construct_xml_for_update_list_items(xml, list, fields, attributes) attributes.map do |k, v| field = fields[k] - xml.Field(v, "Name" => field.StaticName) + if field.StaticName == "ContentType" + type = list.content_types_by_name[v] + xml.Field(v, "Name" => field.StaticName) + xml.Field(type.ID, "Name" => "ContentTypeId") + else + xml.Field(v, "Name" => field.StaticName) + end end end diff --git a/lib/activesp/wasabi_authentication.rb b/lib/activesp/wasabi_authentication.rb new file mode 100644 index 0000000..ad9d5b2 --- /dev/null +++ b/lib/activesp/wasabi_authentication.rb @@ -0,0 +1,19 @@ +module Savon + module Wasabi + class Document + def authenticate(options) + @request ||= HTTPI::Request.new + case options[:method] + when :ntlm + @request.auth.ntlm(options[:usename], options[:password]) + when :basic + @request.auth.basic(options[:usename], options[:password]) + when :digest + @request.auth.digest(options[:usename], options[:password]) + when :gss_negotiate + @request.auth.gssnegotiate(options[:usename], options[:password]) + end + end + end + end +end