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].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:



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
# File 'lib/parse/model/core/properties.rb', line 626

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.



683
684
685
686
687
# File 'lib/parse/model/core/properties.rb', line 683

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:



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/parse/model/core/properties.rb', line 660

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.



597
598
599
# File 'lib/parse/model/core/properties.rb', line 597

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

#attributes=(hash) ⇒ Hash

Supports mass assignment of attributes

Returns:



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

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.



586
587
588
# File 'lib/parse/model/core/properties.rb', line 586

def field_map
  self.class.field_map
end

#fields(type = nil) ⇒ Object

Returns the list of fields

Returns:

  • returns the list of fields



591
592
593
# File 'lib/parse/model/core/properties.rb', line 591

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:



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/parse/model/core/properties.rb', line 696

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:



726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
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
# File 'lib/parse/model/core/properties.rb', line 726

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?
  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