Class: ApplicationController

Overview

Controller: application.

Constant Summary

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Instance Method Summary collapse

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #registration_lead_type, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#account_impersonated?Boolean

Pretender does not ship a boolean helper, but it does set
session[:impersonated_account_id] whenever an impersonation is active
(see vendor/bundle/.../pretender.rb). We define one here as the canonical
check used everywhere in the app (views, MasqueradeGuarded, AnalyticsEvents,
AppsignalTagging, etc.).

Returns:

  • (Boolean)


18
19
20
# File 'app/controllers/application_controller.rb', line 18

def 
  request.session[:impersonated_account_id].present?
end

#add_to_flash(message, type = :error, now: false) ⇒ void (protected)

This method returns an undefined value.

Append a message to flash[type], deduping against what's already there.

Pass now: true for one-shot messages set on a render (rather than a
redirect) — they'll show on this request only, the same way flash.now
works. Without it, the message persists into the next request, which
leaks onto the back/forward target after a Turbo Drive cache miss.

Parameters:

  • message (String, nil)

    the message to append

  • type (Symbol) (defaults to: :error)

    the flash type/key (:error, :info, etc.)

  • now (Boolean) (defaults to: false)

    whether to use flash.now instead of flash



180
181
182
183
184
185
186
# File 'app/controllers/application_controller.rb', line 180

def add_to_flash(message, type = :error, now: false)
  return if message.blank? || (flash[type].present? && flash[type].to_s.downcase.include?(message.to_s.downcase))

  bucket = now ? flash.now : flash
  bucket[type] ||= +''
  bucket[type] += message
end

#after_sign_in_path_for(resource) ⇒ String

Default post-authentication landing for storefront flows. Every storefront
auth controller (sessions, registrations, passwords, invitations, omniauth)
already redirects to @devise_return_path || my_account_path on success;
this override applies the same rule for the implicit call sites Devise
makes directly — most importantly require_no_authentication, which lands
an already-signed-in user back here when they click a password-reset
email link. Without it, Devise's gem default root_path sent them to the
homepage with no signal. CRM falls through to super because the
employee subdomain has no my_account route.

Parameters:

  • resource (Account)

    the Devise resource that just authenticated

Returns:

  • (String)

    storefront: @devise_return_path when set, else my_account_path. CRM: Devise's default (usually root_path).



93
94
95
96
97
98
99
100
101
102
103
104
# File 'app/controllers/application_controller.rb', line 93

def (resource)
  # A stale / legacy-format session can hand us the raw serialized Warden value
  # (`[[id], salt]`) instead of an Account. Calling `super` then explodes deep in
  # `Devise::Mapping.find_scope!` ("Could not find a valid mapping for [[id], …]"),
  # 500ing the first sign-in after a session-format change (it self-heals on the
  # next request once the fresh session is set). Fall back to a safe path instead.
  return @devise_return_path.presence || root_path unless resource.is_a?(Account)

  return super if is_crm_request?

  @devise_return_path.presence || 
end

#bypass_forgery_protection?Boolean (protected)

Returns true if CSRF protection should be skipped for this request.

Returns:

  • (Boolean)

    true if CSRF protection should be skipped for this request



123
124
125
126
127
128
129
# File 'app/controllers/application_controller.rb', line 123

def bypass_forgery_protection?
  return true if json_request?
  # Because the user might be presented with a captcha, this can break the csrf check, so this is a workaround.
  return true if cloudflare_cleared?

  false
end

#chat_enabled?Boolean (protected)

Returns true if the live-chat widget should render.

Returns:

  • (Boolean)

    true if the live-chat widget should render



144
145
146
# File 'app/controllers/application_controller.rb', line 144

def chat_enabled?
  Rails.env.production? || ENV['ENABLE_LIVECHAT'].to_b
end

#cloudflare_cleared?Boolean (protected)

Returns true if Cloudflare has already cleared this browser (has a cf_clearance cookie).

Returns:

  • (Boolean)

    true if Cloudflare has already cleared this browser (has a cf_clearance cookie)



