capi_param_builder_ruby 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a33297523bdd07b142d4a946e4b3134675aec7d95d3479dad27647e566e36e4
4
- data.tar.gz: e62d98b35b46fd27adb878d736f8640c8cb589d437f94a7a19ab9d986eb2d38c
3
+ metadata.gz: 666e5be3f9571422768ea730eb957935a5212dc0ede2d0cb2a98cee69b8acee0
4
+ data.tar.gz: a041d8fb610f9b6282ea7552225c8816930939f74237d1dfc6da25fb0cafeb43
5
5
  SHA512:
6
- metadata.gz: 0c0a171eb5f541ca08f7a08c23f7505bdc5a00818668126698b440ea2268670f17bedefeadf10f2b6b89a9c504111a55b1050a2988180a12165c28e5e3cd5498
7
- data.tar.gz: a44149e6a88bddb67c82239b9f680668fc54f73e4ed6fcc16c6748b541110c1086e9f639cfca24aca7ac701ec11568e45cb2a5e8dae92f5fd3c0c1fd5dc071ea
6
+ metadata.gz: eba9eaab35779653e37b304bacdaa41b198ad95c704e1f94c4670e3682577d9a546fd7a378f89c89b0270a20f7e4df450dcb81f1d78700922b9730d7fe908070
7
+ data.tar.gz: 4101a8e7fa1b721663c5368a42292174ed1cd7e59c5bf6ddec5f5eab6f552d004150417f4ff4636e810825e83cea4bb56fc1fe1723e9cdbabad95c1501770cda
@@ -7,6 +7,8 @@
7
7
  require_relative 'model/fbc_param_configs'
8
8
  require_relative 'model/cookie_settings'
9
9
  require_relative 'model/etld_plus_one_resolver'
10
+ require_relative 'model/plain_data_object'
11
+ require_relative 'util/request_context_adaptor'
10
12
  require_relative 'release_config'
11
13
  require 'set'
12
14
  require 'uri'
@@ -40,6 +42,8 @@ class ParamBuilder
40
42
  @appendix_net_new = get_appendix(APPENDIX_NET_NEW)
41
43
  @appendix_modified_new = get_appendix(APPENDIX_MODIFIED_NEW)
42
44
  @appendix_no_change = get_appendix(APPENDIX_NO_CHANGE)
45
+ @referrer_url = nil
46
+ @event_source_url = nil
43
47
 
44
48
  if input.nil?
45
49
  return
@@ -175,12 +179,18 @@ class ParamBuilder
175
179
  end
176
180
 
177
181
  def process_request(host, queries, cookies, referer=nil)
182
+ @event_source_url = nil
178
183
  compute_etld_plus_one_for_host(host)
179
184
  @cookie_to_set_dict = {}
180
185
  @cookie_to_set = Set.new()
181
186
  @fbc = pre_process_cookies(cookies, FBC_NAME)
182
187
  @fbp = pre_process_cookies(cookies, FBP_NAME)
183
188
 
189
+ @referrer_url = referer
190
+ if @referrer_url.is_a?(String) && !@referrer_url.empty?
191
+ @referrer_url = "#{@referrer_url}.#{@appendix_no_change}"
192
+ end
193
+
184
194
  # Get new fbc payload
185
195
  new_fbc_payload = get_new_fbc_payload_from_url(queries, referer)
186
196
 
@@ -200,6 +210,32 @@ class ParamBuilder
200
210
  return @cookie_to_set
201
211
  end
202
212
 
213
+ # Process a request from a context object.
214
+ #
215
+ # Accepts either a PlainDataObject (used directly) or any framework
216
+ # request / Rack-style env Hash that RequestContextAdaptor knows how to
217
+ # extract from. Nil falls through to the adapter's empty-default behavior.
218
+ #
219
+ # Note: PlainDataObject carries x_forwarded_for and remote_address for
220
+ # parity with the JS / PHP SDKs, but the Ruby ParamBuilder does not yet
221
+ # implement client-IP attribution; those fields are extracted by the
222
+ # adapter but ignored here.
223
+ def process_request_from_context(context = nil)
224
+ data = context.is_a?(PlainDataObject) ?
225
+ context : RequestContextAdaptor.extract(context)
226
+
227
+ process_request(
228
+ data.host,
229
+ data.query_params,
230
+ data.cookies,
231
+ data.referer
232
+ )
233
+
234
+ @event_source_url = construct_event_source_url(data)
235
+
236
+ return @cookie_to_set
237
+ end
238
+
203
239
  def get_cookies_to_set()
204
240
  return @cookie_to_set
205
241
  end
@@ -212,9 +248,25 @@ class ParamBuilder
212
248
  return @fbp
213
249
  end
214
250
 
251
+ def get_referrer_url()
252
+ return @referrer_url
253
+ end
254
+
255
+ def get_event_source_url()
256
+ return @event_source_url
257
+ end
258
+
215
259
  private def compute_etld_plus_one_for_host(host)
216
260
  if @etld_plus_one.nil? || @host.nil?
217
261
  @host = host
