ckeditor5 1.23.2 → 1.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4e4581fc1d9ebbf70b9213ce10a444b5ad2633f45334a7e343f875a8f7180ee
4
- data.tar.gz: 251313b3f5eb8152f9385b9f4e159e30223b9a2bf0edff5009d3b85459d062c4
3
+ metadata.gz: 0d717a5397379dffdfd252d92c6b6f58a524430c0ae99798a46ff7f71556d3b9
4
+ data.tar.gz: 046715b5823ae3f38b3b2c1175fa4f1bdd5e34f9e260777dbcb714be287ec18b
5
5
  SHA512:
6
- metadata.gz: bdce490b8f96f7c365b3cdb782afccb02c8b372c93df042c76cfb10540d56f742b355af8c4d55a638399db4bccf661469117ae55c9cdd1a92b16e993bab37980
7
- data.tar.gz: 9d54670405e3030075495bc0cbceb60524803658e23535f84bf5a72570917cbbdc32ab7130b0c1d1554c5f3065b3d7067afef3df4ae93df260608d20de8b6725
6
+ metadata.gz: a552a32f61e75023f913b6df8c414b9af075942b97144e10582c4cfa1955bf988de8be6f2e652618ae3640364691f20c9ae3b679f3eb2017f78d3aa740580616
7
+ data.tar.gz: 934d9853eb527d2f8a65ecc87e6c4fac989a0bb1133f5a3b6154ebe8aca3239199a84a801c048e6909919b76295bae3e29f6af9e60f98f2cee46cfb8167647d7
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # CKEditor 5 Rails Integration ✨
2
2
 
