mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 12:43:54 -06:00
0e303c7f5d
This change automatically resizes icons for various purposes. Admins can now upload `logo` and `logo_small`, and everything else will be auto-generated. Specific icons can still be uploaded separately if required. ## Core - Adds an SiteIconManager module which manages automatic resizing and fallback - Icons are looked up in the OptimizedImage table at runtime, and then cached in Redis. If the resized version is missing for some reason, then most icons will fall back to the original files. Some icons (e.g. PWA Manifest) will return `nil` (because an incorrectly sized icon is worse than a missing icon). - `SiteSetting.site_large_icon_url` will return the optimized version, including any fallback. `SiteSetting.large_icon` continues to return the upload object. This means that (almost) no changes are required in core/plugins to support this new system. - Icons are resized whenever a relevant site setting is changed, and during post-deploy migrations ## Wizard - Allows `requiresRefresh` wizard steps to reload data via AJAX instead of a full page reload - Add placeholders to the **icons** step of the wizard, which automatically update from the "Square Logo" - Various copy updates to support the changes - Remove the "upload-time" resizing for `large_icon`. This is no longer required. ## Site Settings UX - Move logo/icon settings under a new "Branding" tab - Various copy changes to support the changes - Adds placeholder support to the `image-uploader` component - Automatically reloads site settings after saving. This allows setting placeholders to change based on changes to other settings - Upload site settings will be assigned a placeholder if SiteIconManager `responds_to?` an icon of the same name ## Dashboard Warnings - Remove PWA icon and PWA title warnings. Both are now handled automatically. ## Bonus - Updated the sketch logos to use @awesomerobot's new high-res designs
248 lines
8.1 KiB
Ruby
248 lines
8.1 KiB
Ruby
require_dependency 'distributed_memoizer'
|
|
require_dependency 'file_helper'
|
|
|
|
class StaticController < ApplicationController
|
|
|
|
skip_before_action :check_xhr, :redirect_to_login_if_required
|
|
skip_before_action :verify_authenticity_token, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset]
|
|
skip_before_action :preload_json, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset]
|
|
skip_before_action :handle_theme, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset]
|
|
|
|
PAGES_WITH_EMAIL_PARAM = ['login', 'password_reset', 'signup']
|
|
MODAL_PAGES = ['password_reset', 'signup']
|
|
|
|
def show
|
|
return redirect_to(path '/') if current_user && (params[:id] == 'login' || params[:id] == 'signup')
|
|
if SiteSetting.login_required? && current_user.nil? && ['faq', 'guidelines'].include?(params[:id])
|
|
return redirect_to path('/login')
|
|
end
|
|
|
|
map = {
|
|
"faq" => { redirect: "faq_url", topic_id: "guidelines_topic_id" },
|
|
"tos" => { redirect: "tos_url", topic_id: "tos_topic_id" },
|
|
"privacy" => { redirect: "privacy_policy_url", topic_id: "privacy_topic_id" }
|
|
}
|
|
|
|
@page = params[:id]
|
|
|
|
if map.has_key?(@page)
|
|
site_setting_key = map[@page][:redirect]
|
|
url = SiteSetting.send(site_setting_key)
|
|
return redirect_to(url) unless url.blank?
|
|
end
|
|
|
|
# The /guidelines route ALWAYS shows our FAQ, ignoring the faq_url site setting.
|
|
@page = 'faq' if @page == 'guidelines'
|
|
|
|
# Don't allow paths like ".." or "/" or anything hacky like that
|
|
@page.gsub!(/[^a-z0-9\_\-]/, '')
|
|
|
|
if map.has_key?(@page)
|
|
@topic = Topic.find_by_id(SiteSetting.send(map[@page][:topic_id]))
|
|
raise Discourse::NotFound unless @topic
|
|
title_prefix = if I18n.exists?("js.#{@page}")
|
|
I18n.t("js.#{@page}")
|
|
else
|
|
@topic.title
|
|
end
|
|
@title = "#{title_prefix} - #{SiteSetting.title}"
|
|
@body = @topic.posts.first.cooked
|
|
@faq_overriden = !SiteSetting.faq_url.blank?
|
|
render :show, layout: !request.xhr?, formats: [:html]
|
|
return
|
|
end
|
|
|
|
if I18n.exists?("static.#{@page}")
|
|
render html: I18n.t("static.#{@page}"), layout: !request.xhr?, formats: [:html]
|
|
return
|
|
end
|
|
|
|
if PAGES_WITH_EMAIL_PARAM.include?(@page) && params[:email]
|
|
cookies[:email] = { value: params[:email], expires: 1.day.from_now }
|
|
end
|
|
|
|
file = "static/#{@page}.#{I18n.locale}"
|
|
file = "static/#{@page}.en" if lookup_context.find_all("#{file}.html").empty?
|
|
file = "static/#{@page}" if lookup_context.find_all("#{file}.html").empty?
|
|
|
|
if lookup_context.find_all("#{file}.html").any?
|
|
render file, layout: !request.xhr?, formats: [:html]
|
|
return
|
|
end
|
|
|
|
if MODAL_PAGES.include?(@page)
|
|
render html: nil, layout: true
|
|
return
|
|
end
|
|
|
|
raise Discourse::NotFound
|
|
end
|
|
|
|
# This method just redirects to a given url.
|
|
# It's used when an ajax login was successful but we want the browser to see
|
|
# a post of a login form so that it offers to remember your password.
|
|
def enter
|
|
params.delete(:username)
|
|
params.delete(:password)
|
|
|
|
destination = path("/")
|
|
|
|
if params[:redirect].present? && !params[:redirect].match(login_path)
|
|
begin
|
|
forum_uri = URI(Discourse.base_url)
|
|
uri = URI(params[:redirect])
|
|
|
|
if uri.path.present? &&
|
|
(uri.host.blank? || uri.host == forum_uri.host) &&
|
|
uri.path !~ /\./
|
|
|
|
destination = uri.path
|
|
destination = "#{uri.path}?#{uri.query}" if uri.path =~ /new-topic/ || uri.path =~ /new-message/ || uri.path =~ /user-api-key/
|
|
end
|
|
rescue URI::Error
|
|
# Do nothing if the URI is invalid
|
|
end
|
|
end
|
|
|
|
redirect_to destination
|
|
end
|
|
|
|
FAVICON ||= -"favicon"
|
|
|
|
# We need to be able to draw our favicon on a canvas, this happens when you enable the feature
|
|
# that draws the notification count on top of favicon (per user default off)
|
|
#
|
|
# With s3 the original upload is going to be stored at s3, we don't have a local copy of the favicon.
|
|
# To allow canvas to work with s3 we are going to need to add special CORS headers and use
|
|
# a special crossorigin hint on the original, this is not easily workable.
|
|
#
|
|
# Forcing all consumers to set magic CORS headers on a CDN is also not workable for us.
|
|
#
|
|
# So we cache the favicon in redis and serve it out real quick with
|
|
# a huge expiry, we also cache these assets in nginx so it is bypassed if needed
|
|
def favicon
|
|
is_asset_path
|
|
|
|
hijack do
|
|
data = DistributedMemoizer.memoize("FAVICON#{SiteIconManager.favicon_url}", 60 * 30) do
|
|
favicon = SiteIconManager.favicon
|
|
next "" unless favicon
|
|
|
|
if Discourse.store.external?
|
|
begin
|
|
file = FileHelper.download(
|
|
UrlHelper.absolute(favicon.url),
|
|
max_file_size: favicon.filesize,
|
|
tmp_file_name: FAVICON,
|
|
follow_redirect: true
|
|
)
|
|
|
|
file&.read || ""
|
|
rescue => e
|
|
AdminDashboardData.add_problem_message('dashboard.bad_favicon_url', 1800)
|
|
Rails.logger.warn("Failed to fetch faivcon #{favicon.url}: #{e}\n#{e.backtrace}")
|
|
ensure
|
|
file&.unlink
|
|
end
|
|
else
|
|
File.read(Rails.root.join("public", favicon.url[1..-1]))
|
|
end
|
|
end
|
|
|
|
if data.bytesize == 0
|
|
@@default_favicon ||= File.read(Rails.root + "public/images/default-favicon.png")
|
|
response.headers["Content-Length"] = @@default_favicon.bytesize.to_s
|
|
render body: @@default_favicon, content_type: "image/png"
|
|
else
|
|
immutable_for 1.year
|
|
response.headers["Expires"] = 1.year.from_now.httpdate
|
|
response.headers["Content-Length"] = data.bytesize.to_s
|
|
response.headers["Last-Modified"] = Time.new('2000-01-01').httpdate
|
|
render body: data, content_type: "image/png"
|
|
end
|
|
end
|
|
end
|
|
|
|
def brotli_asset
|
|
is_asset_path
|
|
|
|
serve_asset(".br") do
|
|
response.headers["Content-Encoding"] = 'br'
|
|
end
|
|
end
|
|
|
|
def cdn_asset
|
|
is_asset_path
|
|
|
|
serve_asset
|
|
end
|
|
|
|
def service_worker_asset
|
|
is_asset_path
|
|
|
|
respond_to do |format|
|
|
format.js do
|
|
# https://github.com/w3c/ServiceWorker/blob/master/explainer.md#updating-a-service-worker
|
|
# Maximum cache that the service worker will respect is 24 hours.
|
|
# However, ensure that these may be cached and served for longer on servers.
|
|
immutable_for 1.year
|
|
|
|
if Rails.application.assets_manifest.assets['service-worker.js']
|
|
path = File.expand_path(Rails.root + "public/assets/#{Rails.application.assets_manifest.assets['service-worker.js']}")
|
|
response.headers["Last-Modified"] = File.ctime(path).httpdate
|
|
end
|
|
render(
|
|
plain: Rails.application.assets_manifest.find_sources('service-worker.js').first,
|
|
content_type: 'application/javascript'
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def serve_asset(suffix = nil)
|
|
|
|
path = File.expand_path(Rails.root + "public/assets/#{params[:path]}#{suffix}")
|
|
|
|
# SECURITY what if path has /../
|
|
raise Discourse::NotFound unless path.start_with?(Rails.root.to_s + "/public/assets")
|
|
|
|
response.headers["Expires"] = 1.year.from_now.httpdate
|
|
response.headers["Access-Control-Allow-Origin"] = params[:origin] if params[:origin]
|
|
|
|
begin
|
|
response.headers["Last-Modified"] = File.ctime(path).httpdate
|
|
rescue Errno::ENOENT
|
|
begin
|
|
if GlobalSetting.fallback_assets_path.present?
|
|
path = File.expand_path("#{GlobalSetting.fallback_assets_path}/#{params[:path]}#{suffix}")
|
|
response.headers["Last-Modified"] = File.ctime(path).httpdate
|
|
else
|
|
raise
|
|
end
|
|
rescue Errno::ENOENT
|
|
expires_in 1.second, public: true, must_revalidate: false
|
|
|
|
render plain: "can not find #{params[:path]}", status: 404
|
|
return
|
|
end
|
|
end
|
|
|
|
response.headers["Content-Length"] = File.size(path).to_s
|
|
|
|
yield if block_given?
|
|
|
|
immutable_for 1.year
|
|
|
|
# disable NGINX mucking with transfer
|
|
request.env['sendfile.type'] = ''
|
|
|
|
opts = { disposition: nil }
|
|
opts[:type] = "application/javascript" if params[:path] =~ /\.js$/
|
|
send_file(path, opts)
|
|
|
|
end
|
|
|
|
end
|