Class: PluginRoutes

Inherits:
Object
  • Object
show all
Defined in:
lib/plugin_routes.rb,
lib/generators/camaleon_cms/install_template/plugin_routes.rb

Class Method Summary collapse

Class Method Details

.add_after_reload_routes(command) ⇒ Object

Add a callable (Proc/Lambda) to run after routes reload; strings are not supported.

Raises:

  • (ArgumentError)


202
203
204
205
206
# File 'lib/plugin_routes.rb', line 202

def add_after_reload_routes(command)
  raise(ArgumentError, 'Expected a callable (Proc/Lambda), not a String') if command.is_a?(String)

  after_reload_callbacks << command
end

.add_anonymous_hook(hook_key, callback, hook_id = '') ⇒ Object

add a new anonymous hook sample: PluginRoutes.add_anonymous_hook('before_admin', lambda{|params| puts params })

Parameters:

  • hook_key (String)

    , key of hook

  • hook_id (String) (defaults to: '')

    , identifier for the anonymous hook

  • callback (Lambda)

    , anonymous function to be called when the hook was called

Returns:

  • nil



58
59
60
# File 'lib/plugin_routes.rb', line 58

def add_anonymous_hook(hook_key, callback, hook_id = '')
  (anonymous_hooks[hook_key] ||= []) << { id: hook_id, callback: callback }
end

.all_appsObject

return all apps loaded



422
423
424
# File 'lib/plugin_routes.rb', line 422

def all_apps
  all_plugins + all_themes
end

.all_enabled_appsObject

return all enabled apps as []: system, themes, plugins



233
234
235
# File 'lib/plugin_routes.rb', line 233

def all_enabled_apps
  [system_info] + all_enabled_themes + all_enabled_plugins
end

.all_enabled_pluginsObject

return all enabled plugins (a theme is enabled if at least one site has installed)



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/plugin_routes.rb', line 253

def all_enabled_plugins
  r = cache_variable('all_enabled_plugins')
  return r if r.present? # an empty [] must not stick as a cache hit -- see all_enabled_themes

  # Empty get_sites (the DB is not ready during early boot, or there simply are no sites) means
  # no DB work: gate the batched query on it here, as one explicit early return, so the query
  # never runs against a table that may not exist yet and aborts route loading. get_sites rescues
  # to [] on any DB error, so this single guard keeps the whole draw path safe -- and any batched
  # query added below inherits it, rather than each repeating a per-call emptiness check.
  return [] if get_sites.empty?

  # One query for every enabled plugin slug across all sites, rather than a
  # `site.plugins.active.pluck` per site (the N+1 that dominated multi-site route drawing). A
  # subquery over the site ids avoids marshaling every id into an IN(...) bind list, which can
  # exceed SQLite's SQLITE_MAX_VARIABLE_NUMBER on very large multi-site installs; Postgres and
  # MySQL handle either form.
  enabled_ps = CamaleonCms::Plugin.active.where(parent_id: CamaleonCms::Site.select(:id)).distinct.pluck(:slug)
  res = all_plugins.each_with_object([]) do |plugin, ary|
    ary << plugin if enabled_ps.include?(plugin['key'])
  end
  cache_variable('all_enabled_plugins', res)
end

.all_enabled_themesObject

return all enabled themes (a theme is enabled if at least one site is assigned)



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/plugin_routes.rb', line 238

def all_enabled_themes
  r = cache_variable('all_enabled_themes')
  # Do not treat an empty result as a cache hit (an empty [] is truthy): if a draw runs before
  # the DB is ready, get_sites returns [] and this would otherwise cache [] permanently, matching
  # the self-healing all_plugins/all_themes below rather than the sticky-empty behavior.
  return r if r.present?

  res = get_sites.each_with_object([]) do |site, ary|
    i = theme_info(site.get_theme_slug)
    ary << i if i.present?
  end
  cache_variable('all_enabled_themes', res)
end

.all_helpersObject

all helpers of enabled plugins



286
287
288
289
290
291
292
# File 'lib/plugin_routes.rb', line 286