3
3
  [![License: GPL v2](https://img.shields.io/badge/License-GPL_v2-blue.svg?style=flat-square)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
4
- ![Gem Version](https://img.shields.io/gem/v/ckeditor5?style=flat-square)
5
- ![Gem Total Downloads](https://img.shields.io/gem/dt/ckeditor5?style=flat-square&color=orange)
4
+ [![Gem Version](https://img.shields.io/gem/v/ckeditor5?style=flat-square)](https://rubygems.org/gems/ckeditor5)
5
+ [![Gem Total Downloads](https://img.shields.io/gem/dt/ckeditor5?style=flat-square&color=orange)](https://rubygems.org/gems/ckeditor5)
6
6
  [![Coverage](https://img.shields.io/codecov/c/github/mati365/ckeditor5-rails?style=flat-square)](https://codecov.io/gh/mati365/ckeditor5-rails)
7
7
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg?style=flat-square)](http://makeapullrequest.com)
8
8
  ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/mati365/ckeditor5-rails?style=flat-square)
@@ -3,38 +3,54 @@
3
3
  require 'singleton'
4
4
  require 'terser'
5
5
 
6
- module CKEditor5::Rails::Assets
7
- class WebComponentBundle
8
- include ActionView::Helpers::TagHelper
9
- include Singleton
10
-
11
- WEBCOMPONENTS_PATH = File.join(__dir__, 'webcomponents')
12
- WEBCOMPONENTS_MODULES = [
13
- 'utils.mjs',
14
- 'components/editable.mjs',
15
- 'components/ui-part.mjs',
16
- 'components/editor.mjs',
17
- 'components/context.mjs'
18
- ].freeze
19
-
20
- def source
21
- @source ||= compress_source(raw_source)
22
- end
23
-
24
- def to_html
25
- @to_html ||= tag.script(source, type: 'module', nonce: true)
26
- end
27
-
28
- private
29
-
30
- def raw_source
31
- @raw_source ||= WEBCOMPONENTS_MODULES.map do |file|
32
- File.read(File.join(WEBCOMPONENTS_PATH, file))
33
- end.join("\n")
34
- end
35
-
36
- def compress_source(code)
37
- Terser.new(compress: true, mangle: true).compile(code).html_safe
6
+ require_relative '../editor/props_inline_plugin'
7
+
8
+ module CKEditor5::Rails
9
+ module Assets
10
+ class WebComponentBundle
11
+ include ActionView::Helpers::TagHelper
12
+ include Singleton
13
+
14
+ WEBCOMPONENTS_PATH = File.join(__dir__, 'webcomponents')
15
+ WEBCOMPONENTS_MODULES = [
16
+ 'utils.mjs',
17
+ 'components/editable.mjs',
18
+ 'components/ui-part.mjs',
19
+ 'components/editor.mjs',
20
+ 'components/context.mjs'
21
+ ].freeze
22
+
23
+ def source
24
+ @source ||= compress_source(raw_source)
25
+ end
26
+
27
+ def to_html
28
+ @to_html ||= tag.script(source, type: 'module', nonce: true)
29
+ end
30
+
31
+ private
32
+
33
+ def raw_source
34
+ @raw_source ||= WEBCOMPONENTS_MODULES.map do |file|
35
+ content = File.read(File.join(WEBCOMPONENTS_PATH, file))
36
+
37
+ if file == 'utils.mjs'
38
+ inject_inline_code_signatures(content)
39
+ else
40
+ content
41
+ end
42
+ end.join("\n")
43
+ end
44
+
45
+ def inject_inline_code_signatures(content)
46
+ json_signatures = Editor::InlinePluginsSignaturesRegistry.instance.to_a.to_json
47
+
48
+ content.sub('__INLINE_CODE_SIGNATURES_PLACEHOLDER__', json_signatures)
49
+ end
50
+
51
+ def compress_source(code)
52
+ Terser.new(compress: true, mangle: true).compile(code).html_safe
53
+ end
38
54
  end
39
55
  end
40
56
  end
@@ -568,10 +568,14 @@ class CKEditorComponent extends HTMLElement {
568
568
  }
569
569
 
570
570
  /**
571
- * Gets bundle JSON description from translations attribute
571
+ * Gets bundle JSON description from translations attribute.
572
+ *
573
+ * **warning**: It's present only if the editor is loaded lazily.
572
574
  */
573
575
  #getBundle() {
574
- return this.#bundle ||= JSON.parse(this.getAttribute('bundle'));
576
+ return this.#bundle ||= JSON.parse(
577
+ this.getAttribute('bundle') || '{ "scripts": [], "stylesheets": [] }'
578
+ );
575
579
  }
576
580
 
577
581
 
@@ -20,6 +20,22 @@ function execIfDOMReady(callback) {
20
20
  }
21
21
  }
22
22
 
23
+ /**
24
+ * Verifies code signature using SHA-256 hash.
25
+ *
26
+ * @param {string} code - Source code to verify
27
+ * @returns {Promise<boolean>} True if code signature is valid
28
+ */
29
+ async function verifyCodeSignature(code) {
30
+ const signature = await crypto.subtle
31
+ .digest('SHA-256', new TextEncoder().encode(code))
32
+ .then(hash => Array.from(new Uint8Array(hash))
33
+ .map(b => b.toString(16).padStart(2, '0'))
34
+ .join(''));
35
+
36
+ return __INLINE_CODE_SIGNATURES_PLACEHOLDER__.includes(signature);
37
+ }
38
+
23
39
  /**
24
40
  * Dynamically imports modules based on configuration
25
41
  *
@@ -31,10 +47,14 @@ function execIfDOMReady(callback) {
31
47
  * @param {Object} imports[].window_name - Global window object name (for external type)
32
48
  * @param {('inline'|'external')} imports[].type - Type of import
33
49
  * @returns {Promise<Array<any>>} Array of loaded modules
34
- * @throws {Error} When plugin loading fails
50
+ * @throws {Error} When plugin loading fails or signature validation fails
35
51
  */
36
52
  function loadAsyncImports(imports = []) {
37
53
  const loadInlinePlugin = async ({ name, code }) => {
54
+ if (!await verifyCodeSignature(code)) {
55
+ throw new Error(`Invalid code signature for inline plugin "${name}"!`);
56
+ }
57
+
38
58
  const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
39
59
 
40
60
  if (!module.default) {
@@ -99,7 +99,8 @@ module CKEditor5::Rails
99
99
 
100
100
  if importmap_available?
101
101
  @__ckeditor_context = {
102
- bundle: combined_bundle
102
+ bundle: combined_bundle,
103
+ lazy: true
103
104
  }
104
105
 
105
106
  return Assets::WebComponentBundle.instance.to_html
@@ -40,8 +40,8 @@ module CKEditor5::Rails::Context
40
40
  # <% preset = ckeditor5_context_preset do
41
41
  # plugins :Comments, :TrackChanges, :Collaboration # Shared functionality plugins
42
42
  # end %>
43
- def ckeditor5_context_preset(&block)
44
- PresetBuilder.new(&block)
43
+ def ckeditor5_context_preset(**kwargs, &block)
44
+ PresetBuilder.new(disallow_inline_plugins: true, **kwargs, &block)
45
45
  end
46
46
  end
47
47
  end
@@ -38,7 +38,8 @@ module CKEditor5::Rails
38
38
  # version '43.3.1'
39
39
  # toolbar :bold, :italic
40
40
  # end
41
- def initialize(&block)
41
+ def initialize(disallow_inline_plugins: false, &block)
42
+ @disallow_inline_plugins = disallow_inline_plugins
42
43
  @config = {
43
44
  plugins: []
44
45
  }
@@ -50,7 +50,10 @@ module CKEditor5::Rails::Editor::Helpers
50
50
 
51
51
  raise ArgumentError, 'Configuration block is required for preset definition' unless block_given?
52
52
 
53
- CKEditor5::Rails::Presets::PresetBuilder.new(&block)
53
+ CKEditor5::Rails::Presets::PresetBuilder.new(
54
+ disallow_inline_plugins: true,
55
+ &block
56
+ )
54
57
  end
55
58
  end
56
59
  end
@@ -56,7 +56,7 @@ module CKEditor5::Rails
56
56
  # <%= form_for @post do |f| %>
57
57
  # <%= f.ckeditor5 :content, required: true %>
58
58
  # <% end %>
59
- def ckeditor5_editor( # rubocop:disable Metrics/ParameterLists
59
+ def ckeditor5_editor( # rubocop:disable Metrics/ParameterLists,Metrics/CyclomaticComplexity
60
60
  preset: nil,
61
61
  config: nil, extra_config: {}, type: nil,
62
62
  initial_data: nil, watchdog: true,
@@ -78,7 +78,9 @@ module CKEditor5::Rails
78
78
 
79
79
  editor_props = Editor::Props.new(
80
80
  type, config,
81
- bundle: context[:bundle],
81
+ # Use fallback only if there is no `ckeditor5_assets` in the head.
82
+ # So web-component will be loaded from the CDN.
83
+ bundle: context[:lazy] ? context[:bundle] : nil,
82
84
  watchdog: watchdog,
83
85
  editable_height: editable_height || preset.editable_height
84
86
  )
@@ -159,13 +161,15 @@ module CKEditor5::Rails
159
161
 
160
162
  return {
161
163
  bundle: create_preset_bundle(found_preset),
162
- preset: found_preset
164
+ preset: found_preset,
165
+ lazy: true
163
166
  }
164
167
  end
165
168
 
166
169
  {
167
170
  bundle: nil,
168
- preset: Engine.default_preset
171
+ preset: Engine.default_preset,
172
+ lazy: true
169
173
  }
170
174
  end
171
175
  end
@@ -45,7 +45,7 @@ module CKEditor5::Rails::Editor
45
45
 
46
46
  def serialized_attributes
47
47
  {
48
- bundle: bundle.to_json,
48
+ bundle: bundle&.to_json,
49
49
  plugins: serialize_plugins,
50
50
  config: serialize_config,
51
51
  watchdog: watchdog
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+ require 'digest'
5
+
3
6
  require_relative 'props_base_plugin'
4
7
 
5
8
  module CKEditor5::Rails::Editor
@@ -11,6 +14,8 @@ module CKEditor5::Rails::Editor
11
14
 
12
15
  @code = code
13
16
  validate_code!
17
+
18
+ InlinePluginsSignaturesRegistry.instance.register(code)
14
19
  end
15
20
 
16
21
  def to_h
@@ -32,4 +37,28 @@ module CKEditor5::Rails::Editor
32
37
  'Code must include `export default` that exports plugin definition!'
33
38
  end
34
39
  end
40
+
41
+ class InlinePluginsSignaturesRegistry
42
+ include Singleton
43
+
44
+ def initialize
45
+ @signatures = Set.new
46
+ end
47
+
48
+ def register(code)
49
+ signature = generate_signature(code)
50
+ @signatures.add(signature)
51
+ signature
52
+ end
53
+
54
+ def to_a
55
+ @signatures.to_a
56
+ end
57
+
58
+ private
59
+
60
+ def generate_signature(code)
61
+ Digest::SHA256.hexdigest(code)
62
+ end
63
+ end
35
64
  end
@@ -1,21 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support'
4
+
3
5
  module CKEditor5::Rails
4
6
  module Presets
5
7
  module Concerns
6
8
  module PluginMethods
7
- private
9
+ extend ActiveSupport::Concern
8
10
 
9
- # Register a plugin in the editor configuration
10
- #
11
- # @param plugin_obj [Editor::PropsBasePlugin] Plugin instance to register
12
- # @return [Editor::PropsBasePlugin] The registered plugin
13
- def register_plugin(plugin_obj)
14
- config[:plugins] << plugin_obj
15
- plugin_obj
16
- end
11
+ class DisallowedInlinePlugin < ArgumentError; end
17
12
 
18
- public
13
+ included do
14
+ attr_reader :disallow_inline_plugins
15
+ end
19
16
 
20
17
  # Registers an external plugin loaded from a URL
21
18
  #
@@ -89,6 +86,29 @@ module CKEditor5::Rails
89
86
  builder.instance_eval(&block) if block_given?
90
87
  builder
91
88
  end
89
+
90
+ private
91
+
92
+ def looks_like_inline_plugin?(plugin)
93
+ plugin.to_h[:type] == :inline
94
+ end
95
+
96
+ # Register a plugin in the editor configuration.
97
+ #
98
+ # It will raise an error if inline plugins are not allowed and the plugin is an inline plugin.
99
+ # Most likely, this is being thrown when you use inline_plugin definition in a place where
100
+ # it's not allowed (e.g. in a preset definition placed in controller).
101
+ #
102
+ # @param plugin_obj [Editor::PropsBasePlugin] Plugin instance to register
103
+ # @return [Editor::PropsBasePlugin] The registered plugin
104
+ def register_plugin(plugin_obj)
105
+ if disallow_inline_plugins && looks_like_inline_plugin?(plugin_obj)
106
+ raise DisallowedInlinePlugin, 'Inline plugins are not allowed here.'
107
+ end
108
+
109
+ config[:plugins] << plugin_obj
110
+ plugin_obj
111
+ end
92
112
  end
93
113
  end
94
114
  end
@@ -16,7 +16,8 @@ module CKEditor5::Rails
16
16
  # gpl
17
17
  # type :classic
18
18
  # end
19
- def initialize(&block)
19
+ def initialize(disallow_inline_plugins: false, &block)
20
+ @disallow_inline_plugins = disallow_inline_plugins
20
21
  @version = nil
21
22
  @premium = false
22
23
  @cdn = :jsdelivr
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CKEditor5
4
4
  module Rails
5
- VERSION = '1.23.2'
5
+ VERSION = '1.23.3'
6
6
 
7
7
  DEFAULT_CKEDITOR_VERSION = '44.1.0'
8
8
  end
@@ -107,5 +107,18 @@ RSpec.describe CKEditor5::Rails::Context::Helpers do
107
107
  preset: :custom
108
108
  )
109
109
  end
110
+
111
+ it 'raises error when trying to define inline plugin' do
112
+ expect do
113
+ helper.ckeditor5_context_preset do
114
+ inline_plugin :TestPlugin, <<~JS
115
+ export default class TestPlugin { }
116
+ JS
117
+ end
118
+ end.to raise_error(
119
+ CKEditor5::Rails::Presets::Concerns::PluginMethods::DisallowedInlinePlugin,
120
+ 'Inline plugins are not allowed here.'
121
+ )
122
+ end
110
123
  end
111
124
  end
@@ -38,6 +38,22 @@ RSpec.describe CKEditor5::Rails::Editor::Helpers::Config do
38
38
  it 'yields the block to PresetBuilder' do
39
39
  expect { |b| helper.ckeditor5_preset(&b) }.to yield_control
40
40
  end
41
+
42
+ it 'does not allow inline plugins definition' do
43
+ expect do
44
+ helper.ckeditor5_preset do
45
+ inline_plugin :CustomPlugin, <<~JS
46
+ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
47
+ export default class CustomPlugin extends Plugin {
48
+ static get pluginName() { return 'CustomPlugin'; }
49
+ }
50
+ JS
51
+ end
52
+ end.to raise_error(
53
+ CKEditor5::Rails::Presets::Concerns::PluginMethods::DisallowedInlinePlugin,
54
+ 'Inline plugins are not allowed here.'
55
+ )
56
+ end
41
57
  end
42
58
 
43
59
  context 'when neither name nor block is provided' do
@@ -58,6 +58,7 @@ RSpec.describe CKEditor5::Rails::Editor::Helpers::Editor do
58
58
  result = helper.ckeditor5_context_or_fallback(:custom)
59
59
  expect(result).to match({
60
60
  bundle: 'custom-bundle',
61
+ lazy: true,
61
62
  preset: custom_preset
62
63
  })
63
64
  end
@@ -71,9 +72,33 @@ RSpec.describe CKEditor5::Rails::Editor::Helpers::Editor do
71
72
  result = helper.ckeditor5_context_or_fallback(nil)
72
73
  expect(result).to match({
73
74
  bundle: nil,
75
+ lazy: true,
74
76
  preset: :default
75
77
  })
76
78
  end
79
+
80
+ it 'includes lazy flag in fallback context' do
81
+ allow(CKEditor5::Rails::Engine).to receive(:default_preset)
82
+ .and_return(:default)
83
+
84
+ result = helper.ckeditor5_context_or_fallback(nil)
85
+ expect(result[:lazy]).to be true
86
+ end
87
+
88
+ it 'includes lazy flag in preset context' do
89
+ custom_preset = instance_double(CKEditor5::Rails::Presets::PresetBuilder)
90
+
91
+ allow(CKEditor5::Rails::Engine).to receive(:find_preset)
92
+ .with(:custom)
93
+ .and_return(custom_preset)
94
+
95
+ allow(helper).to receive(:create_preset_bundle)
96
+ .with(custom_preset)
97
+ .and_return('custom-bundle')
98
+
99
+ result = helper.ckeditor5_context_or_fallback(:custom)
100
+ expect(result[:lazy]).to be true
101
+ end
77
102
  end
78
103
 
79
104
  describe '#ckeditor5_editor' do
@@ -225,6 +250,31 @@ RSpec.describe CKEditor5::Rails::Editor::Helpers::Editor do
225
250
  expect(helper.ckeditor5_editor(editable_height: 600)).to include('editable-height="600px"')
226
251
  end
227
252
  end
253
+
254
+ context 'when using bundles' do
255
+ let(:bundle) { 'test-bundle' }
256
+ let(:context) { { preset: :default, bundle: bundle, lazy: true } }
257
+
258
+ it 'uses bundle from context when lazy is true' do
259
+ expect(CKEditor5::Rails::Editor::Props).to receive(:new)
260
+ .with(anything, anything, hash_including(bundle: bundle))
261
+ .and_call_original
262
+
263
+ helper.ckeditor5_editor
264
+ end
265
+
266
+ context 'when lazy is false' do
267
+ let(:context) { { preset: :default, bundle: bundle, lazy: false } }
268
+
269
+ it 'does not use bundle from context' do
270
+ expect(CKEditor5::Rails::Editor::Props).to receive(:new)
271
+ .with(anything, anything, hash_including(bundle: nil))
272
+ .and_call_original
273
+
274
+ helper.ckeditor5_editor
275
+ end
276
+ end
277
+ end
228
278
  end
229
279
 
230
280
  describe '#ckeditor5_editable' do
@@ -18,24 +18,10 @@ RSpec.describe CKEditor5::Rails::Hooks::Form do
18
18
  subject(:rendered_editor) { builder.build_editor(:content) }
19
19
 
20
20
  it 'renders ckeditor element' do
21
- bundle_json = {
22
- scripts: [
23
- {
24
- import_name: 'ckeditor5',
25
- url: 'https://cdn.jsdelivr.net/npm/ckeditor5@34.1.0/dist/browser/ckeditor5.js',
26
- translation: false
27
- }
28
- ],
29
- stylesheets: [
30
- 'https://cdn.jsdelivr.net/npm/ckeditor5@34.1.0/dist/browser/ckeditor5.css'
31
- ]
32
- }.to_json
33
-
34
21
  attrs = {
35
22
  name: 'post[content]',
36
23
  id: 'post_content',
37
24
  type: 'ClassicEditor',
38
- bundle: bundle_json,
39
25
  watchdog: 'true'
40
26
  }
41
27
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ckeditor5
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.23.2
4
+ version: 1.23.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Bagiński
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-12-17 00:00:00.000000000 Z
12
+ date: 2024-12-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails