Class: Parse::Webhooks

Inherits:
Object
  • Object
show all
Extended by:
Registration
Includes:
Client::Connectable
Defined in:
lib/parse/webhooks.rb,
lib/parse/webhooks/payload.rb,
lib/parse/webhooks/registration.rb,
lib/parse/webhooks/trigger_audit.rb,
lib/parse/webhooks/replay_protection.rb

Overview

Interface to the CloudCode webhooks API.

Defined Under Namespace

Modules: Registration, ReplayProtection Classes: Payload, ResponseError, TriggerAudit

Constant Summary collapse

AUTH_TRIGGERS =

The authentication-side triggers (local underscore form). These carry a +_User+ / +_Session+ as the payload object but are NOT object save/delete triggers: the router runs no ActiveModel save/create/destroy callbacks for them, and Parse Server ignores their response body.

%i[
  before_login after_login after_logout before_password_reset_request
].freeze
LIVE_QUERY_TRIGGERS =

The LiveQuery triggers (local underscore form). Connection-global or event-scoped; Parse Server ignores their response body. Delivered over an HTTP webhook only in a co-located single-process LiveQuery setup.

%i[before_connect before_subscribe after_event].freeze
NON_OBJECT_TRIGGERS =

Every trigger whose payload is not an object save/delete/find shape. Parse Server's webhook response handler resolves +{}+ for all of these (the body is ignored), so the router normalizes their handler result to a success no-op rather than serializing a returned object into the response.

(AUTH_TRIGGERS + LIVE_QUERY_TRIGGERS).freeze
REJECTABLE_NON_OBJECT_TRIGGERS =

The +before*+ subset of NON_OBJECT_TRIGGERS for which a handler can DENY the operation. Parse Server only treats an +error+ response as a rejection -- a +success:false+ body resolves and lets the login / connect / subscribe / reset proceed. So, mirroring the +before_save+ convention, the router converts a +false+ return from one of these into a ResponseError (which serializes to +error+). +error!+ works for any trigger; the +after*+ variants fire after the fact and cannot undo it.

%i[
  before_login before_password_reset_request before_connect before_subscribe
].freeze
HTTP_PARSE_WEBHOOK =

The name of the incoming env containing the webhook key.

"HTTP_X_PARSE_WEBHOOK_KEY"
HTTP_PARSE_APPLICATION_ID =

The name of the incoming env containing the application id key.

"HTTP_X_PARSE_APPLICATION_ID"
CONTENT_TYPE =

The content type that needs to be sent back to Parse server.

"application/json"

Constants included from Registration

Registration::ALLOWED_HOOKS

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registration

register_functions!, register_triggers!, register_webhook!, remove_all_functions!, remove_all_triggers!

Methods included from Client::Connectable

#client

Class Attribute Details

.allow_private_webhook_urlsObject



618
619
620
621
# File 'lib/parse/webhooks.rb', line 618

def allow_private_webhook_urls
  return @allow_private_webhook_urls unless @allow_private_webhook_urls.nil?
  ENV["PARSE_WEBHOOK_ALLOW_PRIVATE_URLS"] == "true"
end

.allow_unauthenticatedObject



601
602
603
604
# File 'lib/parse/webhooks.rb', line 601

def allow_unauthenticated
  return @allow_unauthenticated unless @allow_unauthenticated.nil?
  ENV["PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED"] == "true"
end

.keyString

Returns the configured webhook key if available. By default it will use the value of ENV['PARSE_SERVER_WEBHOOK_KEY'] if not configured.

Returns:



584
585
586
587
588
589
# File 'lib/parse/webhooks.rb', line 584

def key=(value)
  @key = value
  # Reset the warn-once flag so a deployment that configures the key
  # after startup gets a clean state if the key is later cleared.
  @missing_key_warned = nil
end

.loggingBoolean

Returns whether to print additional logging information. You may also set this to :debug for additional verbosity.

Returns:

  • (Boolean)

    whether to print additional logging information. You may also set this to :debug for additional verbosity.



142
143
144
# File 'lib/parse/webhooks.rb', line 142

def logging
  @logging
end

Class Method Details

.call(env) ⇒ Array

Standard Rack call method. This method processes an incoming cloud code webhook request from Parse Server, validates it and executes any registered handlers for it. The result of the handler for the matching webhook request is sent back to Parse Server. If the handler raises a ResponseError, it will return the proper error response.

Parameters:

  • env (Hash)

    the environment hash in a Rack request.

Returns:

