mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 10:54:22 +08:00
d8bd3c32ca
Currently the theme is matched by name, which can be fragile when there are many themes with the same name. This functionality will be used by the next version of theme CLI.
250 lines
7.8 KiB
Ruby
250 lines
7.8 KiB
Ruby
require_dependency 'theme_store/git_importer'
|
|
require_dependency 'theme_store/tgz_importer'
|
|
require_dependency 'upload_creator'
|
|
|
|
class RemoteTheme < ActiveRecord::Base
|
|
METADATA_PROPERTIES = %i{
|
|
license_url
|
|
about_url
|
|
authors
|
|
theme_version
|
|
minimum_discourse_version
|
|
maximum_discourse_version
|
|
}
|
|
|
|
class ImportError < StandardError; end
|
|
|
|
ALLOWED_FIELDS = %w{scss embedded_scss head_tag header after_header body_tag footer}
|
|
|
|
GITHUB_REGEXP = /^https?:\/\/github\.com\//
|
|
GITHUB_SSH_REGEXP = /^git@github\.com:/
|
|
|
|
has_one :theme
|
|
scope :joined_remotes, -> {
|
|
joins("JOIN themes ON themes.remote_theme_id = remote_themes.id").where.not(remote_url: "")
|
|
}
|
|
|
|
validates_format_of :minimum_discourse_version, :maximum_discourse_version, with: Discourse::VERSION_REGEXP, allow_nil: true
|
|
|
|
def self.extract_theme_info(importer)
|
|
JSON.parse(importer["about.json"])
|
|
rescue TypeError, JSON::ParserError
|
|
raise ImportError.new I18n.t("themes.import_error.about_json")
|
|
end
|
|
|
|
def self.update_tgz_theme(filename, match_theme: false, user: Discourse.system_user, theme_id: nil)
|
|
importer = ThemeStore::TgzImporter.new(filename)
|
|
importer.import!
|
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
|
theme = Theme.find_by(name: theme_info["name"]) if match_theme # Old theme CLI method, remove Jan 2020
|
|
theme = Theme.find_by(id: theme_id) if theme_id # New theme CLI method
|
|
theme ||= Theme.new(user_id: user&.id || -1, name: theme_info["name"])
|
|
|
|
theme.component = theme_info["component"].to_s == "true"
|
|
|
|
remote_theme = new
|
|
remote_theme.theme = theme
|
|
remote_theme.remote_url = ""
|
|
remote_theme.update_from_remote(importer, skip_update: true)
|
|
|
|
theme.save!
|
|
theme
|
|
ensure
|
|
begin
|
|
importer.cleanup!
|
|
rescue => e
|
|
Rails.logger.warn("Failed cleanup remote path #{e}")
|
|
end
|
|
end
|
|
|
|
def self.import_theme(url, user = Discourse.system_user, private_key: nil, branch: nil)
|
|
importer = ThemeStore::GitImporter.new(url.strip, private_key: private_key, branch: branch)
|
|
importer.import!
|
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
|
component = [true, "true"].include?(theme_info["component"])
|
|
theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"], component: component)
|
|
|
|
remote_theme = new
|
|
theme.remote_theme = remote_theme
|
|
|
|
remote_theme.private_key = private_key
|
|
remote_theme.branch = branch
|
|
remote_theme.remote_url = importer.url
|
|
remote_theme.update_from_remote(importer)
|
|
|
|
theme.save!
|
|
theme
|
|
ensure
|
|
begin
|
|
importer.cleanup!
|
|
rescue => e
|
|
Rails.logger.warn("Failed cleanup remote git #{e}")
|
|
end
|
|
end
|
|
|
|
def self.out_of_date_themes
|
|
self.joined_remotes.where("commits_behind > 0 OR remote_version <> local_version")
|
|
.pluck("themes.name", "themes.id")
|
|
end
|
|
|
|
def self.unreachable_themes
|
|
self.joined_remotes.where("last_error_text IS NOT NULL").pluck("themes.name", "themes.id")
|
|
end
|
|
|
|
def update_remote_version
|
|
return unless is_git?
|
|
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key, branch: branch)
|
|
begin
|
|
importer.import!
|
|
rescue RemoteTheme::ImportError => err
|
|
self.last_error_text = err.message
|
|
else
|
|
self.updated_at = Time.zone.now
|
|
self.remote_version, self.commits_behind = importer.commits_since(local_version)
|
|
self.last_error_text = nil
|
|
end
|
|
end
|
|
|
|
def update_from_remote(importer = nil, skip_update: false)
|
|
cleanup = false
|
|
|
|
unless importer
|
|
cleanup = true
|
|
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key, branch: branch)
|
|
begin
|
|
importer.import!
|
|
rescue RemoteTheme::ImportError => err
|
|
self.last_error_text = err.message
|
|
return self
|
|
else
|
|
self.last_error_text = nil
|
|
end
|
|
end
|
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
|
|
|
theme_info["assets"]&.each do |name, relative_path|
|
|
if path = importer.real_path(relative_path)
|
|
new_path = "#{File.dirname(path)}/#{SecureRandom.hex}#{File.extname(path)}"
|
|
File.rename(path, new_path) # OptimizedImage has strict file name restrictions, so rename temporarily
|
|
upload = UploadCreator.new(File.open(new_path), File.basename(relative_path), for_theme: true).create_for(theme.user_id)
|
|
theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
|
|
end
|
|
end
|
|
|
|
METADATA_PROPERTIES.each do |property|
|
|
self.public_send(:"#{property}=", theme_info[property.to_s])
|
|
end
|
|
if !self.valid?
|
|
raise ImportError, I18n.t("themes.import_error.about_json_values", errors: self.errors.full_messages.join(","))
|
|
end
|
|
|
|
importer.all_files.each do |filename|
|
|
next unless opts = ThemeField.opts_from_file_path(filename)
|
|
value = importer[filename]
|
|
theme.set_field(opts.merge(value: value))
|
|
end
|
|
|
|
if !skip_update
|
|
self.remote_updated_at = Time.zone.now
|
|
self.remote_version = importer.version
|
|
self.local_version = importer.version
|
|
self.commits_behind = 0
|
|
end
|
|
|
|
update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component
|
|
|
|
self
|
|
ensure
|
|
begin
|
|
importer.cleanup! if cleanup
|
|
rescue => e
|
|
Rails.logger.warn("Failed cleanup remote git #{e}")
|
|
end
|
|
end
|
|
|
|
def normalize_override(hex)
|
|
return unless hex
|
|
|
|
override = hex.downcase
|
|
if override !~ /\A[0-9a-f]{6}\z/
|
|
override = nil
|
|
end
|
|
override
|
|
end
|
|
|
|
def update_theme_color_schemes(theme, schemes)
|
|
missing_scheme_names = Hash[*theme.color_schemes.pluck(:name, :id).flatten]
|
|
|
|
schemes&.each do |name, colors|
|
|
missing_scheme_names.delete(name)
|
|
existing = theme.color_schemes.find_by(name: name)
|
|
if existing
|
|
existing.colors.each do |c|
|
|
override = normalize_override(colors[c.name])
|
|
if override && c.hex != override
|
|
c.hex = override
|
|
theme.notify_color_change(c)
|
|
end
|
|
end
|
|
else
|
|
scheme = theme.color_schemes.build(name: name)
|
|
ColorScheme.base.colors_hashes.each do |color|
|
|
override = normalize_override(colors[color[:name]])
|
|
scheme.color_scheme_colors << ColorSchemeColor.new(name: color[:name], hex: override || color[:hex])
|
|
end
|
|
end
|
|
end
|
|
|
|
if missing_scheme_names.length > 0
|
|
ColorScheme.where(id: missing_scheme_names.values).delete_all
|
|
# we may have stuff pointed at the incorrect scheme?
|
|
end
|
|
end
|
|
|
|
def github_diff_link
|
|
if github_repo_url.present? && local_version != remote_version
|
|
"#{github_repo_url.gsub(/\.git$/, "")}/compare/#{local_version}...#{remote_version}"
|
|
end
|
|
end
|
|
|
|
def github_repo_url
|
|
url = remote_url.strip
|
|
return url if url.match?(GITHUB_REGEXP)
|
|
|
|
if url.match?(GITHUB_SSH_REGEXP)
|
|
org_repo = url.gsub(GITHUB_SSH_REGEXP, "")
|
|
"https://github.com/#{org_repo}"
|
|
end
|
|
end
|
|
|
|
def is_git?
|
|
remote_url.present?
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: remote_themes
|
|
#
|
|
# id :integer not null, primary key
|
|
# remote_url :string not null
|
|
# remote_version :string
|
|
# local_version :string
|
|
# about_url :string
|
|
# license_url :string
|
|
# commits_behind :integer
|
|
# remote_updated_at :datetime
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# private_key :text
|
|
# branch :string
|
|
# last_error_text :text
|
|
# authors :string
|
|
# theme_version :string
|
|
# minimum_discourse_version :string
|
|
# maximum_discourse_version :string
|
|
#
|