2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-11-14 00:30:23 -05:00
|
|
|
require 'base64'
|
2017-05-10 18:16:57 -04:00
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
class Admin::ThemesController < Admin::AdminController
|
|
|
|
|
2019-01-23 09:40:21 -05:00
|
|
|
skip_before_action :check_xhr, only: [:show, :preview, :export]
|
2017-04-14 13:35:12 -04:00
|
|
|
|
|
|
|
def preview
|
2019-04-11 01:56:43 -04:00
|
|
|
theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless theme
|
|
|
|
|
|
|
|
redirect_to path("/?preview_theme_id=#{theme.id}")
|
2017-04-14 13:35:12 -04:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2017-05-09 17:20:28 -04:00
|
|
|
def upload_asset
|
|
|
|
path = params[:file].path
|
2017-12-17 18:40:10 -05:00
|
|
|
|
|
|
|
hijack do
|
|
|
|
File.open(path) do |file|
|
|
|
|
filename = params[:file]&.original_filename || File.basename(path)
|
2019-02-12 09:17:34 -05:00
|
|
|
upload = UploadCreator.new(file, filename, for_theme: true).create_for(theme_user.id)
|
2017-12-17 18:40:10 -05:00
|
|
|
if upload.errors.count > 0
|
|
|
|
render_json_error upload
|
|
|
|
else
|
|
|
|
render json: { upload_id: upload.id }, status: :created
|
|
|
|
end
|
2017-05-09 17:20:28 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-09 00:14:21 -05:00
|
|
|
def generate_key_pair
|
|
|
|
require 'sshkey'
|
|
|
|
k = SSHKey.generate
|
|
|
|
|
|
|
|
render json: {
|
|
|
|
private_key: k.private_key,
|
|
|
|
public_key: k.ssh_public_key
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2019-12-11 13:50:23 -05:00
|
|
|
THEME_CONTENT_TYPES ||= %w{
|
|
|
|
application/gzip
|
|
|
|
application/x-gzip
|
|
|
|
application/x-zip-compressed
|
|
|
|
application/zip
|
|
|
|
}
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
def import
|
|
|
|
@theme = nil
|
2019-01-23 09:40:21 -05:00
|
|
|
if params[:theme] && params[:theme].content_type == "application/json"
|
|
|
|
# .dcstyle.json import. Deprecated, but still available to allow conversion
|
2017-12-14 05:40:14 -05:00
|
|
|
json = JSON::parse(params[:theme].read)
|
2017-04-12 10:52:52 -04:00
|
|
|
theme = json['theme']
|
|
|
|
|
2019-02-12 09:17:34 -05:00
|
|
|
@theme = Theme.new(name: theme["name"], user_id: theme_user.id)
|
2017-04-12 10:52:52 -04:00
|
|
|
theme["theme_fields"]&.each do |field|
|
2017-12-14 05:40:14 -05:00
|
|
|
|
2017-11-14 00:30:23 -05:00
|
|
|
if field["raw_upload"]
|
|
|
|
begin
|
|
|
|
tmp = Tempfile.new
|
|
|
|
tmp.binmode
|
|
|
|
file = Base64.decode64(field["raw_upload"])
|
|
|
|
tmp.write(file)
|
|
|
|
tmp.rewind
|
2019-02-12 09:17:34 -05:00
|
|
|
upload = UploadCreator.new(tmp, field["filename"]).create_for(theme_user.id)
|
2017-11-14 00:30:23 -05:00
|
|
|
field["upload_id"] = upload.id
|
|
|
|
ensure
|
|
|
|
tmp.unlink
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@theme.set_field(
|
|
|
|
target: field["target"],
|
|
|
|
name: field["name"],
|
|
|
|
value: field["value"],
|
|
|
|
type_id: field["type_id"],
|
|
|
|
upload_id: field["upload_id"]
|
|
|
|
)
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
if @theme.save
|
|
|
|
log_theme_change(nil, @theme)
|
|
|
|
render json: @theme, status: :created
|
|
|
|
else
|
|
|
|
render json: @theme.errors, status: :unprocessable_entity
|
|
|
|
end
|
|
|
|
elsif params[:remote]
|
2018-03-09 00:14:21 -05:00
|
|
|
begin
|
2018-10-09 02:01:08 -04:00
|
|
|
branch = params[:branch] ? params[:branch] : nil
|
2019-02-12 09:17:34 -05:00
|
|
|
@theme = RemoteTheme.import_theme(params[:remote], theme_user, private_key: params[:private_key], branch: branch)
|
2018-03-09 00:14:21 -05:00
|
|
|
render json: @theme, status: :created
|
2019-01-23 09:40:21 -05:00
|
|
|
rescue RemoteTheme::ImportError => e
|
|
|
|
render_json_error e.message
|
2018-03-12 03:36:06 -04:00
|
|
|
end
|
2019-12-11 13:50:23 -05:00
|
|
|
elsif params[:bundle] || (params[:theme] && THEME_CONTENT_TYPES.include?(params[:theme].content_type))
|
2019-01-23 09:40:21 -05:00
|
|
|
# params[:bundle] used by theme CLI. params[:theme] used by admin UI
|
|
|
|
bundle = params[:bundle] || params[:theme]
|
2019-01-30 09:17:04 -05:00
|
|
|
theme_id = params[:theme_id]
|
2020-03-26 18:11:56 -04:00
|
|
|
update_components = params[:components]
|
2019-01-30 09:17:04 -05:00
|
|
|
match_theme_by_name = !!params[:bundle] && !params.key?(:theme_id) # Old theme CLI behavior, match by name. Remove Jan 2020
|
2018-03-12 03:36:06 -04:00
|
|
|
begin
|
2020-03-26 18:11:56 -04:00
|
|
|
@theme = RemoteTheme.update_zipped_theme(bundle.path, bundle.original_filename, match_theme: match_theme_by_name, user: theme_user, theme_id: theme_id, update_components: update_components)
|
2019-01-23 09:40:21 -05:00
|
|
|
log_theme_change(nil, @theme)
|
2018-03-12 03:36:06 -04:00
|
|
|
render json: @theme, status: :created
|
2019-01-23 09:40:21 -05:00
|
|
|
rescue RemoteTheme::ImportError => e
|
|
|
|
render_json_error e.message
|
2018-03-09 00:14:21 -05:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
else
|
2019-01-28 06:51:14 -05:00
|
|
|
render_json_error I18n.t("themes.import_error.unknown_file_type"), status: :unprocessable_entity
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def index
|
2018-12-20 12:13:05 -05:00
|
|
|
@themes = Theme.order(:name).includes(:child_themes,
|
2019-01-23 04:20:13 -05:00
|
|
|
:parent_themes,
|
2018-12-20 12:13:05 -05:00
|
|
|
:remote_theme,
|
|
|
|
:theme_settings,
|
|
|
|
:settings_field,
|
2019-03-05 07:10:53 -05:00
|
|
|
:locale_fields,
|
2018-12-20 12:13:05 -05:00
|
|
|
:user,
|
|
|
|
:color_scheme,
|
|
|
|
theme_fields: :upload
|
|
|
|
)
|
|
|
|
@color_schemes = ColorScheme.all.includes(:theme, color_scheme_colors: :color_scheme).to_a
|
2018-07-25 05:25:14 -04:00
|
|
|
light = ColorScheme.new(name: I18n.t("color_schemes.light"))
|
2017-04-12 10:52:52 -04:00
|
|
|
@color_schemes.unshift(light)
|
|
|
|
|
|
|
|
payload = {
|
2018-08-23 21:30:00 -04:00
|
|
|
themes: ActiveModel::ArraySerializer.new(@themes, each_serializer: ThemeSerializer),
|
2017-04-12 10:52:52 -04:00
|
|
|
extras: {
|
|
|
|
color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
respond_to do |format|
|
2017-07-27 21:20:09 -04:00
|
|
|
format.json { render json: payload }
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def create
|
|
|
|
@theme = Theme.new(name: theme_params[:name],
|
2019-02-12 09:17:34 -05:00
|
|
|
user_id: theme_user.id,
|
2017-04-12 10:52:52 -04:00
|
|
|
user_selectable: theme_params[:user_selectable] || false,
|
2018-08-23 21:30:00 -04:00
|
|
|
color_scheme_id: theme_params[:color_scheme_id],
|
|
|
|
component: [true, "true"].include?(theme_params[:component]))
|
2017-04-12 10:52:52 -04:00
|
|
|
set_fields
|
|
|
|
|
|
|
|
respond_to do |format|
|
|
|
|
if @theme.save
|
|
|
|
update_default_theme
|
|
|
|
log_theme_change(nil, @theme)
|
2017-07-27 21:20:09 -04:00
|
|
|
format.json { render json: @theme, status: :created }
|
2017-04-12 10:52:52 -04:00
|
|
|
else
|
|
|
|
format.json { render json: @theme.errors, status: :unprocessable_entity }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def update
|
2019-04-11 01:56:43 -04:00
|
|
|
@theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless @theme
|
2017-04-12 10:52:52 -04:00
|
|
|
|
|
|
|
original_json = ThemeSerializer.new(@theme, root: false).to_json
|
2019-07-03 04:18:11 -04:00
|
|
|
disables_component = [false, "false"].include?(theme_params[:enabled])
|
|
|
|
enables_component = [true, "true"].include?(theme_params[:enabled])
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2019-07-03 04:18:11 -04:00
|
|
|
[:name, :color_scheme_id, :user_selectable, :enabled].each do |field|
|
2017-04-12 10:52:52 -04:00
|
|
|
if theme_params.key?(field)
|
2019-05-06 21:27:05 -04:00
|
|
|
@theme.public_send("#{field}=", theme_params[field])
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if theme_params.key?(:child_theme_ids)
|
2019-11-28 00:19:01 -05:00
|
|
|
add_relative_themes!(:child, theme_params[:child_theme_ids])
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2019-11-28 00:19:01 -05:00
|
|
|
if theme_params.key?(:parent_theme_ids)
|
|
|
|
add_relative_themes!(:parent, theme_params[:parent_theme_ids])
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
set_fields
|
2018-03-04 19:04:23 -05:00
|
|
|
update_settings
|
2019-01-17 06:46:11 -05:00
|
|
|
update_translations
|
2018-08-23 21:30:00 -04:00
|
|
|
handle_switch
|
2017-04-12 10:52:52 -04:00
|
|
|
|
|
|
|
if params[:theme][:remote_check]
|
|
|
|
@theme.remote_theme.update_remote_version
|
|
|
|
end
|
|
|
|
|
|
|
|
if params[:theme][:remote_update]
|
|
|
|
@theme.remote_theme.update_from_remote
|
|
|
|
end
|
|
|
|
|
|
|
|
respond_to do |format|
|
|
|
|
if @theme.save
|
|
|
|
update_default_theme
|
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
@theme.reload
|
2019-07-03 04:18:11 -04:00
|
|
|
|
|
|
|
if (!disables_component && !enables_component) || theme_params.keys.size > 1
|
|
|
|
log_theme_change(original_json, @theme)
|
|
|
|
end
|
|
|
|
log_theme_component_disabled if disables_component
|
|
|
|
log_theme_component_enabled if enables_component
|
|
|
|
|
2018-03-04 19:04:23 -05:00
|
|
|
format.json { render json: @theme, status: :ok }
|
2017-04-12 10:52:52 -04:00
|
|
|
else
|
2018-08-08 00:46:34 -04:00
|
|
|
format.json do
|
|
|
|
error = @theme.errors.full_messages.join(", ").presence
|
|
|
|
error = I18n.t("themes.bad_color_scheme") if @theme.errors[:color_scheme].present?
|
|
|
|
error ||= I18n.t("themes.other_error")
|
2017-04-17 16:55:53 -04:00
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
render json: { errors: [ error ] }, status: :unprocessable_entity
|
2018-08-08 00:46:34 -04:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
end
|
2019-02-05 08:49:16 -05:00
|
|
|
rescue RemoteTheme::ImportError => e
|
|
|
|
render_json_error e.message
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def destroy
|
2019-04-11 01:56:43 -04:00
|
|
|
@theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless @theme
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
StaffActionLogger.new(current_user).log_theme_destroy(@theme)
|
|
|
|
@theme.destroy
|
|
|
|
|
|
|
|
respond_to do |format|
|
|
|
|
format.json { head :no_content }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def show
|
2019-04-11 01:56:43 -04:00
|
|
|
@theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless @theme
|
|
|
|
|
2019-01-23 09:40:21 -05:00
|
|
|
render json: ThemeSerializer.new(@theme)
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2019-01-23 09:40:21 -05:00
|
|
|
def export
|
2019-04-11 01:56:43 -04:00
|
|
|
@theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless @theme
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2019-10-03 09:19:35 -04:00
|
|
|
exporter = ThemeStore::ZipExporter.new(@theme)
|
2019-01-23 09:40:21 -05:00
|
|
|
file_path = exporter.package_filename
|
2019-07-18 08:34:48 -04:00
|
|
|
|
2019-01-23 09:40:21 -05:00
|
|
|
headers['Content-Length'] = File.size(file_path).to_s
|
|
|
|
send_data File.read(file_path),
|
|
|
|
filename: File.basename(file_path),
|
2019-11-20 10:19:16 -05:00
|
|
|
content_type: "application/zip"
|
2019-01-23 09:40:21 -05:00
|
|
|
ensure
|
|
|
|
exporter.cleanup!
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
|
2019-05-02 21:43:54 -04:00
|
|
|
def diff_local_changes
|
|
|
|
theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless theme
|
|
|
|
changes = theme.remote_theme&.diff_local_changes
|
|
|
|
respond_to do |format|
|
|
|
|
format.json { render json: changes || {} }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-06-21 13:49:14 -04:00
|
|
|
def update_single_setting
|
|
|
|
params.require("name")
|
|
|
|
@theme = Theme.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) unless @theme
|
|
|
|
|
|
|
|
setting_name = params[:name].to_sym
|
|
|
|
new_value = params[:value] || nil
|
|
|
|
|
2020-05-01 12:51:11 -04:00
|
|
|
previous_value = @theme.cached_settings[setting_name]
|
2019-06-21 13:49:14 -04:00
|
|
|
@theme.update_setting(setting_name, new_value)
|
|
|
|
@theme.save
|
|
|
|
|
|
|
|
log_theme_setting_change(setting_name, previous_value, new_value)
|
|
|
|
|
2020-05-01 12:51:11 -04:00
|
|
|
updated_setting = @theme.cached_settings.select { |key, val| key == setting_name }
|
2019-06-21 13:49:14 -04:00
|
|
|
render json: updated_setting, status: :ok
|
|
|
|
end
|
|
|
|
|
2017-04-12 10:52:52 -04:00
|
|
|
private
|
|
|
|
|
2019-11-28 00:19:01 -05:00
|
|
|
def add_relative_themes!(kind, ids)
|
|
|
|
expected = ids.map(&:to_i)
|
|
|
|
|
|
|
|
relation = kind == :child ? @theme.child_theme_relation : @theme.parent_theme_relation
|
|
|
|
|
|
|
|
relation.to_a.each do |relative|
|
|
|
|
if kind == :child && expected.include?(relative.child_theme_id)
|
|
|
|
expected.reject! { |id| id == relative.child_theme_id }
|
|
|
|
elsif kind == :parent && expected.include?(relative.parent_theme_id)
|
|
|
|
expected.reject! { |id| id == relative.parent_theme_id }
|
|
|
|
else
|
|
|
|
relative.destroy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
Theme.where(id: expected).each do |theme|
|
|
|
|
@theme.add_relative_theme!(kind, theme)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-07 01:28:18 -04:00
|
|
|
def update_default_theme
|
|
|
|
if theme_params.key?(:default)
|
|
|
|
is_default = theme_params[:default].to_s == "true"
|
2018-07-12 00:18:21 -04:00
|
|
|
if @theme.id == SiteSetting.default_theme_id && !is_default
|
2018-06-07 01:28:18 -04:00
|
|
|
Theme.clear_default!
|
|
|
|
elsif is_default
|
|
|
|
@theme.set_default!
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2018-06-07 01:28:18 -04:00
|
|
|
def theme_params
|
|
|
|
@theme_params ||=
|
|
|
|
begin
|
|
|
|
# deep munge is a train wreck, work around it for now
|
|
|
|
params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids)
|
2019-11-28 00:19:01 -05:00
|
|
|
params[:theme][:parent_theme_ids] ||= [] if params[:theme].key?(:parent_theme_ids)
|
2018-06-07 01:28:18 -04:00
|
|
|
|
|
|
|
params.require(:theme).permit(
|
|
|
|
:name,
|
|
|
|
:color_scheme_id,
|
|
|
|
:default,
|
|
|
|
:user_selectable,
|
2018-08-23 21:30:00 -04:00
|
|
|
:component,
|
2019-07-03 04:18:11 -04:00
|
|
|
:enabled,
|
2018-06-07 01:28:18 -04:00
|
|
|
settings: {},
|
2019-01-17 06:46:11 -05:00
|
|
|
translations: {},
|
2018-06-07 01:28:18 -04:00
|
|
|
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
2019-11-28 00:19:01 -05:00
|
|
|
child_theme_ids: [],
|
|
|
|
parent_theme_ids: []
|
2017-05-09 17:20:28 -04:00
|
|
|
)
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def set_fields
|
|
|
|
return unless fields = theme_params[:theme_fields]
|
|
|
|
|
|
|
|
fields.each do |field|
|
|
|
|
@theme.set_field(
|
|
|
|
target: field[:target],
|
|
|
|
name: field[:name],
|
|
|
|
value: field[:value],
|
|
|
|
type_id: field[:type_id],
|
|
|
|
upload_id: field[:upload_id]
|
|
|
|
)
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2018-06-07 01:28:18 -04:00
|
|
|
def update_settings
|
|
|
|
return unless target_settings = theme_params[:settings]
|
2018-03-04 19:04:23 -05:00
|
|
|
|
2018-06-07 01:28:18 -04:00
|
|
|
target_settings.each_pair do |setting_name, new_value|
|
|
|
|
@theme.update_setting(setting_name.to_sym, new_value)
|
2018-03-04 19:04:23 -05:00
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2018-03-04 19:04:23 -05:00
|
|
|
|
2019-01-17 06:46:11 -05:00
|
|
|
def update_translations
|
|
|
|
return unless target_translations = theme_params[:translations]
|
|
|
|
|
|
|
|
target_translations.each_pair do |translation_key, new_value|
|
|
|
|
@theme.update_translation(translation_key, new_value)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-07 01:28:18 -04:00
|
|
|
def log_theme_change(old_record, new_record)
|
|
|
|
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
|
2019-06-21 13:49:14 -04:00
|
|
|
def log_theme_setting_change(setting_name, previous_value, new_value)
|
|
|
|
StaffActionLogger.new(current_user).log_theme_setting_change(setting_name, previous_value, new_value, @theme)
|
|
|
|
end
|
|
|
|
|
2019-07-03 04:18:11 -04:00
|
|
|
def log_theme_component_disabled
|
|
|
|
StaffActionLogger.new(current_user).log_theme_component_disabled(@theme)
|
|
|
|
end
|
|
|
|
|
|
|
|
def log_theme_component_enabled
|
|
|
|
StaffActionLogger.new(current_user).log_theme_component_enabled(@theme)
|
|
|
|
end
|
|
|
|
|
2018-08-23 21:30:00 -04:00
|
|
|
def handle_switch
|
|
|
|
param = theme_params[:component]
|
|
|
|
if param.to_s == "false" && @theme.component?
|
|
|
|
@theme.switch_to_theme!
|
|
|
|
elsif param.to_s == "true" && !@theme.component?
|
|
|
|
@theme.switch_to_component!
|
|
|
|
end
|
|
|
|
end
|
2019-02-12 09:17:34 -05:00
|
|
|
|
|
|
|
# Overridden by theme-creator plugin
|
|
|
|
def theme_user
|
|
|
|
current_user
|
|
|
|
end
|
2017-04-12 10:52:52 -04:00
|
|
|
end
|