Patch #43643 » 0003-Add-stimulus-clipboard_controller-and-copypre_scrubb.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 1441 | 1441 |
$(document).on('focus', '[data-auto-complete=true]', function(event) {
|
| 1442 | 1442 |
inlineAutoComplete(event.target); |
| 1443 | 1443 |
}); |
| 1444 |
document.addEventListener("DOMContentLoaded", () => { setupCopyButtonsToPreElements(); });
|
|
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1928 | 1928 |
end |
| 1929 | 1929 | |
| 1930 | 1930 |
def copy_object_url_link(url) |
| 1931 |
link_to_function( |
|
| 1932 |
sprite_icon('copy-link', l(:button_copy_link)), 'copyDataClipboardTextToClipboard(this);',
|
|
| 1933 |
class: 'icon icon-copy-link', |
|
| 1934 |
data: {'clipboard-text' => url}
|
|
| 1935 |
) |
|
| 1931 |
link_to sprite_icon('copy-link', l(:button_copy_link)),
|
|
| 1932 |
'#', |
|
| 1933 |
class: 'icon icon-copy-link', |
|
| 1934 |
data: {clipboard_text: url, controller: 'clipboard', action: 'clipboard#copyText'}
|
|
| 1936 | 1935 |
end |
| 1937 | 1936 | |
| 1938 | 1937 |
private |
| app/javascript/controllers/clipboard_controller.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 |
import { Controller } from "@hotwired/stimulus"
|
|
| 7 | ||
| 8 |
// Connects to data-controller="clipboard" |
|
| 9 |
export default class extends Controller {
|
|
| 10 |
static targets = ['pre']; |
|
| 11 | ||
| 12 |
copyPre(e) {
|
|
| 13 |
e.preventDefault(); |
|
| 14 |
const element = e.currentTarget; |
|
| 15 |
let textToCopy = (this.preTarget.querySelector("code") || this.preTarget).textContent.replace(/\n$/, '');
|
|
| 16 |
if (this.preTarget.querySelector("code.syntaxhl")) { textToCopy = textToCopy.replace(/ $/, ''); } // Workaround for half-width space issue in Textile's highlighted code
|
|
| 17 | ||
| 18 |
this.copy(textToCopy).then(() => {
|
|
| 19 |
updateSVGIcon(element, "checked"); |
|
| 20 |
setTimeout(() => updateSVGIcon(element, "copy-pre-content"), 2000); |
|
| 21 |
}); |
|
| 22 |
} |
|
| 23 | ||
| 24 |
copyText(e) {
|
|
| 25 |
e.preventDefault(); |
|
| 26 |
this.copy(e.currentTarget.dataset.clipboardText); |
|
| 27 | ||
| 28 |
const element = e.currentTarget.closest('.drdn.expanded');
|
|
| 29 |
if (element !== null) {
|
|
| 30 |
element.classList.remove('expanded');
|
|
| 31 |
} |
|
| 32 |
} |
|
| 33 | ||
| 34 |
copy(text) {
|
|
| 35 |
if (navigator.clipboard) {
|
|
| 36 |
return navigator.clipboard.writeText(text).catch(() => {
|
|
| 37 |
return this.fallback(text); |
|
| 38 |
}); |
|
| 39 |
} else {
|
|
| 40 |
return this.fallback(text); |
|
| 41 |
} |
|
| 42 |
} |
|
| 43 | ||
| 44 |
fallback(text) {
|
|
| 45 |
const temp = document.createElement('textarea');
|
|
| 46 |
temp.value = text; |
|
| 47 |
temp.style.position = 'fixed'; |
|
| 48 |
temp.style.left = '-9999px'; |
|
| 49 |
document.body.appendChild(temp); |
|
| 50 |
temp.select(); |
|
| 51 |
document.execCommand('copy');
|
|
| 52 |
document.body.removeChild(temp); |
|
| 53 |
return Promise.resolve(); |
|
| 54 |
} |
|
| 55 |
} |
|
| app/views/journals/update.js.erb | ||
|---|---|---|
| 14 | 14 |
} else {
|
| 15 | 15 |
journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
|
| 16 | 16 |
} |
| 17 |
setupCopyButtonsToPreElements(); |
|
| 18 | 17 |
setupHoverTooltips(); |
| 19 | 18 |
<% end %> |
| 20 | 19 | |
| lib/redmine/wiki_formatting/common_mark/formatter.rb | ||
|---|---|---|
| 56 | 56 |
SCRUBBERS = [ |
| 57 | 57 |
SyntaxHighlightScrubber.new, |
| 58 | 58 |
Redmine::WikiFormatting::TablesortScrubber.new, |
| 59 |
Redmine::WikiFormatting::CopypreScrubber.new, |
|
| 59 | 60 |
FixupAutoLinksScrubber.new, |
| 60 | 61 |
ExternalLinksScrubber.new, |
| 61 | 62 |
AlertsIconsScrubber.new |
| lib/redmine/wiki_formatting/copypre_scrubber.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# This code is released under the GNU General Public License. |
|
| 6 | ||
| 7 |
module Redmine |
|
| 8 |
module WikiFormatting |
|
| 9 |
class CopypreScrubber < Loofah::Scrubber |
|
| 10 |
def scrub(node) |
|
| 11 |
return unless node.name == 'pre' |
|
| 12 | ||
| 13 |
node['data-clipboard-target'] = 'pre' |
|
| 14 |
# Wrap the <pre> element with a container and add a copy button |
|
| 15 |
node.wrap(wrapper) |
|
| 16 | ||
| 17 |
# Copy the contents of the pre tag when copyButton is clicked |
|
| 18 |
node.parent.prepend_child(button) |
|
| 19 |
end |
|
| 20 | ||
| 21 |
def wrapper |
|
| 22 |
@wrapper ||= Nokogiri::HTML5.fragment('<div class="pre-wrapper" data-controller="clipboard"></div>').children.first
|
|
| 23 |
end |
|
| 24 | ||
| 25 |
def button |
|
| 26 |
icon = ApplicationController.helpers.sprite_icon('copy-pre-content', size: 18)
|
|
| 27 |
button_copy = ApplicationController.helpers.l(:button_copy) |
|
| 28 |
html = '<a class="copy-pre-content-link icon-only" title="' + button_copy + '" data-action="clipboard#copyPre">' + icon + '</a>' |
|
| 29 |
@button ||= Nokogiri::HTML5.fragment(html).children.first |
|
| 30 |
end |
|
| 31 |
end |
|
| 32 |
end |
|
| 33 |
end |
|
| lib/redmine/wiki_formatting/textile/formatter.rb | ||
|---|---|---|
| 22 | 22 |
module Textile |
| 23 | 23 |
SCRUBBERS = [ |
| 24 | 24 |
SyntaxHighlightScrubber.new, |
| 25 |
Redmine::WikiFormatting::TablesortScrubber.new |
|
| 25 |
Redmine::WikiFormatting::TablesortScrubber.new, |
|
| 26 |
Redmine::WikiFormatting::CopypreScrubber.new |
|
| 26 | 27 |
] |
| 27 | 28 | |
| 28 | 29 |
class Formatter |
| test/helpers/application_helper_test.rb | ||
|---|---|---|
| 1320 | 1320 |
"<div class=\"bold\">content</div>" => "<p><div class=\"bold\">content</div></p>", |
| 1321 | 1321 |
"<script>some script;</script>" => "<p><script>some script;</script></p>", |
| 1322 | 1322 |
# do not escape pre/code tags |
| 1323 |
"<pre>\nline 1\nline2</pre>" => "<pre>line 1\nline2</pre>",
|
|
| 1324 |
"<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
|
|
| 1325 |
"<pre><div class=\"foo\">content</div></pre>" => "<pre><div class=\"foo\">content</div></pre>",
|
|
| 1326 |
"<pre><div class=\"<foo\">content</div></pre>" => "<pre><div class=\"<foo\">content</div></pre>",
|
|
| 1323 |
"<pre>\nline 1\nline2</pre>" => pre_wrapper("<pre data-clipboard-target=\"pre\">line 1\nline2</pre>"),
|
|
| 1324 |
"<pre><code>\nline 1\nline2</code></pre>" => pre_wrapper("<pre data-clipboard-target=\"pre\"><code>\nline 1\nline2</code></pre>"),
|
|
| 1325 |
"<pre><div class=\"foo\">content</div></pre>" => pre_wrapper("<pre data-clipboard-target=\"pre\"><div class=\"foo\">content</div></pre>"),
|
|
| 1326 |
"<pre><div class=\"<foo\">content</div></pre>" => pre_wrapper("<pre data-clipboard-target=\"pre\"><div class=\"<foo\">content</div></pre>"),
|
|
| 1327 | 1327 |
"<!-- opening comment" => "<p><!-- opening comment</p>", |
| 1328 | 1328 |
# remove attributes including class |
| 1329 |
"<pre class='foo'>some text</pre>" => "<pre>some text</pre>",
|
|
| 1330 |
'<pre class="foo">some text</pre>' => '<pre>some text</pre>',
|
|
| 1331 |
"<pre class='foo bar'>some text</pre>" => "<pre>some text</pre>",
|
|
| 1332 |
'<pre class="foo bar">some text</pre>' => '<pre>some text</pre>',
|
|
| 1333 |
"<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
|
|
| 1329 |
"<pre class='foo'>some text</pre>" => pre_wrapper('<pre data-clipboard-target="pre">some text</pre>'),
|
|
| 1330 |
'<pre class="foo">some text</pre>' => pre_wrapper('<pre data-clipboard-target="pre">some text</pre>'),
|
|
| 1331 |
"<pre class='foo bar'>some text</pre>" => pre_wrapper('<pre data-clipboard-target="pre">some text</pre>'),
|
|
| 1332 |
'<pre class="foo bar">some text</pre>' => pre_wrapper('<pre data-clipboard-target="pre">some text</pre>'),
|
|
| 1333 |
"<pre onmouseover='alert(1)'>some text</pre>" => pre_wrapper('<pre data-clipboard-target="pre">some text</pre>'),
|
|
| 1334 | 1334 |
# xss |
| 1335 |
'<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
|
|
| 1336 |
'<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
|
|
| 1335 |
'<pre><code class=""onmouseover="alert(1)">text</code></pre>' => pre_wrapper('<pre data-clipboard-target="pre"><code>text</code></pre>'),
|
|
| 1336 |
'<pre class=""onmouseover="alert(1)">text</pre>' => pre_wrapper('<pre data-clipboard-target="pre">text</pre>'),
|
|
| 1337 | 1337 |
} |
| 1338 | 1338 |
with_settings :text_formatting => 'textile' do |
| 1339 | 1339 |
to_test.each {|text, result| assert_equal result, textilizable(text)}
|
| ... | ... | |
| 1342 | 1342 | |
| 1343 | 1343 |
def test_allowed_html_tags |
| 1344 | 1344 |
to_test = {
|
| 1345 |
"<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
|
|
| 1345 |
"<pre>preformatted text</pre>" => pre_wrapper('<pre data-clipboard-target="pre">preformatted text</pre>'),
|
|
| 1346 | 1346 |
"<notextile>no *textile* formatting</notextile>" => "no *textile* formatting", |
| 1347 | 1347 |
"<notextile>this is <tag>a tag</tag></notextile>" => "this is <tag>a tag</tag>" |
| 1348 | 1348 |
} |
| ... | ... | |
| 1363 | 1363 |
RAW |
| 1364 | 1364 |
expected = <<~EXPECTED |
| 1365 | 1365 |
<p>Before</p> |
| 1366 |
<pre> |
|
| 1367 |
<prepared-statement-cache-size>32</prepared-statement-cache-size> |
|
| 1368 |
</pre> |
|
| 1366 |
#{pre_wrapper('<pre data-clipboard-target="pre"><prepared-statement-cache-size>32</prepared-statement-cache-size></pre>')}
|
|
| 1369 | 1367 |
<p>After</p> |
| 1370 | 1368 |
EXPECTED |
| 1371 | 1369 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 1392 | 1390 |
"/issues/1", |
| 1393 | 1391 |
:class => Issue.find(1).css_classes, |
| 1394 | 1392 |
:title => "Bug: Cannot print recipes (New)") |
| 1395 |
expected = <<~EXPECTED |
|
| 1396 |
<p>#{result1}</p>
|
|
| 1397 |
<p>#{result2}</p>
|
|
| 1398 |
<pre> |
|
| 1393 |
pre = <<~PRE |
|
| 1394 |
<pre data-clipboard-target="pre"> |
|
| 1399 | 1395 |
[[CookBook documentation]] |
| 1400 | 1396 | |
| 1401 | 1397 |
#1 |
| 1402 | 1398 |
</pre> |
| 1399 |
PRE |
|
| 1400 |
expected = <<~EXPECTED |
|
| 1401 |
<p>#{result1}</p>
|
|
| 1402 |
<p>#{result2}</p>
|
|
| 1403 |
#{pre_wrapper(pre)}
|
|
| 1403 | 1404 |
EXPECTED |
| 1404 | 1405 |
@project = Project.find(1) |
| 1405 | 1406 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 1411 | 1412 |
raw = <<~RAW |
| 1412 | 1413 |
<pre><code> |
| 1413 | 1414 |
RAW |
| 1415 |
pre = <<~PRE |
|
| 1416 |
<pre data-clipboard-target="pre"> |
|
| 1417 |
<code></code> |
|
| 1418 |
</pre> |
|
| 1419 |
PRE |
|
| 1414 | 1420 |
expected = <<~EXPECTED |
| 1415 |
<pre><code> |
|
| 1416 |
</code></pre> |
|
| 1421 |
#{pre_wrapper(pre)}
|
|
| 1417 | 1422 |
EXPECTED |
| 1418 | 1423 |
@project = Project.find(1) |
| 1419 | 1424 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 1435 | 1440 |
</code></pre> |
| 1436 | 1441 |
RAW |
| 1437 | 1442 |
expected = <<~EXPECTED |
| 1438 |
<pre><code class="ECMA_script syntaxhl" data-language="ECMA_script"><span class="cm">/* Hello */</span><span class="nb">document</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello World!</span><span class="dl">"</span><span class="p">);</span></code></pre>
|
|
| 1443 |
#{pre_wrapper('<pre data-clipboard-target="pre"><code class="ECMA_script syntaxhl" data-language="ECMA_script"><span class="cm">/* Hello */</span><span class="nb">document</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello World!</span><span class="dl">"</span><span class="p">);</span></code></pre>')}
|
|
| 1439 | 1444 |
EXPECTED |
| 1440 | 1445 |
with_settings :text_formatting => 'textile' do |
| 1441 | 1446 |
assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
|
| ... | ... | |
| 1449 | 1454 |
</code></pre> |
| 1450 | 1455 |
RAW |
| 1451 | 1456 |
expected = <<~EXPECTED |
| 1452 |
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="n">x</span> <span class="o">=</span> <span class="n">a</span> <span class="o">&</span> <span class="n">b</span></code></pre>
|
|
| 1457 |
#{pre_wrapper('<pre data-clipboard-target="pre"><code class="ruby syntaxhl" data-language="ruby"><span class="n">x</span> <span class="o">=</span> <span class="n">a</span> <span class="o">&</span> <span class="n">b</span></code></pre>')}
|
|
| 1453 | 1458 |
EXPECTED |
| 1454 | 1459 |
with_settings :text_formatting => 'textile' do |
| 1455 | 1460 |
assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
|
| ... | ... | |
| 2424 | 2429 |
assert_equal({}, list_autofill_data_attributes)
|
| 2425 | 2430 |
end |
| 2426 | 2431 |
end |
| 2432 | ||
| 2433 |
def pre_wrapper(text) |
|
| 2434 |
'<div class="pre-wrapper" data-controller="clipboard"><a class="copy-pre-content-link icon-only" data-action="clipboard#copyPre">' + |
|
| 2435 |
'<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-34cfafab.svg#icon--copy-pre-content"></use></svg></a>' + |
|
| 2436 |
text + |
|
| 2437 |
'</div>' |
|
| 2438 |
end |
|
| 2427 | 2439 |
end |
| test/unit/lib/redmine/wiki_formatting/macros_test.rb | ||
|---|---|---|
| 438 | 438 | |
| 439 | 439 |
{{hello_world(bar)}}
|
| 440 | 440 |
RAW |
| 441 |
expected = <<~EXPECTED |
|
| 442 |
<p>Hello world! Object: NilClass, Arguments: foo and no block of text.</p> |
|
| 443 | ||
| 444 |
<pre> |
|
| 441 |
pre = <<~PRE |
|
| 442 |
<pre data-clipboard-target="pre"> |
|
| 445 | 443 |
{{hello_world(pre)}}
|
| 446 | 444 |
!{{hello_world(pre)}}
|
| 447 | 445 |
</pre> |
| 446 |
PRE |
|
| 447 |
expected = <<~EXPECTED |
|
| 448 |
<p>Hello world! Object: NilClass, Arguments: foo and no block of text.</p> |
|
| 449 | ||
| 450 |
#{pre_wrapper(pre)}
|
|
| 448 | 451 | |
| 449 | 452 |
<p>Hello world! Object: NilClass, Arguments: bar and no block of text.</p> |
| 450 | 453 |
EXPECTED |
| ... | ... | |
| 456 | 459 |
def test_macros_should_be_escaped_in_pre_tags |
| 457 | 460 |
with_settings :text_formatting => 'textile' do |
| 458 | 461 |
text = '<pre>{{hello_world(<tag>)}}</pre>'
|
| 459 |
assert_equal '<pre>{{hello_world(<tag>)}}</pre>', textilizable(text)
|
|
| 462 |
assert_equal pre_wrapper('<pre data-clipboard-target="pre">{{hello_world(<tag>)}}</pre>'), textilizable(text)
|
|
| 460 | 463 |
end |
| 461 | 464 |
end |
| 462 | 465 | |
| ... | ... | |
| 633 | 636 |
end |
| 634 | 637 |
end |
| 635 | 638 |
end |
| 639 | ||
| 640 |
def pre_wrapper(text) |
|
| 641 |
'<div class="pre-wrapper" data-controller="clipboard"><a class="copy-pre-content-link icon-only" data-action="clipboard#copyPre">' + |
|
| 642 |
'<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-34cfafab.svg#icon--copy-pre-content"></use></svg></a>' + |
|
| 643 |
text + |
|
| 644 |
'</div>' |
|
| 645 |
end |
|
| 636 | 646 |
end |
| test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb | ||
|---|---|---|
| 613 | 613 |
"class=\"ruby \"foo\" bar\"" => "data-language=\"ruby \"", |
| 614 | 614 |
}.each do |classattr, codeattr| |
| 615 | 615 |
assert_html_output({"<code #{classattr}>test</code>" => "<code #{codeattr}>test</code>"}, false)
|
| 616 |
assert_html_output({"<pre #{classattr}>test</pre>" => "<pre>test</pre>"}, false)
|
|
| 616 |
assert_html_output({"<pre #{classattr}>test</pre>" => pre_wrapper('<pre data-clipboard-target="pre">test</pre>')}, false)
|
|
| 617 | 617 |
assert_html_output({"<kbd #{classattr}>test</kbd>" => "<kbd>test</kbd>"}, false)
|
| 618 | 618 |
end |
| 619 | 619 | |
| ... | ... | |
| 652 | 652 |
end |
| 653 | 653 | |
| 654 | 654 |
def test_should_not_allow_valid_language_class_attribute_on_non_code_offtags |
| 655 |
%w(pre kbd).each do |tag| |
|
| 656 |
assert_html_output({"<#{tag} class=\"ruby\">test</#{tag}>" => "<#{tag}>test</#{tag}>"}, false)
|
|
| 657 |
end |
|
| 655 |
assert_html_output({"<pre class=\"ruby\">test</pre>" => pre_wrapper('<pre data-clipboard-target="pre">test</pre>')}, false)
|
|
| 656 |
assert_html_output({"<kbd class=\"ruby\">test</kbd>" => "<kbd>test</kbd>"}, false)
|
|
| 658 | 657 | |
| 659 | 658 |
assert_html_output({"<notextile class=\"ruby\">test</notextile>" => "test"}, false)
|
| 660 | 659 |
end |
| ... | ... | |
| 755 | 754 |
</p> |
| 756 | 755 |
</pre> |
| 757 | 756 |
STR |
| 758 |
expected = <<~EXPECTED |
|
| 759 |
<p>Hello world.</p> |
|
| 760 | ||
| 761 |
<p>Foo</p> |
|
| 762 | ||
| 763 |
<pre> |
|
| 757 |
pre = <<~PRE |
|
| 758 |
<pre data-clipboard-target="pre"> |
|
| 764 | 759 |
This is a code block. |
| 765 | 760 |
<p> |
| 766 | 761 |
<!-- comments in a code block should be preserved --> |
| 767 | 762 |
</p> |
| 768 | 763 |
</pre> |
| 764 |
PRE |
|
| 765 |
expected = <<~EXPECTED |
|
| 766 |
<p>Hello world.</p> |
|
| 767 | ||
| 768 |
<p>Foo</p> |
|
| 769 | ||
| 770 |
#{pre_wrapper(pre)}
|
|
| 769 | 771 | |
| 770 | 772 |
EXPECTED |
| 771 | 773 |
assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '')
|
| ... | ... | |
| 820 | 822 |
assert_equal expected, result.first, "section content did not match" |
| 821 | 823 |
assert_equal ActiveSupport::Digest.hexdigest(expected), result.last, "section hash did not match" |
| 822 | 824 |
end |
| 825 | ||
| 826 |
def pre_wrapper(text) |
|
| 827 |
'<div class="pre-wrapper" data-controller="clipboard"><a class="copy-pre-content-link icon-only" data-action="clipboard#copyPre">' + |
|
| 828 |
'<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-34cfafab.svg#icon--copy-pre-content"></use></svg></a>' + |
|
| 829 |
text + |
|
| 830 |
'</div>' |
|
| 831 |
end |
|
| 823 | 832 |
end |
- « Previous
- 1
- 2
- 3
- Next »