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