def all_helpers
  r = cache_variable('plugins_helper')
  return r if r

  res = all_apps.filter_map { |settings| settings['helpers'].presence }.flatten
  cache_variable('plugins_helper', res.uniq)
end

.all_localesObject

return all locales for all sites joined by |



337
338
339
340
341
342
343
344
345
# File 'lib/plugin_routes.rb', line 337

def all_locales
  r = cache_variable('site_all_locales')
  # A blank '' must not stick as a hit: an empty all_locales makes the frontend
  # `locale: /#{all_locales}/` constraint an empty regex `//` that matches anything.
  return r if r.present?

  res = get_sites.flat_map(&:get_languages)
  cache_variable('site_all_locales', res.uniq.join('|'))
end

.all_pluginsObject

return all plugins located in cms and in this project



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/plugin_routes.rb', line 359

def all_plugins
  camaleon_gem = get_gem('camaleon_cms')
  return [] unless camaleon_gem

  r = cache_variable('all_plugins')
  return r if r.present?

  res = get_gem_plugins
  entries = %w[. ..]
  res.each { |plugin| entries << plugin['key'] }
  (Dir["#{apps_dir}/plugins/*"] + Dir["#{camaleon_gem.gem_dir}/app/apps/plugins/*"]).each do |path|
    entry = path.split('/').last
    config = File.join(path, 'config', 'config.json')
    next if entries.include?(entry) || !File.directory?(path) || !File.exist?(config)

    p = JSON.parse(File.read(config))
    p = begin
      p.with_indifferent_access
    rescue StandardError
      p
    end
    p['key'] = entry
    p['path'] = path
    p['kind'] = 'plugin'
    res << p
    entries << entry
  end
  cache_variable('all_plugins', res)
end

.all_themesObject

return an array of all themes installed for all sites



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/plugin_routes.rb', line 390

def all_themes
  camaleon_gem = get_gem('camaleon_cms')
  return [] unless camaleon_gem

  r = cache_variable('all_themes')
  return r if r.present?

  res = get_gem_themes
  entries = %w[. ..]
  res.each { |theme| entries << theme['key'] }
  Dir["#{apps_dir}/themes/*"].each do |path|
    entry = path.split('/').last
    config = File.join(path, 'config', 'config.json')
    next if entries.include?(entry) || !File.directory?(path) || !File.exist?(config)

    p = JSON.parse(File.read(config))
    p = begin
      p.with_indifferent_access
    rescue StandardError
      p
    end
    p['key'] = entry
    p['path'] = path
    p['kind'] = 'theme'
    p['title'] = p['name']
    res << p
    entries << entry
  end
  cache_variable('all_themes', res)
end

.all_translations(key, *args) ⇒ Object

return all translations for all languages, sample: ['Sample', 'Ejemplo', '....']



348
349
350
351
# File 'lib/plugin_routes.rb', line 348

def all_translations(key, *args)
  args = args.extract_options!
  all_locales.split('|').map { |_l| I18n.t(key, **args.merge({ locale: _l })) }.uniq
end

.apps_dirObject

return apps directory path



354
355
356
# File 'lib/plugin_routes.rb', line 354

def apps_dir
  @apps_dir ||= Rails.root.join('app/apps').to_s
end

.cache_variable(var_name, value = nil) ⇒ Object



304
305
306
307
308
309
310
311
312
# File 'lib/plugin_routes.rb', line 304

def cache_variable(var_name, value = nil)
  reload_monitor.synchronize do
    if value.nil?
      cache[var_name]
    else
      cache[var_name] = value
    end
  end
end

.db_installed?Boolean

check if db migrate already done

Returns:

  • (Boolean)


332
333
334
# File 'lib/plugin_routes.rb', line 332

def db_installed?
  @db_installed ||= ActiveRecord::Base.connection.table_exists?(CamaleonCms::Site.table_name)
end

.default_url_optionsObject

return the default url options for Camaleon CMS



483
484
485
486
487
488
489
490
491
# File 'lib/plugin_routes.rb', line 483

def default_url_options
  options = { host: begin
    CamaleonCms::Site.main_site.slug
  rescue StandardError
    ''
  end }
  options[:protocol] = 'https' if Rails.application.config.force_ssl
  options