Raises:

  • Parse::Webhooks::ResponseError whenever Object, ActiveModel::ValidationError



631
632
633
634
# File 'lib/parse/webhooks.rb', line 631

def call(env)
  # Thraed safety
  dup.call!(env)
end

.call_route(type, className, payload = nil) ⇒ Object

Calls the set of registered webhook trigger blocks or the specific function block. This method is usually called when an incoming request from Parse Server is received.

Parameters:

  • payload (Parse::Webhooks::Payload) (defaults to: nil)

    the payload object received from the server.

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are :before_save, :after_save,

  • className (String)

    if type is not :function, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Returns:

  • (Object)

    the result of the trigger or function.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/parse/webhooks.rb', line 309

def call_route(type, className, payload = nil)
  type = type.to_s.underscore.to_sym #support camelcase
  className = className.parse_class if className.respond_to?(:parse_class)
  className = className.to_s

  return unless routes[type].present? && routes[type][className].present?
  registry = routes[type][className]

  # Track the header-derived ruby_initiated flag on the payload so
  # user code can introspect it (`payload.ruby_initiated?`). For the
  # framework's own callback-deduplication logic below we use the
  # stricter `trusted_ruby_initiated`, which additionally requires the
  # master key. The X-Parse-Request-Id header is client-controllable,
  # so honoring `_RB_` alone would let any client send `_RB_attacker`
  # and trick the framework into skipping server-side callbacks.
  # Server-side Parse-Stack saves use the master key by default, so
  # the AND is a safe condition for legitimate Ruby-initiated traffic.
  if payload
    request_id = payload&.raw&.dig(:headers, "x-parse-request-id") ||
                 payload&.raw&.dig("headers", "x-parse-request-id") ||
                 payload&.raw&.dig(:headers, "X-Parse-Request-Id") ||
                 payload&.raw&.dig("headers", "X-Parse-Request-Id")
    ruby_initiated = request_id&.start_with?("_RB_") || false
    payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
    trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
  else
    ruby_initiated = false
    trusted_ruby_initiated = false
  end

  # Pre-block: apply declarative write protection (guard :field, :mode)
  # to the parse_object that the handler will receive. Running BEFORE
  # the handler block means trusted server-side writes performed inside
  # the block are preserved -- only client-supplied values for guarded
  # fields are reverted.
  #
  # Notably we do NOT gate this on ruby_initiated. That flag derives
  # from a client-controlled X-Parse-Request-Id header, so trusting it
  # to bypass write protection would allow a one-header attack. Master
  # key requests still bypass via the master:/payload.master? check.
  if type == :before_save && payload && payload.object?
    klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
    if klass && klass.respond_to?(:field_guards) && klass.field_guards.any?
      pre_obj = payload.parse_object # memoized; the handler sees this same instance
      if pre_obj.respond_to?(:apply_field_guards!)
        pre_obj.apply_field_guards!(
          master: payload.master? || false,
          is_new: payload.original.blank?
        )
      end
    end
  end

  if registry.is_a?(Array)
    result = registry.map { |hook| invoke_handler(payload, hook) }.last
  else
    result = invoke_handler(payload, registry)
  end

  if result.is_a?(Parse::Object)
    # if it is a Parse::Object, we will call the registered ActiveModel callbacks
    if type == :before_save
      # returning false from the callback block only runs the before_* callback
      # Skip prepare_save! when this request is trusted-Ruby-initiated
      # (both `_RB_` header AND master key), since Parse-Stack already
      # ran ActiveModel before_save callbacks locally. A client-spoofed
      # `_RB_` without master falls through and runs them here.
      unless trusted_ruby_initiated
        before_save_result = result.run_before_save_callbacks
        # If a before_save callback halted the chain (returned false), reject the save.
        if before_save_result == false
          raise Parse::Webhooks::ResponseError, "Save halted by before_save callback"
        end
        # Parse Server exposes no separate beforeCreate trigger, so the
        # beforeSave hook is the single point at which before_create must
        # run for a client-initiated create. Run it AFTER before_save, for
        # new objects only -- matching ActiveModel order (before_save wraps
        # before_create) and mirroring the afterSave hook, which runs
        # after_create then after_save. `original.nil?` marks a create.
        if payload && payload.original.nil?
          create_result = result.run_before_create_callbacks
          if create_result == false
            raise Parse::Webhooks::ResponseError, "Save halted by before_create callback"
          end
        end
      end
      # For before_save, return the changes payload (what Parse Server expects)
      result = result.changes_payload
    elsif type == :before_delete
      result.run_callbacks(:destroy) { false }
      result = true
    end
  elsif type == :before_save && result == false
    # If webhook block returns false, halt the save by throwing an error
    raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
  elsif type == :before_save && (result == true || result.nil?)
    # Open Source Parse server does not accept true results on before_save hooks.
    result = {}
  end

  # Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/
  # afterLogout/beforePasswordResetRequest, beforeConnect/beforeSubscribe/
  # afterEvent). Parse Server IGNORES the response body for all of these --
  # its webhook response handler resolves {} regardless -- so the ONLY way
  # a handler can affect the operation is the error path, and only for the
  # "before" variants (a login/connect/subscribe/reset can be denied; an
  # after_* fires after the fact and cannot be undone).
  #
  # Crucially, Parse Server treats only an {error} response as a rejection:
  # a {success:false} body RESOLVES and lets the operation proceed. So a
  # handler that returns `false` to "deny login" would silently allow it.
  # We mirror the before_save convention and convert that false into a
  # ResponseError (=> {error} => Parse Server denies). `error!` works for
  # any of them (the call! rescue converts it). Every other return value --
  # including a Parse::Object a handler happened to return (e.g. the _User
  # from beforeLogin) -- is normalized to a success no-op so we never
  # serialize an object into the response or the redacted request log.
  if NON_OBJECT_TRIGGERS.include?(type)
    if result == false && REJECTABLE_NON_OBJECT_TRIGGERS.include?(type)
      raise Parse::Webhooks::ResponseError, "#{type} rejected by webhook handler"
    end
    result = true
  end

  # Guard-injection: when a handler returns a Hash (or true/nil normalized
  # to {}) for a class with field_guards, Parse Server would otherwise
  # merge the response with the client's original payload and persist
  # the client-supplied values for guarded fields. Inject the pre-built
  # parse_object's changes_payload entries for any guarded field so the
  # response carries the appropriate revert (Delete op on create, prior
  # value on update). The Parse::Object return path already runs through
  # changes_payload on the same memoized instance and therefore needs no
  # extra injection.
  if type == :before_save && result.is_a?(Hash) && payload && payload.object?
    guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
    if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any?
      pre_obj = payload.parse_object # same memoized instance the pre-block step mutated
      if pre_obj.respond_to?(:changes_payload)
        guard_payload = pre_obj.changes_payload
        field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {}
        guard_klass.field_guards.each_key do |field|
          remote = (field_map[field.to_sym] || field).to_s
          result[remote] = guard_payload[remote] if guard_payload.key?(remote)
        end
      end
    end
  end

  if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
    # The chained ActiveModel after_save/after_create callbacks are NOT
    # fired here. `call!` dispatches every trigger twice -- once for the
    # specific class route and once for the generic `"*"` route -- so
    # firing the model callbacks inside this per-route block double-fired
    # them for any app that registered BOTH a class route and a `"*"`
    # route (e.g. an `after_save :send_email` would send two emails per
    # save). The dispatch now lives in `run_after_save_chain`, which
    # `call!` invokes exactly once per delivery after both route calls.
    #
    # We still normalize the result to `true` so a handler that returned
    # the parse_object (the recommended before_save pattern, easy to copy
    # by mistake) never leaks an object into the response or the log.
    result = true
  end

  result
