lib/facebook_ads/ad_objects/server_side/util.rb (203 lines of code) (raw):

# Copyright (c) 2017-present, Facebook, Inc. All rights reserved. # # You are hereby granted a non-exclusive, worldwide, royalty-free license to use, # copy, modify, and distribute this software in source code or binary form for use # in connection with the web services and APIs provided by Facebook. # # As with any software that integrates with the Facebook platform, your use of # this software is subject to the Facebook Platform Policy # [http://developers.facebook.com/policy/]. This copyright notice shall be # included in all copies or substantial portions of the software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'digest' require 'countries' require 'money' require 'time' module FacebookAds module ServerSide class Util PHONE_NUMBER_IGNORE_CHAR_SET = /[\-\s\(\)]+/ PHONE_NUMBER_DROP_PREFIX_ZEROS = /^\+?0{0,2}/ US_PHONE_NUMBER_REGEX = /^1\(?\d{3}\)?\d{7}$/ INTL_PHONE_NUMBER_REGEX = /^\d{1,4}\(?\d{2,3}\)?\d{4,}$/ # RFC 2822 for email format EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i # Normalizes the input array of strings given the field_type # @param [Array<String>] input string array that needs to be normalized # @param [String] field_type Type/Key for the value provided # @return [Array<String>] Normalized values for the input and field_type. def self.normalize_array(input_array, field_type) return nil unless input_array.is_a?(Enumerable) return nil if input_array.empty? return nil unless input_array.all?{|value| value.is_a?(String)} input_array .map{|value| self.normalize(value, field_type)} .to_set .to_a end # Normalizes the input string given the field_type # @param [String] input Input string that needs to be normalized # @param [String] field_type Type/Key for the value provided # @return [String] Normalized value for the input and field_type. def self.normalize(input, field_type) if input.nil? or field_type.nil? return nil; end input = input.strip.downcase # If the data is already hashed, we by-pass input normalization if is_already_hashed?(input) == true return input end normalized_input = input; case field_type when 'action_source' return normalize_action_source input when 'country' normalized_input = normalize_country input when 'ct' normalized_input = normalize_city input when 'currency' return normalize_currency input when 'delivery_category' return normalize_delivery_category input when 'em' normalized_input = normalize_email input when 'ge' normalized_input = normalize_gender input when 'ph' normalized_input = normalize_phone input when 'st' normalized_input = normalize_state input when 'zp' normalized_input = normalize_zip input when 'f5first' normalized_input = normalize_f5 input when 'f5last' normalized_input = normalize_f5 input when 'fi' normalized_input = normalize_fi input when 'dobd' normalized_input = normalize_dobd input when 'dobm' normalized_input = normalize_dobm input when 'doby' normalized_input = normalize_doby input end normalized_input = sha256Hash normalized_input return normalized_input end # @return [String] SHA 256 hash of input string def self.sha256Hash(input) unless input.nil? Digest::SHA256.hexdigest input end end # Boolean method which checks if a input is already hashed with MD5 or SHA256 # @param [String] input Input string that is to be validated # @return [TrueClass|FalseClass] representing whether the value is hashed def self.is_already_hashed?(input) # We support Md5 and SHA256, and highly recommend users to use SHA256 for hashing PII keys. md5_match = /^[a-f0-9]{32}$/.match(input) sha256_match = /^[a-f0-9]{64}$/.match(input) if md5_match != nil or sha256_match != nil return true end return false end # Normalizes the given country code and returns acceptable hashed country ISO code def self.normalize_country(country) # Replace unwanted characters and retain only alpha characters bounded for ISO code. country = country.gsub(/[^a-z]/,'') iso_country = ISO3166::Country.search(country) if iso_country == nil raise ArgumentError, "Invalid format for country:'" + country + "'.Please follow ISO 2-letter ISO 3166-1 standard for representing country. eg: us" end return country end # Normalizes the given city and returns acceptable hashed city value def self.normalize_city(city) # Remove commonly occuring characters from city name. city = city.gsub(/[0-9.\s\-()]/,'') return city end # Normalizes the given currency code and returns acceptable hashed currency ISO code def self.normalize_currency(currency) # Retain only alpha characters bounded for ISO code. currency = currency.gsub(/[^a-z]/,'') iso_currency = Money::Currency.find(currency) if iso_currency == nil raise ArgumentError, "Invalid format for currency:'" + currency + "'.Please follow ISO 3-letter ISO 4217 standard for representing currency. Eg: usd" end return currency; end # Normalizes the given email and returns acceptable hashed email value def self.normalize_email(email) if EMAIL_REGEX.match(email) == nil raise ArgumentError, "Invalid email format for the passed email: '#{email}'. Please check the passed email format." end return email end # Normalizes the given gender and returns acceptable hashed gender value def self.normalize_gender(gender) # Replace extra characters with space, to bound under alpha characters set. gender = gender.gsub(/[^a-z]/,'') case gender when 'female' , 'f' gender = 'f' when 'male' , 'm' gender = 'm' else return nil end return gender end # Normalizes the given phone and returns acceptable hashed phone value def self.normalize_phone(phone) # Drop the spaces, hyphen and parenthesis from the Phone Number normalized_phone = phone.gsub(PHONE_NUMBER_IGNORE_CHAR_SET, '') if(is_international_number?(normalized_phone)) normalized_phone = normalized_phone.gsub(PHONE_NUMBER_DROP_PREFIX_ZEROS, '') end if normalized_phone.length < 7 || normalized_phone.length > 15 return nil; end return normalized_phone end # Normalizes the given state and returns acceptable hashed state value def self.normalize_state(state) state = state.gsub(/[0-9.\s\-()]/,'') return state end # Normalizes the given zip and returns acceptable hashed zip code value def self.normalize_zip(zip) # Remove spaces from the Postal code zip = zip.gsub(/[\s]/,'') # If the zip code '-', we retain just the first part alone. zip = zip.split('-')[0] if zip.length < 2 return nil end return zip end # Boolean method which checks if a given number is represented in international format # @param [String] phone_number that has to be tested. # @return [TrueClass | FalseClass] boolean value representing if a number is international def self.is_international_number?(phone_number) # Drop upto 2 leading 0s from the number phone_number = phone_number.gsub(PHONE_NUMBER_DROP_PREFIX_ZEROS, '') if phone_number.start_with?('0') return false; end if phone_number.start_with?('1') && US_PHONE_NUMBER_REGEX.match(phone_number) != nil return false; end if INTL_PHONE_NUMBER_REGEX.match(phone_number) != nil return true; end return false; end def self.normalize_f5(input) input[0, 5] end def self.normalize_fi(fi) fi[0, 1] end def self.normalize_dobd(dobd) if dobd.length == 1 dobd = '0' + dobd end dobd_int = dobd.to_i if dobd.length > 2 or dobd_int < 1 or dobd_int > 31 raise ArgumentError.new("Invalid dobd format: '#{dobd}'. Please pass in a valid date of birth day in 'DD' format.") end return dobd end def self.normalize_dobm(dobm) if dobm.length == 1 dobm = '0' + dobm end dobm_int = dobm.to_i if dobm.length > 2 or dobm_int < 1 or dobm_int > 12 raise ArgumentError.new("Invalid dobm format: '#{dobm}'. Please pass in a valid date of birth month in 'MM' format.") end return dobm end def self.normalize_doby(doby) unless doby.match("^[0-9]{4}$") raise ArgumentError.new("Invalid doby format: '#{doby}'. Please pass in a valid birth year in 'YYYY' format.") end doby end # Normalizes the input delivery category and returns valid value (or throw exception if invalid). def self.normalize_delivery_category(delivery_category) unless FacebookAds::ServerSide::DeliveryCategory.include?(delivery_category) raise ArgumentError.new("Invalid delivery_category passed: " + delivery_category + ". Please use one of the defined values #{FacebookAds::ServerSide::DeliveryCategory.to_a.join(',')}" ) end delivery_category; end # Normalizes the input action_source and returns valid value (or throw exception if invalid). def self.normalize_action_source(action_source) unless FacebookAds::ServerSide::ActionSource.include?(action_source) values = FacebookAds::ServerSide::ActionSource.to_a.join(',') raise ArgumentError.new( "Invalid action_source passed: #{action_source}. Please use one of the defined values: #{values}" ) end action_source end end end end