require "json" require 'openssl' module RailsConnector # The CMS file class # # [children] an Array of objects, Obj, of which this one is the parent # [parent] the Obj of which this one is a child - nil for the root object # @api public class BasicObj < CmsBaseModel CRYPT_KEY = "\xd7\x28\x9c\x63\xd6\x29\xdf\x20\xcd\x32\xcf\x30\xcf\x30\xcf\x30\xdf\x20\xb6\x49"\ "\x91\x6e\x99\x66\x90\x6f\x8f\x70\x9e\x61\x8d\x72\x94\x6b\xdf\x20\xbe\x41\xb8\x47\xc4\x3b"\ "\xdf\x20\xbd\x42\x9a\x65\x8d\x72\x93\x6c\x96\x69\x91\x6e".freeze include DateAttribute self.store_full_sti_class = false def self.configure_for_content(which) case which when :released then configure_column_information("objs", true) when :edited configure_column_information("preview_objs", false) has_many(:arel_permissions, :class_name => "::RailsConnector::Permission", :foreign_key => "object_id") else raise "configure_for_content called with unknown parameter #{which}" end end def self.configure_column_information(table_name_postfix, use_cached_permissions) reset_column_information self.table_name = "#{table_name_prefix}#{table_name_postfix}" self.primary_key = "obj_id" self.inheritance_column = "obj_class" @@use_cached_permissions = use_cached_permissions end # use released contents as a default configure_for_content(:released) # Patch to avoid a type_condition being added by ActiveRecord::Base.add_conditions! for Obj.find(params): def self.descends_from_active_record? self == BasicObj || superclass == BasicObj end # A CMS administrator can specify the obj_class for a given CMS object. # In Rails, this could be either: # # * A valid and existing model name # * A valid and non-existing model name # * An invalid model name # # Rails' STI mechanism only considers the first case. # In any other case, RailsConnector::Obj is used, except when explicitely asked # for a model in the RailsConnector namespace (RailsConnector::Permission etc.) def self.compute_type(type_name) try_type { type_name.constantize } || ::Obj end def self.reset_type_cache # We don't cache types at all here. end # @api public def permissions @@use_cached_permissions ? attr_dict.permissions : arel_permissions end # @api public def permitted_for_user?(user) if permitted_groups.blank? true else if user (permitted_groups & user.live_server_groups).any? else false end end end # Returns the root Obj. Its id is 2001 and cannot be changed. # @api public def self.root Obj.find(2001) end # Returns the homepage object. This can be overwritten in your application's +Obj+. # Use Obj#homepage? to check if an object is the homepage. # @api public def self.homepage root end # for testing purposes only def self.reset_homepage @@homepage_id = nil end # returns the obj's permalink. # @api public def permalink self[:permalink] end # This method determines the controller that should be invoked when the Obj is requested. # By default a controller matching the Obj's obj_class will be used. # If the controller does not exist, the CmsController will be used as a fallback. # Overwrite this method to force a different controller to be used. # @api public def controller_name obj_class end # This method determines the action that should be invoked when the Obj is requested. # The default action is 'index'. # Overwrite this method to force a different action to be used. # @api public def controller_action_name "index" end @@homepage_id = nil # Returns true if the current object has the same id as the homepage object. Always use this method instead of # manually comparing an object to Obj.homepage, as Obj#homepage? caches the object id # and thus requires no extra database access. # @api public def homepage? self.id == (@@homepage_id ||= self.class.homepage.id) end # This method is used to calculate a part of a URL of an obj. # # The routing schema: / # # The default is +obj.name+. # # You can customize this part by overwriting +obj.slug+ in {Obj}. # @return [String] # @api public def slug name end # Returns the title of the content or the name. # @api public def display_title self.title || name end OBJECT_TYPES = { '2' => :document, '5' => :publication, 'B' => :image, 'C' => :generic }.freeze unless defined?(OBJECT_TYPES) # Returns the type of the object: :document, :publication, :image or :generic # @api public def object_type OBJECT_TYPES[obj_type_code] end # Returns true if image? or generic? # @api public def binary? [:image, :generic].include? object_type end # Returns true if object_type == :image # @api public def image? object_type == :image end # Returns true if object_type == :generic # @api public def generic? object_type == :generic end # Returns true if object_type == :publication (for folders) # @api public def publication? object_type == :publication end # Returns true if object_type == :document # @api public def document? object_type == :document end # Returns true if this object is active. # @api public def active? return false if !valid_from valid_from <= Time.now && (!valid_until || Time.now <= valid_until) end # Returns true if this object has edited content. # Note that edited content is not available when Configuration.mode == :live # and edited? will always return false in this case. # @api public def edited? (is_edited == 1) end # Returns true if this object has released content # @api public def released? (is_released == 1) end # Returns true if the Obj is suppressed. # A suppressed Obj does not represent an entire web page, but only a part of a page # (for example a teaser) and will not be delivered by the rails application # as a standalone web page. # @api public def suppressed? suppress_export == 1 end # Returns true if the export of the object is not suppressed and the content is active? # @api public def exportable? !suppressed? && active? end # Returns the file name to which the Content.file_extension has been appended. # @api public def filename extension = ".#{file_extension}" unless file_extension.blank? "#{name}#{extension}" rescue NoMethodError name end # Returns an array with the names of groups that are permitted to access this Obj. # This corresponds to the cms permission "permissionLiveServerRead". # @api public def permitted_groups attr_dict.permitted_groups end # Returns true if this object is the root object. # @api public def root? parent_obj_id.nil? end has_many :children, :class_name => 'Obj', :foreign_key => 'parent_obj_id' belongs_to :parent, :class_name => 'Obj', :foreign_key => 'parent_obj_id' # Returns a list of exportable? children excluding the binary? ones unless :all is specfied. # This is mainly used for navigations. # @api public def toclist(*args) return [] unless publication? toclist = children.to_a.select{ |toc| toc.exportable? } toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end # Returns the sorted +toclist+, respecting sort order and type of this Obj. # @api public def sorted_toclist(*args) list = self.toclist(*args) return [] if list.blank? cached_sort_key1 = self.sort_key1 cached_sort_type1 = self.sort_type1 sorted_list = if cached_sort_key1.blank? list.sort { |left_obj, right_obj| left_obj.name <=> right_obj.name } else cached_sort_key2 = self.sort_key2 cached_sort_type2 = self.sort_type2 cached_sort_key3 = self.sort_key3 cached_sort_type3 = self.sort_type3 list.sort do |left_obj, right_obj| compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key1, cached_sort_type1) if compare == 0 && cached_sort_key2 compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key2, cached_sort_type2) if compare == 0 && cached_sort_key3 compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key3, cached_sort_type3) end end compare end end return self.sort_order == "descending" ? sorted_list.reverse : sorted_list end def sort_key1 self[:sort_key1] end def sort_key2 self[:sort_key2] end def sort_key3 self[:sort_key3] end # Returns an Array of all the ancestor objects, starting at the root and ending at this object's parent. # @api public def ancestors if parent parent.ancestors + [parent] else [] end end # Returns the Object with the given name next in the hierarchy # returns nil if no object with the given name was found. # @api public def find_nearest(name) obj = self.children.find_by_name(name) return obj if obj and obj.active? parent.find_nearest(name) unless self.root? end # Returns the value of the attribute specified by its name. # # Passing an invalid key will not raise an error, but return nil. # @api public def [](key) # convenience access to name return name if key.to_sym == :name # regular activerecord attributes if @attributes.key?(key.to_s) if key == :valid_from or key == :valid_until or key == :last_changed return as_date(super(key)) else return super(key) end end # Unknown Obj attributes are delegated to the corresponding instance of AttrDict. begin return (attr_dict.send key) rescue NoMethodError end # fallback return nil end # Reloads the attributes of this object from the database, invalidates custom attributes # @api public def reload super @attr_dict = nil @attr_values = nil @attr_defs = nil self end # object_name is a legacy alias to name. Please use name instead, because only name # works with ActiveRecord's dynamic finder methods like find_by_name(...) def object_name name end # object_class is a legacy alias to name. Please use obj_class instead, because only obj_class # works with ActiveRecord's dynamic finder methods like find_by_obj_class(...) # @api public def object_class obj_class end # for binary Objs body_length equals the file size # for non-binary Objs body_length equals the number of characters in the body (main content) # @api public def body_length attr_dict.body_length end # Override this method to provide an external url # where the content (the body) of this Obj can be downloaded. # The Rails Connector will then use this url when creating links to this Obj. # This is useful when delivering images via a content delivery network (CDN), for example. # Returns nil by default. # # Note: When this method returns an url, the Rails Connector's DefaultCmsController # will redirect to this url instead of delivering this Obj's body. # And the Rails Connector's cms_path and cms_url helpers will link to this url # instead of linking to the Obj. # # Note also that the url will be used regardless of the Obj's permissions, so be careful not # to provide urls that contain unprotected secrets. # @api public def body_data_url nil end def set_attr_values(dictionary) @attr_values = dictionary @attr_dict = nil end # Returns an instance of AttrDict which provides access to formatted attribute values (i.e. content). # The method uses attr_defs to determine the right formatting depending on the particular field type. def attr_dict @attr_dict ||= AttrDict.new(self, attr_values, attr_defs) end # Returns a nested hash of attribute values. def attr_values @attr_values ||= begin encoded_and_encrypted_attr_values = read_attribute(:attr_values) return {} unless encoded_and_encrypted_attr_values encrypted_attr_values = Base64.decode64(encoded_and_encrypted_attr_values) if !encrypted_attr_values.starts_with?('Salted__') || encrypted_attr_values.length < 16 raise 'attr_values has wrong format' end cipher = OpenSSL::Cipher.new('rc4') cipher.decrypt salt = encrypted_attr_values[8..15] cipher.pkcs5_keyivgen(CRYPT_KEY, salt, 1) encrypted_attr_values = encrypted_attr_values[16..-1] if encrypted_attr_values.present? decrypted_attr_values = cipher.update(encrypted_attr_values) decrypted_attr_values << cipher.final JSON.parse(decrypted_attr_values) else {} end end end # Returns a nested hash of attribute definitions. def attr_defs @attr_defs ||= JSON.parse(read_attribute(:attr_defs) || "{}") end # Provides access to field metadata: # # <%= @obj.metadata_for_field(:body_de, :titles, :en) %> # # In addition to the field name, the method takes an arbitrary number of arguments # constituting a path through the nested (hash) structures of the attribute definition (attr_defs). # # If the path doesn't fit the metadata structure, the method returns nil and doesn't raise an exception. def metadata_for_field(field, *args) rslt = fiona_fields[field.to_s] || attr_defs[field.to_s] args.each do |key| rslt = rslt[key.to_s] unless rslt.nil? end rslt end # @api public def last_changed self[:last_changed] end # @api public def valid_from self[:valid_from] end # @api public def valid_until self[:valid_until] end # deprecated, use file_extension instead def content_type logger.warn "DEPRECATION WARNING: Obj#content_type is deprecated, use file_extension instead" file_extension end # Returns the MIME-type as determined from the file_extension - see MIME::Types # @api public def mime_type @mime_type ||= compute_mime_type end def respond_to?(method_id, include_private=false) if super true elsif %w(_attr_dict _attr_defs _attr_values).include?(method_id.to_s) # prevent infinite recursion when calling "attr_*" below, # since rails checks the absence of an "_attr_*" method internally return false else attr_dict.respond_to?(method_id) end end private def as_date(value) DateAttribute.parse(value) unless value.nil? end # Forwards any unknown method call to a corresponding instance # of AttrDict and thus provides access to object fields, i.e. content. # # In case of an invalid method call, an error is raised. # # Hint: Use [] instead to suppress error messages. def method_missing(method_id, *args) super rescue NoMethodError, NameError # prevent infinite recursion when calling "attr_*" below, # since rails checks the absence of an "_attr_*" method internally raise if %w(_attr_dict _attr_defs _attr_values).include?(method_id.to_s) if attr_dict.respond_to?(method_id) attr_dict.send method_id, *args else raise end end def fiona_fields @fiona_fields ||= ['name', 'obj_class', 'workflow', 'suppressexport', 'permalink'].inject({}) do |all,field| all.merge! field => { 'titles' => {'de' => field.humanize, 'en' => field.humanize}, 'type' => 'string', 'help_texts' => {'de' => field, 'en' => field} } end end def compute_mime_type MIME::Types.type_for(file_extension).first.content_type rescue binary? ? "application/octet-stream" : "text/plain" end def compare_on_sort_key(left_obj, right_obj, my_sort_key, my_sort_type) left_value = left_obj[my_sort_key] right_value = right_obj[my_sort_key] if left_value.nil? 1 elsif right_value.nil? -1 # hardcoded type check needed for speed elsif left_value.is_a?(Time) && right_value.is_a?(Time) left_value <=> right_value else if my_sort_type == "numeric" (left_value.to_i rescue 0) <=> (right_value.to_i rescue 0) else left_value.to_s.downcase <=> right_value.to_s.downcase end end end def self.try_type result = yield result if result < CmsBaseModel rescue NameError, ActiveRecord::ActiveRecordError nil end end end