end

.dispatch_deferred(env, payload)

This method returns an undefined value.

Run any Parse::Webhooks::Payload#after_response callbacks a handler registered, AFTER the response has been produced. Prefers the server's rack.after_reply hook (Puma / Unicorn), which fires once the response is flushed to the socket on the same worker thread; falls back to a detached thread when the server does not provide it (e.g. WEBrick). Each callback is isolated so one raising neither aborts the others nor reaches the client. No-op when nothing was deferred.

Parameters:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/parse/webhooks.rb', line 270

def dispatch_deferred(env, payload)
  return if payload.nil? || !payload.respond_to?(:deferred_callbacks)
  callbacks = payload.deferred_callbacks
  return if callbacks.blank?

  runner = proc do
    callbacks.each do |cb|
      begin
        cb.call
      rescue => e
        warn "[Webhooks::after_response] deferred callback raised: #{e.class}: #{e.message}"
      end
    end
  end

  # Enqueueing must never break an otherwise-successful response: this runs
  # just before `response.finish`, so a raise here (a frozen after_reply
  # array, thread exhaustion) would discard the buffered reply and surface
  # as a 500. Failing to schedule deferred work degrades to "not run",
  # never to a failed response.
  begin
    after_reply = env.is_a?(Hash) ? env["rack.after_reply"] : nil
    if after_reply.respond_to?(:<<)
      after_reply << runner
    else
      Thread.new(&runner)
    end
  rescue => e
    warn "[Webhooks::after_response] could not schedule deferred work: #{e.class}: #{e.message}"
  end
  nil