end

.destroy_plugin(plugin_key) ⇒ Object

destroy plugin



295
296
297
298
299
300
301
302
# File 'lib/plugin_routes.rb', line 295

def destroy_plugin(plugin_key)
  begin
    FileUtils.rm_r(Rails.root.join('app', 'apps', 'plugins', plugin_key))
  rescue StandardError
    nil
  end
  PluginRoutes.reload
end

.draw_gemsObject

draw "all" gems registered for the plugins or themes and camaleon gems



4
5
6
7
8
9
10
# File 'lib/generators/camaleon_cms/install_template/plugin_routes.rb', line 4

def self.draw_gems
  gemfiles = Dir["#{apps_dir}/{plugins,themes}/*/config/Gemfile"]

  # map + read + join is very memory efficient for strings
  # Ensuring a newline between files prevents syntax errors if a file lacks a trailing newline
  gemfiles.map { |gem| File.read(gem, encoding: 'UTF-8') }.join("\n\n")
end

.draw_routes_eagerlyObject

Draw the route table at boot so the first request is never served against a half-built set of routes. Production already eager-loads routes; this closes the development window where the first request after a restart races the (multi-site) draw. Guarded by db_installed? and rescued so boot never aborts when the DB is unavailable (migrations, asset precompile, or a fresh install before db:create).



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/plugin_routes.rb', line 180

def draw_routes_eagerly
  return if Rails.application.config.eager_load # production draws routes at boot already
  # Skip while a db: Rake task runs (db:migrate, db:schema:load, app:db:test:prepare, ...): it
  # boots against a schema that is mid-change, so drawing here would query that schema -- and it
  # never serves a request that could race the draw. Other non-server processes (console,
  # workers) still draw: Rails exposes no reliable, version-stable "is this the web server?"
  # signal, and the draw is otherwise bounded and safe (a single guarded, rescued, idempotent
  # draw over long-stable columns), so scoping it further is not worth a fragile gate.
  return if running_db_rake_task?
  return unless db_installed?

  Rails.application.reload_routes!
rescue ActiveRecord::ActiveRecordError => e
  # Only swallow database-unavailability (migrations, asset precompile, a fresh install before
  # db:create) so boot never aborts on it. A route-file syntax error, a raised constraint lambda
  # or any other bug in the draw is NOT a boot-safety concern and must surface, not be logged and
  # hidden here where nothing would ever redraw and reveal it.
  Rails.logger&.warn("Camaleon CMS: skipped eager route draw at boot (#{e.class}: #{e.message})")
  nil
end

.enabled_apps(site, theme_slug = nil) ⇒ Object

return all enabled apps for site (themes + system + plugins) [] theme_slug: current theme slug



223
224
225
226
227
228
229
230
# File 'lib/plugin_routes.rb', line 223

def enabled_apps(site, theme_slug = nil)
  theme_slug ||= site.get_theme_slug
  r = cache_variable("enabled_apps_#{site.id}_#{theme_slug}")
  return r if r

  res = [system_info] + enabled_plugins(site) + [theme_info(theme_slug)]
  cache_variable("enabled_apps_#{site.id}_#{theme_slug}", res)
end

.enabled_plugins(site) ⇒ Object

return all enabled plugins []



209
210
211
212
213
214
215
216
217
218
219
# File 'lib/plugin_routes.rb', line 209

def enabled_plugins(site)
  r = cache_variable("enable_plugins_site_#{site.id}")
  return r if r

  enabled_ps = site.plugins.active.pluck(:slug)
  res = all_plugins.each_with_object([]) do |plugin, ary|
    ary << plugin if enabled_ps.include?(plugin['key'])
  end
  res = res.sort_by { |e| e['position'] || 10 }
  cache_variable("enable_plugins_site_#{site.id}", res)
end

.fixActionParameter(h) ⇒ Object

convert action parameter into hash



42
43
44
45
46
47
48
49
50
# File 'lib/plugin_routes.rb', line 42

