Module: Parse::Properties

Included in:
Object
Defined in:
lib/parse/model/core/properties.rb

Overview

This module provides support for handling all the different types of column data types supported in Parse and mapping them between their remote names with their local ruby named attributes.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

TYPES =

These are the base types supported by Parse.

[:string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :polygon, :bytes, :object, :acl, :timezone, :phone, :email, :vector].freeze
BASE =

These are the base mappings of the remote field name types.

{ objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze
BASE_KEYS =

The list of properties that are part of all objects

[:id, :created_at, :updated_at].freeze
PROTECTED_MASS_ASSIGNMENT_KEYS =

Attribute names refused on the mass-assignment path (Parse::Object#attributes= and apply_attributes! with dirty_track: true). Internal hydration from server responses uses dirty_track: false and is unaffected, so server-issued sessionTokens etc. still flow through during decoding.

The list intentionally covers ONLY server-managed and security- internal fields. User-facing properties like acl and objectId are deliberately omitted because constructor calls like Document.new(acl: my_acl) are legitimate developer code. Rails applications receiving form input should use StrongParameters (params.permit(...)) to filter attacker-controlled keys before passing the hash to Model.new or attributes=.

%w[
  sessionToken session_token
  roles _rperm _wperm
  _hashed_password _password_history
  authData _auth_data auth_data
  className __type
  createdAt created_at updatedAt updated_at
].freeze
PROTECTED_INITIALIZE_KEYS =

Narrow subset of PROTECTED_MASS_ASSIGNMENT_KEYS that closes the documented authentication / authorization mass-assignment attacks (NEW-EXT-1) without breaking the legitimate "build a hydrated object" pattern (Klass.new("objectId" => id, "createdAt" => ts, "field" => …)). Applied by Parse::Object#initialize when trusted: false (the default) so caller-supplied hashes — even those bearing an objectId — cannot forge session tokens, ACL row-permissions, password hashes, OAuth auth_data, or roles.

Excluded from this narrow set on purpose:

  • createdAt / updatedAt: timestamp integrity, not a security boundary. App code commonly rehydrates cached objects via Klass.new(hash) and expects timestamps to populate.
  • className / __type: routing metadata. Parse::Object.build has its own className-mismatch guard; the in-memory value here is informational only.

The wider PROTECTED_MASS_ASSIGNMENT_KEYS list still applies to Parse::Object#attributes= and explicit apply_attributes!(dirty_track: true) calls, where Rails-form input is the expected source and timestamp forgery is also undesirable.

%w[
  sessionToken session_token
  roles _rperm _wperm
  _hashed_password _password_history
  authData _auth_data auth_data
].freeze
BASE_FIELD_MAP =

Default hash map of local attribute name to remote column name

{ id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL }.freeze
CORE_FIELDS =

The delete operation hash.

{ id: :string, created_at: :date, updated_at: :date, acl: :acl }.freeze
DELETE_OP =

The delete operation hash.

{ "__op" => "Delete" }.freeze

Instance Method Summary collapse

Instance Method Details

#apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil) ⇒ Hash

support for setting a hash of attributes on the object with a given dirty tracking value if dirty_track: is set to false (default), attributes are set without dirty tracking. Allos mass assignment of properties with a provided hash.

Parameters:

  • hash (Hash)

    the hash matching the property field names.

  • dirty_track (Boolean) (defaults to: false)

    whether dirty tracking be enabled. When true, permission-sensitive keys (PROTECTED_MASS_ASSIGNMENT_KEYS) are skipped by default so attacker-controlled params cannot overwrite acl/roles/sessionToken/etc. Set explicitly via the typed property writers when the caller is trusted.

  • filter_protected (Boolean, nil) (defaults to: nil)

    whether to filter out PROTECTED_MASS_ASSIGNMENT_KEYS. Defaults to dirty_track for backwards-compat (the historical coupling). Callers can pass true explicitly to filter even on the trusted hydration path (used by Object#initialize when constructed with trusted: false but an objectId is in the hash). false explicitly preserves the legacy "server response" semantics.

  • protected_set (Array<String>, nil) (defaults to: nil)

    override which key list to filter when filter_protected is true. Defaults to the wider PROTECTED_MASS_ASSIGNMENT_KEYS. Object#initialize passes PROTECTED_INITIALIZE_KEYS here to allow legitimate hydration patterns (Klass.new("objectId" => …, "createdAt" => …)) while still refusing security-critical forgeries (sessionToken, _rperm, authData, …).

Returns:



676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/parse/model/core/properties.rb', line 676

def apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil)
  return unless hash.is_a?(Hash)

  filter_protected = dirty_track if filter_protected.nil?
  protected_set ||= Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS
  protected_keys = filter_protected ? protected_set : nil
  # Internal hydration path lifts objectId out of the response hash. The
  # mass-assignment path must not, or attacker-controlled params can
  # overwrite the primary key of an in-memory object.
  unless dirty_track
    @id ||= hash[Parse::Model::ID] || hash[Parse::Model::OBJECT_ID] || hash[:objectId]
  end
  hash.each do |key, value|
    next if protected_keys && protected_keys.include?(key.to_s)
    method = "#{key}_set_attribute!".freeze
    send(method, value, dirty_track) if respond_to?(method)
  end
end

#attribute_changes?Boolean

Returns true if any of the attributes have changed.

Returns:

  • (Boolean)

    true if any of the attributes have changed.



733
734
735
736
737
# File 'lib/parse/model/core/properties.rb', line 733

def attribute_changes?
  changed.any? do |key|
    fields[key.to_sym].present?
  end
end

#attribute_updates(include_all = false) ⇒ Hash

Returns a hash of attributes for properties that have changed. This will not include any of the base attributes (ex. id, created_at, etc). This method helps generate the change payload that will be sent when saving objects to Parse.

Parameters:

  • include_all (Boolean) (defaults to: false)

    whether to include all BASE_KEYS attributes.

Returns:



710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/parse/model/core/properties.rb', line 710

def attribute_updates(include_all = false)
  # TODO: Replace this algorithm with reduce()
  h = {}
  changed.each do |key|
    key = key.to_sym
    next if include_all == false && Parse::Properties::BASE_KEYS.include?(key)
    next unless fields[key].present?
    remote_field = self.field_map[key] || key
    h[remote_field] = send key
    h[remote_field] = { __op: :Delete } if h[remote_field].nil?
    # in the case that the field is a Parse object, generate a pointer
    # if it is a Parse::PointerCollectionProxy, then make sure we get a list of pointers.
    h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
    # For regular CollectionProxy arrays containing Parse objects, convert to pointers for storage
    if h[remote_field].is_a?(Parse::CollectionProxy) && !h[remote_field].is_a?(Parse::PointerCollectionProxy)
      h[remote_field] = h[remote_field].as_json(pointers_only: true)
    end
    h[remote_field] = h[remote_field].pointer if h[remote_field].respond_to?(:pointer)
  end
  h
end

#attributesHash

TODO: We can optimize

Returns:

  • (Hash)

    returns the list of property attributes for this class.



647
648
649
# File 'lib/parse/model/core/properties.rb', line 647

def attributes
  { __type: :string, :className => :string }.merge!(self.class.attributes)
end

#attributes=(hash) ⇒ Hash

Supports mass assignment of attributes

Returns:



697
698
699
700
701
702
# File 'lib/parse/model/core/properties.rb', line 697

def attributes=(hash)
  return unless hash.is_a?(Hash)
  # - [:id, :objectId]
  # only overwrite @id if it hasn't been set.
  apply_attributes!(hash, dirty_track: true)
end

#field_mapHash

Returns a hash mapping of all property fields and their types.

Returns:

  • (Hash)

    a hash mapping of all property fields and their types.



636
637
638
# File 'lib/parse/model/core/properties.rb', line 636

def field_map
  self.class.field_map
end

#fields(type = nil) ⇒ Object

Returns the list of fields

Returns:

  • returns the list of fields



641
642
643
# File 'lib/parse/model/core/properties.rb', line 641

def fields(type = nil)
  self.class.fields(type)
end

#format_operation(key, val, data_type) ⇒ Object

Returns a formatted value based on the operation hash and data_type of the property. For some values in Parse, they are specified as operation hashes which could include Add, Remove, Delete, AddUnique and Increment.

Parameters:

  • key (Symbol)

    the name of the property

  • val (Hash)

    the Parse operation hash value.

  • data_type (Symbol)

    The data type of the property.

Returns:



746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/parse/model/core/properties.rb', line 746

def format_operation(key, val, data_type)
  return val unless val.is_a?(Hash) && val["__op"].present?
  op = val["__op"]
  ivar = :"@#{key}"
  #handles delete case otherwise 'null' shows up in column
  if "Delete" == op
    val = nil
  elsif "Add" == op && data_type == :array
    val = (instance_variable_get(ivar) || []).to_a + (val["objects"] || [])
  elsif "Remove" == op && data_type == :array
    val = (instance_variable_get(ivar) || []).to_a - (val["objects"] || [])
  elsif "AddUnique" == op && data_type == :array
    objects = (val["objects"] || []).uniq
    original_items = (instance_variable_get(ivar) || []).to_a
    objects.reject! { |r| original_items.include?(r) }
    val = original_items + objects
  elsif "Increment" == op && data_type == :integer || data_type == :integer
    # for operations that increment by a certain amount, they come as a hash
    val = (instance_variable_get(ivar) || 0) + (val["amount"] || 0).to_i
  end
  val
end

#format_value(key, val, data_type = nil) ⇒ Object

this method takes an input value and transforms it to the proper local format depending on the data type that was set for a particular property key. Return the internal representation of a property value for a given data type.

Parameters:

  • key (Symbol)

    the name of the property

  • val (Object)

    the value to format.

  • data_type (Symbol) (defaults to: nil)

    provide a hint to the data_type of this value.

Returns:



776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/parse/model/core/properties.rb', line 776

def format_value(key, val, data_type = nil)
  # if data_type wasn't passed, then get the data_type from the fields hash
  data_type ||= self.fields[key]

  val = format_operation(key, val, data_type)

  case data_type
  when :object
    val = val.with_indifferent_access if val.is_a?(Hash)
  when :array
    # All "array" types use a collection proxy
    val = val.to_a if val.is_a?(Parse::CollectionProxy) #all objects must be in array form
    val = [val] unless val.is_a?(Array) #all objects must be in array form
    val.compact! #remove any nil
    val = Parse::CollectionProxy.new val, delegate: self, key: key
  when :geopoint
    val = Parse::GeoPoint.new(val) unless val.blank?
  when :polygon
    val = Parse::Polygon.new(val) unless val.blank?
  when :file
    if val.is_a?(Hash) && val["__type"] == "File"
      val = Parse::File.new(val)
    elsif !val.blank?
      val = Parse::File.new(val)
    end
  when :bytes
    if val.is_a?(Hash) && val["__type"] == "Bytes"
      val = Parse::Bytes.new(val["base64"] || val[:base64])
    elsif !val.blank?
      val = Parse::Bytes.new(val)
    end
  when :integer
    if val.nil? || val.respond_to?(:to_i) == false
      val = nil
    else
      val = val.to_i
    end
  when :boolean
    if val.nil?
      val = nil
    else
      val = val ? true : false
    end
  when :string
    val = val.to_s unless val.blank?
  when :float
    val = val.to_f unless val.blank?
  when :acl
    # ACL types go through a special conversion
    val = ACL.typecast(val, self)
  when :date
    # if it respond to parse_date, then use that as the conversion.
    if val.respond_to?(:parse_date) && val.is_a?(Parse::Date) == false
      val = val.parse_date
      # if the value is a hash, then it may be the Parse hash format for an iso date.
    elsif val.is_a?(Hash) # val.respond_to?(:iso8601)
      iso_val = (val["iso"] || val[:iso]).to_s.strip.presence
      val = iso_val ? Parse::Date.parse(iso_val) : nil
    elsif val.is_a?(String)
      # if it's a string, try parsing the date
      val = (stripped = val.strip).present? ? Parse::Date.parse(stripped) : nil
      #elsif val.present?
      #  pus "[Parse::Stack] Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
      #   raise ValueError, "Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
    end
  when :timezone
    val = Parse::TimeZone.new(val) if val.present?
  when :phone
    val = Parse::Phone.new(val) if val.present?
  when :email
    val = Parse::Email.new(val) if val.present?
  when :vector
    # nil/blank → unset; coerce Arrays (and pass-through Parse::Vector)
    # to a Parse::Vector, which validates that every element is a
    # finite Numeric. Dimension mismatch is reported via
    # validates_each so callers can rescue ActiveModel::ValidationError
    # at save time rather than at every assignment; raising here
    # would break partial hydration where the dimension class-level
    # declaration may not yet be loaded.
    if val.nil?
      val = nil
    elsif val.is_a?(Parse::Vector)
      val = val
    elsif val.is_a?(Array)
      val = Parse::Vector.new(val)
    else
      raise ArgumentError,
            "Property #{self.class}##{key} :vector requires an Array or Parse::Vector " \
            "(got #{val.class})."
    end
  else
    # You can provide a specific class instead of a symbol format
    if data_type.respond_to?(:typecast)
      val = data_type.typecast(val)
    else
      warn "Property :#{key}: :#{data_type} has no valid data type"
      val = val #default
    end
  end
  val
end