117
118
119
120
# File 'app/controllers/application_controller.rb', line 117

def cloudflare_cleared?
  # See https://developers.cloudflare.com/fundamentals/reference/policies-compliances/cloudflare-cookies/
  request.cookies['cf_clearance'].present?
end

#default_catalogCatalog (protected)

Returns the current user's catalog, or the locale's default.

Returns:

  • (Catalog)

    the current user's catalog, or the locale's default



165
166
167
# File 'app/controllers/application_controller.rb', line 165

def default_catalog
  current_user.try(:catalog) || Catalog.locale_to_catalog(I18n.locale)
end

#default_url_options(options = {}) ⇒ Hash (protected)

Default URL options applied app-wide.

Parameters:

Options Hash (options):

  • host (String)

    Hostname for generated URLs

  • protocol (String)

    URL protocol (e.g. 'https')

Returns:

  • (Hash)

    Merged default URL options



209
210
211
# File 'app/controllers/application_controller.rb', line 209

def default_url_options(options = {})
  UrlHelper.default_url_options(options)
end

#enable_turbo_framesObject

Enable Turbo Frames and Streams for specific sections



77
78
79
# File 'app/controllers/application_controller.rb', line 77

def enable_turbo_frames
  @turbo_frames_enabled = true
end

#find_publication(sku, override_locale_store = nil) ⇒ Publication? (protected)

Returns the item's publication in the resolved store.

Parameters:

  • sku (String)

    the item SKU to find a publication for

  • override_locale_store (Store, nil) (defaults to: nil)

    store to look up in, defaulting to #locale_store

Returns:

  • (Publication, nil)

    the item's publication in the resolved store



159
160
161
162
# File 'app/controllers/application_controller.rb', line 159

def find_publication(sku, override_locale_store = nil)
  store = override_locale_store || locale_store
  Item.find_publication(sku, store)
end

#fix_invalid_accept_headerObject (protected)

Clean possibly misconfigured proxies or old browsers



297
298
299
300
301
# File 'app/controllers/application_controller.rb', line 297

def fix_invalid_accept_header
  return unless is_crm_request? || is_www_request?

  request.headers['Accept'] = '*/*' if request.headers['Accept'] == '*'
end

#init_js_utilsvoid (protected)

This method returns an undefined value.

Initializes the props hash React components read via server-to-client-data.



139
140
141
# File 'app/controllers/application_controller.rb', line 139

def init_js_utils
  @react_props = {}
end

#is_globals_call?Boolean (protected)

Returns true if this request is for the /globals endpoint.

Returns:

  • (Boolean)

    true if this request is for the /globals endpoint



132
133
134
# File 'app/controllers/application_controller.rb', line 132

def is_globals_call?
  request&.fullpath.to_s.start_with?('/globals')
end

#layout_by_resourceString, false (protected)

Returns the layout name to render, or false to skip the layout.

Returns:

  • (String, false)

    the layout name to render, or false to skip the layout



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/controllers/application_controller.rb', line 228

def layout_by_resource
  # Turbo Frame requests only consume the matching <turbo-frame> from the
  # response; the surrounding layout chrome (navbar, sidebar, head/asset
  # tags, modal containers) is rendered then immediately discarded by the
  # browser. Skip it server-side to save render time and bandwidth on every
  # frame fetch, app-wide.
  #
  # Exception: tab-frame breakouts. A navigation inside a tab pane that
  # lands on a page whose own tab frame id differs (an order link inside a
  # warehouse tab, an item link inside another item's kit tab) deliberately
  # misses the requesting frame so the client's turbo:frame-missing handler
  # promotes the response to a full Drive visit via `visit(response)`. That
  # promotion renders the fetched body as the whole page — without the
  # layout the user lands on a bare, chrome-less fragment.
  return false if skip_layout_for_turbo_frame?

  if devise_controller?
    if is_crm_request?
      @disable_sign_in = true
      'crm/sessions'
    else
      'www/cms_page'
    end
  elsif is_crm_request?
    'crm/crm'
  else
    'www/cms_page'
  end
