2018-03-12 15:36:06 +08:00
|
|
|
require_dependency 'theme_store/git_importer'
|
|
|
|
require_dependency 'theme_store/tgz_importer'
|
2017-05-11 06:16:57 +08:00
|
|
|
require_dependency 'upload_creator'
|
2017-04-12 22:52:52 +08:00
|
|
|
|
|
|
|
class RemoteTheme < ActiveRecord::Base
|
2019-01-25 22:19:01 +08:00
|
|
|
METADATA_PROPERTIES = %i{
|
|
|
|
license_url
|
|
|
|
about_url
|
|
|
|
authors
|
|
|
|
theme_version
|
|
|
|
minimum_discourse_version
|
|
|
|
maximum_discourse_version
|
|
|
|
}
|
2017-05-03 04:01:01 +08:00
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
class ImportError < StandardError; end
|
|
|
|
|
2017-05-03 04:01:01 +08:00
|
|
|
ALLOWED_FIELDS = %w{scss embedded_scss head_tag header after_header body_tag footer}
|
|
|
|
|
2018-08-06 13:29:15 +08:00
|
|
|
GITHUB_REGEXP = /^https?:\/\/github\.com\//
|
|
|
|
GITHUB_SSH_REGEXP = /^git@github\.com:/
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
has_one :theme
|
2018-09-08 21:24:11 +08:00
|
|
|
scope :joined_remotes, -> {
|
|
|
|
joins("JOIN themes ON themes.remote_theme_id = remote_themes.id").where.not(remote_url: "")
|
|
|
|
}
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2019-01-25 22:19:01 +08:00
|
|
|
validates_format_of :minimum_discourse_version, :maximum_discourse_version, with: Discourse::VERSION_REGEXP, allow_nil: true
|
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
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)
|
2018-03-12 15:36:06 +08:00
|
|
|
importer = ThemeStore::TgzImporter.new(filename)
|
|
|
|
importer.import!
|
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
|
|
|
theme = Theme.find_by(name: theme_info["name"]) if match_theme
|
2018-03-12 15:36:06 +08:00
|
|
|
theme ||= Theme.new(user_id: user&.id || -1, name: theme_info["name"])
|
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
theme.component = theme_info["component"].to_s == "true"
|
|
|
|
|
2018-03-12 15:36:06 +08:00
|
|
|
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
|
|
|
|
|
2018-10-09 14:01:08 +08:00
|
|
|
def self.import_theme(url, user = Discourse.system_user, private_key: nil, branch: nil)
|
2018-12-17 22:27:49 +08:00
|
|
|
importer = ThemeStore::GitImporter.new(url.strip, private_key: private_key, branch: branch)
|
2017-04-12 22:52:52 +08:00
|
|
|
importer.import!
|
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
2018-08-24 09:30:00 +08:00
|
|
|
component = [true, "true"].include?(theme_info["component"])
|
|
|
|
theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"], component: component)
|
2017-04-12 22:52:52 +08:00
|
|
|
|
|
|
|
remote_theme = new
|
|
|
|
theme.remote_theme = remote_theme
|
|
|
|
|
2018-03-09 13:14:21 +08:00
|
|
|
remote_theme.private_key = private_key
|
2018-10-09 14:01:08 +08:00
|
|
|
remote_theme.branch = branch
|
2017-04-12 22:52:52 +08:00
|
|
|
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
|
|
|
|
|
2018-08-03 07:53:48 +08:00
|
|
|
def self.out_of_date_themes
|
2018-09-08 21:24:11 +08:00
|
|
|
self.joined_remotes.where("commits_behind > 0 OR remote_version <> local_version")
|
2018-08-03 07:53:48 +08:00
|
|
|
.pluck("themes.name", "themes.id")
|
|
|
|
end
|
|
|
|
|
2018-09-08 21:24:11 +08:00
|
|
|
def self.unreachable_themes
|
|
|
|
self.joined_remotes.where("last_error_text IS NOT NULL").pluck("themes.name", "themes.id")
|
|
|
|
end
|
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
def update_remote_version
|
2019-01-23 22:40:21 +08:00
|
|
|
return unless is_git?
|
2018-10-09 14:01:08 +08:00
|
|
|
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key, branch: branch)
|
2018-09-08 21:24:11 +08:00
|
|
|
begin
|
|
|
|
importer.import!
|
2019-01-23 22:40:21 +08:00
|
|
|
rescue RemoteTheme::ImportError => err
|
2018-09-08 21:24:11 +08:00
|
|
|
self.last_error_text = err.message
|
|
|
|
else
|
|
|
|
self.updated_at = Time.zone.now
|
|
|
|
self.remote_version, self.commits_behind = importer.commits_since(local_version)
|
2018-09-10 23:17:56 +08:00
|
|
|
self.last_error_text = nil
|
2018-09-08 21:24:11 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2018-03-12 15:36:06 +08:00
|
|
|
def update_from_remote(importer = nil, skip_update: false)
|
2017-04-12 22:52:52 +08:00
|
|
|
cleanup = false
|
2017-04-18 03:56:13 +08:00
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
unless importer
|
|
|
|
cleanup = true
|
2018-10-09 14:01:08 +08:00
|
|
|
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key, branch: branch)
|
2018-09-08 21:24:11 +08:00
|
|
|
begin
|
|
|
|
importer.import!
|
2019-01-23 22:40:21 +08:00
|
|
|
rescue RemoteTheme::ImportError => err
|
2018-09-08 21:24:11 +08:00
|
|
|
self.last_error_text = err.message
|
|
|
|
return self
|
2018-09-10 23:17:56 +08:00
|
|
|
else
|
|
|
|
self.last_error_text = nil
|
2018-09-08 21:24:11 +08:00
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
theme_info = RemoteTheme.extract_theme_info(importer)
|
2017-05-10 05:20:28 +08:00
|
|
|
|
|
|
|
theme_info["assets"]&.each do |name, relative_path|
|
|
|
|
if path = importer.real_path(relative_path)
|
2019-01-18 23:20:11 +08:00
|
|
|
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)
|
2017-05-10 05:20:28 +08:00
|
|
|
theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-01-25 22:19:01 +08:00
|
|
|
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
|
2018-03-12 15:36:06 +08:00
|
|
|
|
2019-01-23 22:40:21 +08:00
|
|
|
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
|
|
|
|
|
2018-03-12 15:36:06 +08:00
|
|
|
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
|
2017-04-12 22:52:52 +08:00
|
|
|
|
2018-08-24 09:30:00 +08:00
|
|
|
update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component
|
2017-04-18 03:56:13 +08:00
|
|
|
|
2017-04-12 22:52:52 +08:00
|
|
|
self
|
|
|
|
ensure
|
|
|
|
begin
|
|
|
|
importer.cleanup! if cleanup
|
|
|
|
rescue => e
|
|
|
|
Rails.logger.warn("Failed cleanup remote git #{e}")
|
|
|
|
end
|
|
|
|
end
|
2017-04-18 03:56:13 +08:00
|
|
|
|
|
|
|
def normalize_override(hex)
|
|
|
|
return unless hex
|
|
|
|
|
|
|
|
override = hex.downcase
|
2017-04-18 04:57:37 +08:00
|
|
|
if override !~ /\A[0-9a-f]{6}\z/
|
2017-04-18 03:56:13 +08:00
|
|
|
override = nil
|
|
|
|
end
|
|
|
|
override
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_theme_color_schemes(theme, schemes)
|
2018-03-15 15:26:54 +08:00
|
|
|
missing_scheme_names = Hash[*theme.color_schemes.pluck(:name, :id).flatten]
|
2017-04-18 03:56:13 +08:00
|
|
|
|
2018-03-16 08:19:06 +08:00
|
|
|
schemes&.each do |name, colors|
|
2018-03-15 15:26:54 +08:00
|
|
|
missing_scheme_names.delete(name)
|
2017-04-18 03:56:13 +08:00
|
|
|
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
|
2018-03-15 15:26:54 +08:00
|
|
|
|
|
|
|
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
|
2017-04-18 03:56:13 +08:00
|
|
|
end
|
2018-03-15 15:26:54 +08:00
|
|
|
|
2018-08-06 13:29:15 +08:00
|
|
|
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
|
2019-01-23 22:40:21 +08:00
|
|
|
|
|
|
|
def is_git?
|
|
|
|
remote_url.present?
|
|
|
|
end
|
2017-04-12 22:52:52 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: remote_themes
|
|
|
|
#
|
2019-01-30 09:34:51 +08:00
|
|
|
# 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
|
2017-04-12 22:52:52 +08:00
|
|
|
#
|