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=andapply_attributes!withdirty_track: true). Internal hydration from server responses usesdirty_track: falseand 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
aclandobjectIdare deliberately omitted because constructor calls likeDocument.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 toModel.neworattributes=. %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 byParse::Object#initializewhentrusted: false(the default) so caller-supplied hashes — even those bearing anobjectId— 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 viaKlass.new(hash)and expects timestamps to populate.className/__type: routing metadata.Parse::Object.buildhas 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 explicitapply_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
-
#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.
-
#attribute_changes? ⇒ Boolean
True if any of the attributes have changed.
-
#attribute_updates(include_all = false) ⇒ Hash
Returns a hash of attributes for properties that have changed.
-
#attributes ⇒ Hash
TODO: We can optimize.
-
#attributes=(hash) ⇒ Hash
Supports mass assignment of attributes.
-
#field_map ⇒ Hash
A hash mapping of all property fields and their types.
-
#fields(type = nil) ⇒ Object
Returns the list of fields.
-
#format_operation(key, val, data_type) ⇒ Object
Returns a formatted value based on the operation hash and data_type of the property.
-
#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.
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.
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.
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.
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 |
#attributes ⇒ Hash
TODO: We can optimize
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
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_map ⇒ Hash
Returns 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
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.
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.
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 |