def fixActionParameter(h)
  return h unless h.is_a?(ActionController::Parameters)

  begin
    h.permit!.to_h
  rescue StandardError
    h.to_hash
  end
end

.get_anonymous_hooks(hook_key) ⇒ Array

return all registered anonymous hooks for hook_key

Parameters:

  • hook_key (String)

    name of the hook

Returns:

  • (Array)

    array of hooks for hook_key



65
66
67
# File 'lib/plugin_routes.rb', line 65

def get_anonymous_hooks(hook_key)
  (anonymous_hooks[hook_key.to_s] || []).map { |item| item[:callback] }
end

.get_gem(name) ⇒ Object

check if a gem is available or not Arguemnts: name: name of the gem return (Boolean) true/false



474
475
476
477
478
479
480
# File 'lib/plugin_routes.rb', line 474

def get_gem(name)
  Gem::Specification.find_by_name(name)
rescue Gem::LoadError
  false
rescue StandardError
  Gem.available?(name)
end

.get_gem_pluginsObject

return all plugins registered as gems



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/plugin_routes.rb', line 427

def get_gem_plugins
  Gem::Specification.each_with_object([]) do |gem, ary|
    path = gem.gem_dir
    config = File.join(path, 'config', 'camaleon_plugin.json')
    next unless File.exist?(config)

    p = JSON.parse(File.read(config))
    p = begin
      p.with_indifferent_access
    rescue StandardError
      p
    end
    p['key'] = gem.name if p['key'].nil? # TODO: REVIEW ERROR FOR conflict plugin keys
    p['version'] = gem.version.to_s
    p['path'] = path
    p['kind'] = 'plugin'
    p['descr'] = gem.description if p['descr'].blank?
    p['gem_mode'] = true
    ary << p
  end
end

.get_gem_themesObject

return all themes registered as gems



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/plugin_routes.rb', line 450

def get_gem_themes
  Gem::Specification.each_with_object([]) do |gem, ary|
    path = gem.gem_dir
    config = File.join(path, 'config', 'camaleon_theme.json')
    next unless File.exist?(config)

    p = JSON.parse(File.read(config))
    p = begin
      p.with_indifferent_access
    rescue StandardError
      p
    end
    p['key'] = gem.name if p['key'].nil? # TODO: REVIEW ERROR FOR conflict plugin keys
    p['path'] = path
    p['kind'] = 'theme'
    p['gem_mode'] = true
    ary << p
  end
end

.get_sitesObject

return all sites registered for Plugin routes



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/plugin_routes.rb', line 315

def get_sites
  # Eager-load metas and post_types so the per-site reads route drawing performs come from the
  # loaded associations instead of one query per site: metas back the option/language/theme
  # lookups (get_meta checks metas.loaded?), and post_types back the frontend post-type route
  # loop. On large multi-site installs those N+1s are the bulk of cold-boot route-draw time --
  # the window where the first request races a half-built table.
  #
  # The whole metas set is loaded on purpose, not a scoped subset: get_meta's loaded branch reads
  # a key absent from the loaded records as unset, so preloading only _default/languages_site
  # would make every other site meta read silently return its default. Site-level metas are few
  # rows per site, so this bounded over-fetch is the safe trade against that correctness hazard.
  @all_sites ||= CamaleonCms::Site.includes(:metas, :post_types).order(id: :asc).to_a
rescue StandardError
  []
end

.get_user_class_nameObject

return the class name for user model



78
79
80
# File 'lib/plugin_routes.rb', line 78

def get_user_class_name
  static_system_info['user_model'].presence || 'CamaleonCms::User'
end

.load(env = 'admin') ⇒ Object

load plugin routes if it is enabled



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/plugin_routes.rb', line 83

def load(env = 'admin')
  plugins = all_enabled_plugins
  res = ''
  case env
  when 'front'
    res << "namespace :plugins do \n"
    plugins.each do |plugin|
      res << "namespace '#{plugin['key']}' do \n"
      begin
        res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
      rescue StandardError
        ''
      end
      res << "end\n"
    end
    res << "end\n"

  when 'admin' # admin
    res << "scope 'admin', as: 'admin' do \n"
    res << "namespace :plugins do \n"
    plugins.each do |plugin|
      res << "namespace '#{plugin['key']}' do \n"
      begin
        res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
      rescue StandardError
        ''
      end
      res << "end\n"
    end
    res << "end\n"
    res << "end\n"
  else # main
    plugins.each do |plugin|
      res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
    rescue StandardError
      ''
    end
  end
  res + load_themes(env)