end

#locale_storeStore (protected)

Returns the store for the current I18n locale.

Returns:

  • (Store)

    the store for the current I18n locale



214
215
216
# File 'app/controllers/application_controller.rb', line 214

def locale_store
  Store.locale_to_catalog(I18n.locale)
end

#redirect_to(options = {}, response_options = {}) ⇒ void

Note:

explicitly public because this override sits below the protected keyword

This method returns an undefined value.

Hotwire: default redirects after mutating verbs to 303 See Other unless an explicit
status: is passed (matches Turbo / Rails 7 guidance for POST/PUT/PATCH/DELETE flows).

above. Without this, the inherited public redirect_to becomes protected on the
whole app, and any gem that calls controller.redirect_to(...) externally (e.g. the
responders gem used by Devise's respond_with) raises NoMethodError.

Parameters:

  • options (Hash, String) (defaults to: {})

    The URL or url_for options to redirect to

  • response_options (Hash) (defaults to: {})

    Response options forwarded to super

Options Hash (options):

  • status (Symbol)

    Explicit redirect status (skips the 303 default)

Options Hash (response_options):

  • status (Symbol)

    HTTP status for the redirect



364
365
366
367
368
369
370
# File 'app/controllers/application_controller.rb', line 364

public def redirect_to(options = {}, response_options = {})
  response_options = {} unless response_options.is_a?(Hash)
  unless response_options.key?(:status) || (options.is_a?(Hash) && options.key?(:status))
    response_options = { status: :see_other }.merge(response_options) if %w[POST PUT PATCH DELETE].include?(request.request_method)
  end
  super
end

#require_employee_for_crmObject (protected)

Gate: every request on the CRM subdomain must be from an authenticated employee.
Controllers with legitimate open access (webhooks, OAuth, token downloads) must
explicitly declare: skip_before_action :require_employee_for_crm



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'app/controllers/application_controller.rb', line 261

def require_employee_for_crm
  return unless is_crm_request?
  return if devise_controller?

  # `current_account` is wrapped by Pretender, which can return a non-Account
  # value when the session is partially corrupt (e.g. `:impersonated_account_id`
  # set with no `true_account`). `&.is_employee?` only short-circuits on nil,
  # so an Array/Hash receiver crashes the request with a 500 instead of
  # redirecting to sign-in. Coerce anything that isn't an Account to nil.
   =  if .is_a?(Account)

  return if &.is_employee?

  if 
    flash[:error] = "Access to the CRM is restricted to employees."
    redirect_to cms_link('/my_account')
  else
    flash[:info] = t('controllers.authenticable.sign_in_to_proceed')
    redirect_to (devise_return_path: request.fullpath)
  end
end

#set_base_hostvoid (protected)

This method returns an undefined value.



284
285
286
# File 'app/controllers/application_controller.rb', line 284

def set_base_host
  @base_url = "#{request.scheme}://#{request.host_with_port}"
end

#set_real_ipvoid (protected)

This method returns an undefined value.

Fake our location in development when working locally, this is what
request geocoder uses.



292
293
294
# File 'app/controllers/application_controller.rb', line 292

def set_real_ip
  request.env['HTTP_X_REAL_IP'] = NetworkConstants::REAL_FAKE_IP if Rails.env.development?
end

#set_report_errors_forvoid (protected)

This method returns an undefined value.

Stub, override in your own controllers, put the models that are updated
in the array, note use instance variable @ not symbols.



222
223
224
225
# File 'app/controllers/application_controller.rb', line 222

def set_report_errors_for
  @report_errors_for = []
  logger.debug "set_report_errors_for is set to #{@report_errors_for.join(', ')}" if @report_errors_for.present?
end

#should_render_layout?Boolean (protected)

Determines if a layout should be rendered based on the request type.

Returns false (no layout) for:

  • XHR requests (legacy AJAX approach)
  • Turbo Frame fetches that target this controller's content (e.g.
    Turbo-Frame: tab-content-<controller_name> or any non-tab frame
    like pagination scopes)