end

.error(data = false) ⇒ Hash

Generates an error response for Parse Server.

Parameters:

  • data (Object) (defaults to: false)

    the data to send back with the error.

Returns:

  • (Hash)

    a error data payload



576
577
578
# File 'lib/parse/webhooks.rb', line 576

def error(data = false)
  { error: data }.to_json
end

.invoke_handler(payload, block) ⇒ Object

Evaluate a single registered handler block in the scope of the payload.

The block runs with self bound to the Payload, so a handler can call parse_object, params, error!, etc. directly -- exactly as it could under the historical payload.instance_exec(payload, &block) invocation. The difference is the return semantics:

  • return value returns value as the handler result (instead of the LocalJumpError: unexpected return that bare instance_exec raised when the block was defined inside a method).
  • The legacy idioms still work unchanged: the last expression's value, next value, and break value all return value, and raise propagates untouched (so error! / before_save rejections behave the same).

This is achieved by attaching the block as a singleton method on the per-request payload (so return gets method semantics) and removing it afterward. The payload is a per-request instance, so this neither leaks nor mutates shared state across threads.

Arity is matched to the old instance_exec(payload, ...) contract: a zero-arity block (do ... end / proc { }) is called with no args; a block that declares a parameter (do |payload| ... end) or a splat receives the payload.

Parameters:

  • payload (Parse::Webhooks::Payload)

    the request payload (becomes self).

  • block (Proc)

    the registered handler block.

Returns:

  • (Object)

    the handler's result value.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/parse/webhooks.rb', line 233

def invoke_handler(payload, block)
  name = :"__parse_webhook_handler_#{block.object_id}__"
  payload.define_singleton_method(name, &block)
  handler = payload.method(name)
  begin
    # Match the old `payload.instance_exec(payload, &block)` arity
    # leniency: a zero-arity block is called bare; otherwise it receives
    # the payload, plus a nil for each additional REQUIRED positional so a
    # block declaring `|payload, extra|` (or more) does not raise — under
    # instance_exec those surplus params were silently nil. `arity` is
    # negative for optional/splat params (e.g. -1 for `|*a|`, -2 for
    # `|a, *b|`); `~arity` gives the required count in that case.
    if handler.arity == 0
      handler.call
    else
      required = handler.arity.negative? ? ~handler.arity : handler.arity
      handler.call(payload, *Array.new([required - 1, 0].max))
    end
  ensure
    singleton = payload.singleton_class
    if singleton.method_defined?(name) || singleton.private_method_defined?(name)
      singleton.send(:remove_method, name)
    end
  end
end

.route(type, className) { ... } ⇒ OpenStruct

Internally registers a route for a specific webhook trigger or function. :before_delete, :after_delete or :function.

Parameters:

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are :before_save, :after_save,

  • className (String)

    if type is not :function, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Yields:

  • the block that will handle of the webhook trigger or function.

Returns:

  • (OpenStruct)


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/parse/webhooks.rb', line 163

def route(type, className, &block)
  type = type.to_s.underscore.to_sym #support camelcase
  if type != :function && className.respond_to?(:parse_class)
    className = className.parse_class
  end
  className = className.to_s
  # Parse Server has no beforeCreate/afterCreate webhook trigger; the
  # create variants are ActiveModel callbacks that run inside the
  # beforeSave/afterSave handler for new objects. Point callers there
  # rather than registering a route that can never fire.
  if type == :before_create || type == :after_create
    save = type == :before_create ? :before_save : :after_save
    raise ArgumentError,
          "There is no #{type} webhook. Register `webhook :#{save}` instead — " \
          "your #{type} ActiveModel callbacks run inside the #{save} handler " \
          "for new objects (registering #{save} enables BOTH the #{save} and " \
          "#{type} callbacks)."
  end
  if routes[type].nil? || block.respond_to?(:call) == false
    raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
  end

  # AfterSave/AfterDelete hooks support more than one
  if type == :after_save || type == :after_delete
    routes[type][className] ||= []
    routes[type][className].push block
  else
    routes[type][className] = block
  end
  @routes
end

.routesOpenStruct

A hash-like structure composing of all the registered webhook triggers and functions. These are :before_save, :after_save, :before_delete, :after_delete or :function.