262
+ # Guard empty/nil host: Ruby's "".split(".") returns [], so naively
263
+ # computing size - 1 would yield -1 and emit malformed `fb.-1.…`
264
+ # cookies. Match Python's behavior: empty host -> nil etld+1, index 0.
265
+ if host.nil? || host.empty?
266
+ @etld_plus_one = nil
267
+ @sub_domain_index = 0
268
+ return
269
+ end
218
270
  host_name = extract_host_from_http_host(host)
219
271
  if is_ip_address(host_name)
220
272
  @etld_plus_one = maybe_bracket_ipv6(host_name)
@@ -286,6 +338,21 @@ class ParamBuilder
286
338
  return host_name
287
339
  end
288
340
 
341
+ private def construct_event_source_url(data)
342
+ return nil if data.nil?
343
+ return nil if data.host.nil? || data.host.empty?
344
+ return nil if data.scheme.nil? || data.scheme.empty?
345
+
346
+ url = "#{data.scheme.downcase}://#{data.host}"
347
+ if !data.request_uri.nil? && !data.request_uri.empty?
348
+ url += data.request_uri
349
+ end
350
+ if url.is_a?(String) && !url.empty?
351
+ url = "#{url}.#{@appendix_net_new}"
352
+ end
353
+ url
354
+ end
355
+
289
356
  private def get_updated_fbc_cookie(existing_fbc = nil, new_fbc_payload)
290
357
  if @fbc_params_configs.nil?
291
358
  return nil
@@ -0,0 +1,24 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+ # This source code is licensed under the license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ class PlainDataObject
8
+ attr_accessor :host, :query_params, :cookies, :referer,
9
+ :x_forwarded_for, :remote_address, :scheme,
10
+ :request_uri
11
+
12
+ def initialize(host, query_params, cookies, referer = nil,
13
+ x_forwarded_for = nil, remote_address = nil,
14
+ scheme = nil, request_uri = nil)
15
+ @host = host
16
+ @query_params = query_params
17
+ @cookies = cookies
18
+ @referer = referer
19
+ @x_forwarded_for = x_forwarded_for
20
+ @remote_address = remote_address
21
+ @scheme = scheme
22
+ @request_uri = request_uri
23
+ end
24
+ end
@@ -4,5 +4,5 @@
4
4
  # This source code is licensed under the license found in the
5
5
  # LICENSE file in the root directory of this source tree.
6
6
  module ReleaseConfig
7
- VERSION = "1.1.1"
7
+ VERSION = "1.3.0"
8
8
  end
@@ -0,0 +1,170 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+ # This source code is licensed under the license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ require 'cgi'
8
+ require_relative '../model/plain_data_object'
9
+
10
+ # Universal Request Context Adaptor for Ruby.
11
+ #
12
+ # Extracts request data (host, query params, cookies, referer,
13
+ # x-forwarded-for, remote address) from a Rack-style environ hash or any
14
+ # framework request object that exposes #env (Rack, Rails, Sinatra). Falls
15
+ # back to empty defaults for nil or unrecognized inputs.
16
+ class RequestContextAdaptor
17
+ HTTP_DEFAULT_PORT = 80
18
+ HTTPS_DEFAULT_PORT = 443
19
+
20
+ def self.extract(request_obj = nil)
21
+ host = ''
22
+ query_params = {}
23
+ cookies = {}
24
+ referer = nil
25
+ x_forwarded_for = nil
26
+ remote_address = nil
27
+ scheme = nil
28
+ request_uri = nil
29
+
30
+ if request_obj.nil?
31
+ return PlainDataObject.new(
32
+ host, query_params, cookies, referer, x_forwarded_for, remote_address,
33
+ scheme, request_uri
34
+ )
35
+ end
36
+
37
+ begin
38
+ env = resolve_env(request_obj)
39
+ if env.is_a?(Hash) && !env.empty?
40
+ host = wsgi_host(env)
41
+ referer = nilify(env['HTTP_REFERER'])
42
+ x_forwarded_for = nilify(env['HTTP_X_FORWARDED_FOR'])
43
+ remote_address = nilify(env['REMOTE_ADDR'])
44
+
45
+ query_params = parse_query_string(env['QUERY_STRING'])
46
+ cookies = parse_cookie_header(env['HTTP_COOKIE'])
47
+
48
+ scheme = extract_scheme(env)
49
+ request_uri = extract_request_uri(env)
50
+ end
51
+ rescue StandardError
52
+ # Silently swallow exceptions and return the object with default values.
53
+ end
54
+
55
+ PlainDataObject.new(
56
+ host, query_params, cookies, referer, x_forwarded_for, remote_address,
57
+ scheme, request_uri
58
+ )
59
+ end
60
+
61
+ def self.resolve_env(request_obj)
62
+ if request_obj.respond_to?(:env) && request_obj.env.is_a?(Hash)
63
+ return request_obj.env
64
+ end
65
+ return request_obj if request_obj.is_a?(Hash)
66
+ nil
67
+ end
68
+ private_class_method :resolve_env
69
+
70
+ def self.parse_query_string(query_string)
71
+ return {} if query_string.nil? || query_string.to_s.empty?
72
+ CGI.parse(query_string.to_s)
73
+ end
74
+ private_class_method :parse_query_string
75
+
76
+ def self.parse_cookie_header(raw_cookie)
77
+ return {} if raw_cookie.nil? || raw_cookie.to_s.empty?
78
+ raw_cookie.to_s.split(';').each_with_object({}) do |pair, hash|
79
+ parts = pair.split('=', 2)
80
+ next unless parts.size == 2
81
+ key = parts[0].strip
82
+ next if key.empty?
83
+ begin
84
+ hash[key] = percent_decode(parts[1].strip)
85
+ rescue StandardError
86
+ # Per-pair isolation: a single bad cookie (e.g. an encoding error)
87
+ # must not drop the other valid cookies in the same header.
88
+ end
89
+ end
90
+ end
91
+ private_class_method :parse_cookie_header
92
+
93
+ # Percent-decode a cookie value WITHOUT converting `+` to space. CGI.unescape
94
+ # applies form decoding (`+` -> ` `), which would corrupt base64 / JWT-like
95
+ # cookie values that legitimately contain `+`.
96
+ #
97
+ # Operates on a BINARY copy first so that gsub-ing ASCII-8BIT bytes (from
98
+ # `pack("H2")`) into a string that already carries UTF-8 multi-byte
99
+ # characters does not raise `Encoding::CompatibilityError`. After decoding
100
+ # we relabel as UTF-8 and `scrub` any invalid byte sequences (e.g. lone
101
+ # `%FF`) so downstream JSON / logging / hashing does not choke on
102
+ # invalid UTF-8.
103
+ def self.percent_decode(value)
104
+ value.to_s.b
105
+ .gsub(/%([0-9a-fA-F]{2})/) { [Regexp.last_match(1)].pack('H2') }
106
+ .force_encoding(Encoding::UTF_8)
107
+ .scrub
108
+ end
109
+ private_class_method :percent_decode
110
+
111
+ def self.wsgi_host(env)
112
+ host = env['HTTP_HOST']
113
+ return host if host && !host.empty?
114
+ server_name = env['SERVER_NAME']
115
+ return '' if server_name.nil? || server_name.empty?
116
+ server_port = env['SERVER_PORT']
117
+ scheme = env['rack.url_scheme'] || 'http'
118
+ format_host_port(server_name, server_port, scheme)
119
+ end
120
+ private_class_method :wsgi_host
121
+
122
+ # Build a host[:port] authority, bracketing bare IPv6 literals. We always
123
+ # bracket bare IPv6 — even when the port is omitted — so that downstream
124
+ # `extract_host_from_http_host` (which treats the last `:` as a port
125
+ # separator when no `]` is present) does not truncate the address.
126
+ def self.format_host_port(host, port, scheme)
127
+ bracketed = host.include?(':') && !host.start_with?('[') ? "[#{host}]" : host
128
+ default_port =
129
+ scheme == 'https' || scheme == 'wss' ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT
130
+ if port.nil? || port.to_s.empty? || port.to_s == default_port.to_s
131
+ return bracketed
132
+ end
133
+ "#{bracketed}:#{port}"
134
+ end
135
+ private_class_method :format_host_port
136
+
137
+ def self.extract_scheme(env)
138
+ raw_str = (env['REQUEST_SCHEME'] || env['rack.url_scheme']).to_s
139
+ return raw_str.downcase unless raw_str.empty?
140
+
141
+ https_str = env['HTTPS'].to_s
142
+ return 'https' if !https_str.empty? && https_str.downcase != 'off'
143
+ nil
144
+ end
145
+ private_class_method :extract_scheme
146
+
147
+ def self.extract_request_uri(env)
148
+ raw = env['REQUEST_URI']
149
+ return raw.to_s if raw && !raw.to_s.empty?
150
+
151
+ script_name = env['SCRIPT_NAME'].to_s
152
+ path_info = env['PATH_INFO'].to_s
153
+ uri = script_name + path_info
154
+ query_string = env['QUERY_STRING']
155
+ has_qs = query_string && !query_string.to_s.empty?
156
+
157
+ return nil if uri.empty? && !has_qs
158
+ uri = '/' if uri.empty?
159
+ uri = "#{uri}?#{query_string}" if has_qs
160
+ uri
161
+ end
162
+ private_class_method :extract_request_uri
163
+
164
+ def self.nilify(value)
165
+ return nil if value.nil?
166
+ return nil if value.respond_to?(:empty?) && value.empty?
167
+ value
168
+ end
169
+ private_class_method :nilify
170
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capi_param_builder_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Facebook
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-30 00:00:00.000000000 Z
11
+ date: 2026-05-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -20,7 +20,9 @@ files:
20
20
  - lib/model/cookie_settings.rb
21
21
  - lib/model/etld_plus_one_resolver.rb
22
22
  - lib/model/fbc_param_configs.rb
23
+ - lib/model/plain_data_object.rb
23
24
  - lib/release_config.rb
25
+ - lib/util/request_context_adaptor.rb
24
26
  homepage:
25
27
  licenses:
26
28
  - Nonstandard