AlchemyCMS/alchemy_cms

View on GitHub
app/controllers/alchemy/pages_controller.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

module Alchemy
  class PagesController < Alchemy::BaseController
    SHOW_PAGE_PARAMS_KEYS = [
      "action",
      "controller",
      "urlname",
      "locale"
    ]

    include OnPageLayout::CallbacksRunner

    # Redirecting concerns. Order is important here!
    include SiteRedirects

    before_action :enforce_no_locale,
      if: :locale_prefix_not_allowed?,
      only: [:index, :show]

    before_action :load_index_page, only: [:index]
    before_action :load_page, only: [:show]

    # Legacy page redirects need to run after the page was loaded and before we render 404.
    include LegacyPageRedirects

    # From here on, we need a published +@page+ to work with!
    before_action :page_not_found!, unless: -> { @page&.public? }, only: [:index, :show]

    # Page redirects need to run after the page was loaded and we're sure to have a public +@page+ set.
    before_action :enforce_locale,
      if: :locale_prefix_missing?,
      only: [:index, :show]

    # Page layout callbacks need to run after all other callbacks
    before_action :run_on_page_layout_callbacks,
      if: :run_on_page_layout_callbacks?,
      only: [:index, :show]

    before_action :set_expiration_headers, only: [:index, :show], if: -> { @page }

    rescue_from ActionController::UnknownFormat, with: :page_not_found!

    # == The index action gets invoked if one requests '/' or '/:locale'
    #
    # If the locale is the default locale, then it redirects to '/' without the locale.
    #
    # Loads the current language root page. The current language is either loaded via :locale
    # parameter or, if that's missing, the default language is used.
    #
    # If this page is not published then it redirects to the first published descendant it finds.
    #
    # If no public page can be found it renders a 404 error.
    #
    def index
      show
    end

    # == The show action gets invoked if one requests '/:urlname' or '/:locale/:urlname'
    #
    # If the locale is the default locale, then it redirects to '/' without the locale.
    #
    # Loads the page via it's urlname. If more than one language is published the
    # current language is either loaded via :locale parameter or, if that's missing,
    # the page language is used and a redirect to the page with prefixed locale happens.
    #
    # If the requested page is not published then it redirects to the first published
    # descendant it finds. If no public page can be found it renders a 404 error.
    #
    def show
      authorize! :show, @page
      render_page if render_fresh_page?
    end

    # Renders a search engine compatible xml sitemap.
    def sitemap
      @pages = Page.sitemap
      respond_to do |format|
        format.xml { render layout: "alchemy/sitemap" }
      end
    end

    private

    # Redirects to requested action without locale prefixed
    def enforce_no_locale
      redirect_permanently_to additional_params.merge(locale: nil)
    end

    # Is the requested locale allowed?
    #
    # If Alchemy is not in multi language mode or the requested locale is the default locale,
    # then we want to redirect to a non prefixed url.
    #
    def locale_prefix_not_allowed?
      params[:locale].present? && !multi_language? ||
        params[:locale].presence == ::I18n.default_locale.to_s
    end

    # == Loads index page
    #
    # Loads the current public language root page.
    #
    # If no index page and no admin users are present we show the "Welcome to Alchemy" page.
    #
    def load_index_page
      @page ||= Language.current_root_page
      Current.page = @page
      render template: "alchemy/welcome", layout: false if signup_required?
    end

    # == Loads page by urlname
    #
    # If a locale is specified in the request parameters,
    # scope pages to it to make sure we can raise a 404 if the urlname
    # is not available in that language.
    #
    # @return Alchemy::Page
    # @return NilClass
    #
    def load_page
      page_not_found! unless Current.language

      @page ||= Current.language.pages.contentpages.find_by(
        urlname: params[:urlname],
        language_code: params[:locale] || Current.language.code
      )
      Current.page = @page
    end

    def enforce_locale
      redirect_permanently_to page_locale_redirect_url(locale: Current.language.code)
    end

    def locale_prefix_missing?
      multi_language? && params[:locale].blank? && !default_locale?
    end

    def default_locale?
      Current.language.code.to_sym == ::I18n.default_locale.to_sym
    end

    # Page url with or without locale while keeping all additional params
    def page_locale_redirect_url(options = {})
      options = {
        locale: prefix_locale? ? @page.language_code : nil,
        urlname: @page.urlname
      }.merge(options)

      alchemy.show_page_path additional_params.merge(options)
    end

    # Redirects to given url with 301 status
    def redirect_permanently_to(url)
      redirect_to url, status: :moved_permanently
    end

    # Returns url parameters that are not internal show page params.
    #
    # * action
    # * controller
    # * urlname
    # * locale
    #
    def additional_params
      params.to_unsafe_hash.delete_if do |key, _|
        SHOW_PAGE_PARAMS_KEYS.include?(key)
      end
    end

    # == Renders the page :show template
    #
    # Handles html requests
    #
    # Omits the layout, if the request is a XHR request.
    #
    def render_page
      respond_to do |format|
        format.html do
          render action: :show, layout: !request.xhr?
        end
      end
    end

    def set_expiration_headers
      if must_not_cache?
        expires_now
      else
        expires_in @page.expiration_time, public: !@page.restricted, must_revalidate: true
      end
    end

    def signup_required?
      if Alchemy.user_class.respond_to?(:admins)
        Alchemy.user_class.admins.empty? && @page.nil?
      end
    end

    # Returns the etag used for response headers.
    #
    # If a user is logged in, we append theirs etag to prevent caching of user related content.
    #
    # IMPORTANT:
    #
    # If your user does not have a +cache_key_with_version+ method (i.e. it's not an ActiveRecord model),
    # you have to ensure to implement it and return a unique identifier for that particular user.
    # Otherwise all users will see the same cached page, regardless of user's state.
    #
    def page_etag
      [@page, current_alchemy_user]
    end

    # We only render the page if either the cache is disabled for this page
    # or the cache is stale, because it's been republished by the user.
    #
    def render_fresh_page?
      must_not_cache? || stale?(
        etag: page_etag,
        last_modified: @page.last_modified_at,
        public: !@page.restricted,
        template: "pages/show"
      )
    end

    # don't cache pages if we have flash message to display or the page has caching disabled
    def must_not_cache?
      flash.present? || !@page.cache_page?
    end

    def page_not_found!
      not_found_error!("Alchemy::Page not found \"#{request.fullpath}\"")
    end
  end
end