Returns:

  • (OpenStruct)


148
149
150
151
152
# File 'lib/parse/webhooks.rb', line 148

def routes
  return @routes unless @routes.nil?
  r = Parse::API::Hooks::TRIGGER_NAMES_LOCAL + [:function]
  @routes = OpenStruct.new(r.reduce({}) { |h, t| h[t] = {}; h })
end

.run_after_save_chain(payload)

This method returns an undefined value.

Fires the chained ActiveModel after_save (and after_create, for a new object) callbacks for an afterSave delivery -- exactly once per request.

This lives in call! rather than call_route because call! dispatches every trigger twice (the specific class route AND the generic "*" route). Firing the model callbacks per-route would double-fire any side effect for an app that registered both routes. Calling this once, after both route calls, fires the chain exactly once regardless of how many routes matched.

The decision to fire depends ONLY on request origin, never on what a handler returned: Parse Server discards the afterSave response body entirely, so a handler returning the parse_object must not suppress the callbacks. For trusted-Ruby-initiated saves (both the _RB_ request-id header AND the master key) Parse Stack's local run_callbacks :save already fires these after the REST response returns, so we skip them here to avoid the double-fire. The route-present guard preserves the "an unregistered afterSave trigger never fires model callbacks" contract that call_route's early return used to provide.

Parameters:



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/parse/webhooks.rb', line 498

def run_after_save_chain(payload)
  return unless payload&.after_save?
  return unless payload.parse_object.is_a?(Parse::Object)

  # Preserve the "no registered route => no model callbacks" behavior that
  # call_route's `return unless routes[type][className].present?` enforced.
  # Mirror that guard exactly: key on parse_class.to_s (as call_route does)
  # and use `.present?` on the value -- registration stores an Array, and an
  # empty/absent registration must NOT fire (matching the original).
  after_save_routes = routes[:after_save]
  return unless after_save_routes &&
                (after_save_routes[payload.parse_class.to_s].present? ||
                 after_save_routes["*"].present?)

  # Trusted-Ruby-initiated saves run their callbacks locally; firing again
  # here would double them. This must match call_route's trusted_ruby_initiated
  # EXACTLY. call_route runs (and stamps @ruby_initiated) before this for any
  # matched route, so read that stamped value rather than recomputing via
  # `ruby_initiated?` -- whose `||=` memoization re-derives on a stamped
  # `false` and could disagree with call_route's header lookup.
  return if payload.ruby_initiated? && payload.master? == true

  # By the time afterSave fires the object is ALREADY persisted in Parse
  # Server, and Parse Server discards the afterSave response body entirely
  # (it resolves success even if the handler throws). So a chained callback
  # that raises must not (a) 500 the webhook endpoint -- `call!`'s rescue
  # only catches ResponseError / ValidationError, so a bare StandardError
  # would escape -- nor (b) take out the OTHER phase's unrelated side
  # effects. Run the after_create and after_save phases independently, each
  # guarded, logging and swallowing any StandardError. This mirrors Parse's
  # own afterSave semantics (log-and-continue on a post-persist failure):
  # a raising `after_create :send_welcome_email` no longer silently skips
  # an unrelated `after_save :reindex`, and neither can crash the endpoint.
  obj = payload.parse_object
  run_after_save_phase(obj, :after_create) if payload.original.nil?
  run_after_save_phase(obj, :after_save)
  nil
end

.run_after_save_phase(obj, phase)

This method returns an undefined value.

Runs one phase (:after_create or :after_save) of an afterSave object's chained ActiveModel callbacks, swallowing and logging any StandardError so a post-persist callback failure can't crash the webhook endpoint or suppress the sibling phase. ActiveModel still halts the rest of this phase's chain on a raise -- only the cross-phase / endpoint blast radius is contained here. Note this also swallows a ResponseError/ValidationError raised from inside an after_save callback: afterSave is post-persist and Parse Server discards the response body, so an error! there cannot deny the (already-committed) write -- it is logged, not propagated.

Parameters:

  • obj (Parse::Object)

    the persisted afterSave object.

  • phase (Symbol)

    :after_create or :after_save.



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/parse/webhooks.rb', line 549

def run_after_save_phase(obj, phase)
  case phase
  when :after_create then obj.run_after_create_callbacks
  when :after_save then obj.run_after_save_callbacks
  end
  nil