Returns true (full layout) for:

  • Plain navigation
  • Tab-frame breakouts (see #tab_frame_breakout_request?): clicking an
    order link from inside the warehouse tab, or an item link from inside
    another item's kit tab. These trigger turbo:frame-missing
    client-side, and the global handler's visit(response) fallback needs
    a complete HTML page to render. A layoutless response leaves the user
    staring at just the body content (no chrome, no nav, no breadcrumb).

Returns:

  • (Boolean)


319
320
321
322
323
324
# File 'app/controllers/application_controller.rb', line 319

def should_render_layout?
  return false if request.xhr?
  return false if request.format.turbo_stream?

  !skip_layout_for_turbo_frame?
end

#skip_layout_for_turbo_frame?Boolean (protected)

In-pane Turbo Frame fetches skip the layout; breakouts (and plain
navigation) keep it. Single source of truth shared by
layout_by_resource and should_render_layout? so the app-wide layout
decision and the explicit render layout: call sites can't drift.

Returns:

  • (Boolean)


330
331
332
# File 'app/controllers/application_controller.rb', line 330

def skip_layout_for_turbo_frame?
  turbo_frame_request? && !tab_frame_breakout_request?
end

#stamp_impersonation_contextvoid (protected)

This method returns an undefined value.

Records the true (pre-impersonation) account id so downstream logging/
AR-versioning can attribute changes to the real actor while masquerading.



112
113
114
# File 'app/controllers/application_controller.rb', line 112

def stamp_impersonation_context
  CurrentScope. =  ? &.id : nil
end

#tab_frame_breakout_request?Boolean (protected)

A Turbo Frame fetch from inside a tab-content-* pane whose response
will render under a DIFFERENT tab frame id — i.e. an in-frame navigation
that must break out to a full-page visit instead of slotting into the
pane. Tab frame ids are instance-scoped (tab-content-items-13140, see
ApplicationHelper#tab_frame_id), so this covers both cross-controller
clicks (order link inside a warehouse tab) and same-controller record
navigation (item link inside another item's kit-contents tab). The
mismatched response fires turbo:frame-missing client-side; the global
handler upgrades it to a Drive visit of the already-fetched page — which
is why layout_by_resource keeps the full layout on these responses.

Returns:

  • (Boolean)


344
345
346
347
348
349
# File 'app/controllers/application_controller.rb', line 344

def tab_frame_breakout_request?
  header = request.headers['Turbo-Frame'].to_s
  return false unless header.start_with?('tab-content-')

  helpers.tab_frame_id != header
end

#warmlyyours_canada_ip?(ip = nil) ⇒ Boolean (protected)

Returns true if the IP belongs to WarmlyYours' Canadian office.

Parameters:

  • ip (String, nil) (defaults to: nil)

    IP to check, defaulting to the request's remote IP

Returns:

  • (Boolean)

    true if the IP belongs to WarmlyYours' Canadian office



199
200
201
202
# File 'app/controllers/application_controller.rb', line 199

def warmlyyours_canada_ip?(ip = nil)
  ip ||= request.remote_ip
  IpDetector.warmlyyours_canada_ip?(ip)
end

#warmlyyours_ip?(ip = nil) ⇒ Boolean (protected)

Returns true if the IP belongs to WarmlyYours.

Parameters:

  • ip (String, nil) (defaults to: nil)

    IP to check, defaulting to the request's remote IP

Returns:

  • (Boolean)

    true if the IP belongs to WarmlyYours



192
193
194
195
# File 'app/controllers/application_controller.rb', line 192

def warmlyyours_ip?(ip = nil)
  ip ||= request.remote_ip
  IpDetector.warmlyyours_ip?(ip)
end

#y(value) ⇒ String (protected)

Renders a loosely-typed value as "Yes"/"No" for view display.

Parameters:

  • value (Object)

    any value coercible via .to_b

Returns:

  • (String)

    "Yes" or "No"



152
153
154
# File 'app/controllers/application_controller.rb', line 152

def y(value)
  value.to_b ? 'Yes' : 'No'
end