ckeditor5 1.23.2 → 1.23.3

Sign up to get free protection for your applications and to get access to all the features.
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