rescue => e
  # Redact the exception message before logging: a callback error can echo
  # record contents/tokens, and the rest of this file routes log output
  # through the same redactor.
  warn "[Parse::Webhooks] afterSave #{phase} callback raised for " \
       "#{obj.class}##{obj.id} -- the object is already persisted; " \
       "logging and continuing: #{e.class}: " \
       "#{Parse::Middleware::BodyBuilder.redact(e.message)}"
  nil
end

.run_function(name, params) ⇒ Object

Run a locally registered webhook function. This bypasses calling a function through Parse-Server if the method handler is registered locally.

Returns:

  • (Object)

    the result of the function.



198
199
200
201
202
203
# File 'lib/parse/webhooks.rb', line 198

def run_function(name, params)
  payload = Payload.new
  payload.function_name = name
  payload.params = params
  call_route(:function, name, payload)
end

.success(data = true) ⇒ Hash

Generates a success response for Parse Server.

Parameters:

  • data (Object) (defaults to: true)

    the data to send back with the success.

Returns:

  • (Hash)

    a success data payload



569
570
571
# File 'lib/parse/webhooks.rb', line 569

def success(data = true)
  { success: data }.to_json
end

.trigger_audit(pretty: false, network: true, client: nil, include_framework: false) ⇒ Hash, String

Audit trigger logic across all registered classes, cross-referencing model ActiveModel callbacks, locally registered webhook blocks, and the triggers registered on Parse Server. See TriggerAudit.

The server comparison reads the master-key-only hooks/triggers endpoint, so network: true (the default) requires a master-key client. Pass network: false for a credential-free audit of callbacks vs. local routes.

Parameters:

  • pretty (Boolean) (defaults to: false)

    when true, return the human-readable String summary instead of the Hash report.

  • network (Boolean) (defaults to: true)

    query Parse Server for registered triggers.

  • client (Parse::Client, nil) (defaults to: nil)

    optional client override.

  • include_framework (Boolean) (defaults to: false)

    include gem-internal callbacks.

Returns:

  • (Hash, String)

    the report Hash, or the pretty String when pretty: true.



494
495
496
497
498
499
# File 'lib/parse/webhooks/trigger_audit.rb', line 494

def trigger_audit(pretty: false, network: true, client: nil, include_framework: false)
  audit = TriggerAudit.new(
    network: network, client: client, include_framework: include_framework
  )
  pretty ? audit.pretty : audit.to_h
end

.trigger_class_from_path(path) ⇒ String?

Extract the Parse class name from a webhook request path. Parse Server registers each trigger at <endpoint>/<triggerName>/<className> (functions at <endpoint>/<functionName>), so for a trigger the class is the last segment and the second-to-last is a known trigger name. Returns nil for a function path, a path with no recognizable trigger segment, or a className that fails the conservative charset check (Parse class names are [A-Za-z0-9_], built-ins prefixed with _). The charset gate keeps an attacker-supplied path (reachable when allow_unauthenticated is set) from injecting an arbitrary routing / scrub key.

Parameters:

  • path (String)

    the request PATH_INFO.

Returns:

  • (String, nil)

    the sanitized class name, or nil.



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/parse/webhooks.rb', line 649

def trigger_class_from_path(path)
  segments = path.to_s.split("/").reject(&:empty?)
  return nil if segments.size < 2
  trigger, klass = segments[-2], segments[-1]
  # register_triggers! builds the URL with the LOCAL snake_case trigger
  # name (`after_find`), while Parse Server sends the camelCase form in the
  # body — accept both so the path segment is recognized either way.
  known = (Parse::API::Hooks::TRIGGER_NAMES + Parse::API::Hooks::TRIGGER_NAMES_LOCAL).map(&:to_s)
  return nil unless known.include?(trigger)
  # Allow a leading `@` for the Parse pseudo-classes (`@Connect` for the
  # connection-global LiveQuery trigger, `@File` for file triggers): the
  # SDK encodes the className in the per-trigger URL, so beforeConnect
  # would not route without it. Mirrors the trigger-className validator
  # (Parse::API::PathSegment.trigger_class_name!). Still anchored and
  # charset-limited -- this gate keeps an attacker-supplied path (reachable
  # only under allow_unauthenticated) from injecting an arbitrary routing
  # / scrub key.
  return nil unless /\A@?_?[A-Za-z][A-Za-z0-9_]*\z/.match?(klass)
  klass
end

Instance Method Details

#keyString

The Parse Webhook Key to be used for authenticating webhook requests. See key on setting this value.

Returns:



129
130
131
# File 'lib/parse/webhooks.rb', line 129

def key
  self.class.key
end