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
-
#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.
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.
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.
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 |
#attributes ⇒ Hash
TODO: We can optimize
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
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_map ⇒ Hash
Returns 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
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.
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.
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 |