end

.load_themes(env = 'admin') ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/plugin_routes.rb', line 124

def load_themes(env = 'admin')
  plugins = all_enabled_themes
  res = ''
  case env
  when 'front'
    res << "namespace :themes do \n"
    plugins.each do |plugin|
      res << "namespace '#{plugin['key']}' do \n"
      begin
        res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
      rescue StandardError
        ''
      end
      res << "end\n"
    end
    res << "end\n"

  when 'admin' # admin
    res << "scope 'admin', as: 'admin' do \n"
    res << "namespace :themes do \n"
    plugins.each do |plugin|
      res << "namespace '#{plugin['key']}' do \n"
      begin
        res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
      rescue StandardError
        ''
      end
      res << "end\n"
    end
    res << "end\n"
    res << "end\n"
  else # main
    plugins.each do |plugin|
      res << "#{File.open(File.join(plugin['path'], 'config', "routes_#{env}.txt")).read}\n"
    rescue StandardError
      ''
    end
  end
  res
end

.migration_classObject



493
494
495
# File 'lib/plugin_routes.rb', line 493

def migration_class
  ActiveRecord::Migration[4.2]
end

.plugin_info(plugin_key) ⇒ Object

return plugin information



9
10
11
# File 'lib/plugin_routes.rb', line 9

def plugin_info(plugin_key)
  all_plugins.find { |p| p['key'] == plugin_key || p['path'].split('/').last == plugin_key }
end

.reloadObject

reload routes (thread-safe)



166
167
168
169
170
171
172
173
# File 'lib/plugin_routes.rb', line 166

def reload
  reload_monitor.synchronize do
    @all_sites = nil
    cache.clear
    Rails.application.reload_routes!
    after_reload_callbacks.uniq.each(&:call)
  end
end

.remove_anonymous_hook(hook_key, hook_id) ⇒ Array

return all registered anonymous hooks for hook_key

Parameters:

  • hook_key (String)

    name of the hook

  • hook_id (String)

    identifier of the anonymous hooks

Returns:

  • (Array)

    array of hooks for hook_key



73
74
75
# File 'lib/plugin_routes.rb', line 73

def remove_anonymous_hook(hook_key, hook_id)
  (anonymous_hooks[hook_key.to_s] || []).delete_if { |item| item[:id] == hook_id }
end

.site_plugin_helpers(site) ⇒ Object

all helpers of enabled plugins for site



277
278
279
280
281
282
283
# File 'lib/plugin_routes.rb', line 277

def site_plugin_helpers(site)
  r = cache_variable('site_plugin_helpers')
  return r if r

  res = enabled_apps(site).filter_map { |settings| settings['helpers'].presence }.flatten
  cache_variable('site_plugin_helpers', res)
end

.static_system_infoObject Also known as: system_info

return system static settings (config.json values)



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/plugin_routes.rb', line 20

def static_system_info
  r = cache_variable('statis_system_info')
  return r if r

  settings = {}

  gem_settings = File.join($camaleon_engine_dir, 'config', 'system.json')
  app_settings = Rails.root.join('config/system.json')

  settings.merge!(JSON.parse(File.read(gem_settings))) if File.exist?(gem_settings)
  settings.merge!(JSON.parse(File.read(app_settings))) if File.exist?(app_settings)

  # custom settings
  settings['key'] = 'system'
  settings['path'] = ''
  settings['kind'] = 'system'
  settings['hooks']['on_notification'] ||= []
  cache_variable('statis_system_info', settings)
end

.theme_info(theme_name) ⇒ Object

return theme information if theme_name is nil, the use current site theme



15
16
17
# File 'lib/plugin_routes.rb', line 15

def theme_info(theme_name)
  all_themes.find { |p| p['key'] == theme_name }
end