Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor "copy to clipboard" button and use it for citations #4289

Merged
merged 9 commits into from
Mar 11, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,24 @@ class CopyToClipboardButton extends \Laminas\View\Helper\AbstractHelper
/**
* This helper creates button for copying content of an element into clipboard
*
* @param string $elementSelector jQuery selector for element to copy
* @param string $elementSelector css selector for element to copy
* @param bool $hideButtonText controls whether the description of the button's purpose
* is displayed as text or only with a title attribute
*
* @return string HTML string
*/
public function __invoke(string $elementSelector)
public function __invoke(string $elementSelector, bool $hideButtonText = true)
{
static $buttonNumber = 0;
$buttonNumber++;
$view = $this->getView();
return $view->render(
'Helpers/copy-to-clipboard-button.phtml',
['selector' => $elementSelector, 'buttonNumber' => $buttonNumber]
[
'selector' => $elementSelector,
'buttonNumber' => $buttonNumber,
'hideButtonText' => $hideButtonText,
]
);
}
}
2 changes: 1 addition & 1 deletion themes/bootstrap5/css/compiled.css

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions themes/bootstrap5/js/copy_to_clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*global VuFind, bootstrap */
VuFind.register('copyToClipboard', function copyToClipboard() {

/**
* Show popover for 2 seconds
* @param {object} popover Popover
*/
function _showPopover(popover) {
if (popover !== undefined) {
popover.show();
setTimeout(() => {
popover.hide();
}, 2000);
}
}

/**
* Initialise copy button
* @param {Element} button Copy button
*/
function _initButton(button) {
if (button.dataset.initialized !== 'true') {
button.dataset.initialized = 'true';
if (!button.dataset.target) return;
const targetElement = document.querySelector(button.dataset.target);
if (!targetElement) return;
let successPopover;
const successMessageElement = document.querySelector('#copySuccessMessage' + button.dataset.number);
if (successMessageElement) {
successPopover = new bootstrap.Popover(button.parentNode, {trigger: 'manual', 'html': true, 'content': successMessageElement.innerHTML});
}
let errorPopover;
const errorMessageElement = document.querySelector('#copyFailureMessage' + button.dataset.number);
if (errorMessageElement) {
errorPopover = new bootstrap.Popover(button.parentNode, {trigger: 'manual', 'html': true, 'content': errorMessageElement.innerHTML});
}
button.addEventListener('click', () => {
let content = targetElement.textContent.trim();
if (typeof ClipboardItem !== 'undefined') {
const html = targetElement.innerHTML.trim();
content = [ new ClipboardItem({
['text/plain']: new Blob([content], {type: 'text/plain'}),
['text/html']: new Blob([html], {type: 'text/html'})
})];
}
navigator.clipboard.write(content).then(() => _showPopover(successPopover), () => _showPopover(errorPopover));
});
button.classList.remove('hidden');
}
}

/**
* Initializes the copy to clipboard buttons in the provided container
* @param {object} params Params (has to include a container element)
*/
function updateContainer(params) {
let container = params.container;
container.querySelectorAll('.copy-to-clipboard-button').forEach(_initButton);
}

/**
* Init copy to clipboard
*/
function init() {
updateContainer({container: document});
VuFind.listen('lightbox.rendered', updateContainer);
}

return { init, updateContainer };
});


1 change: 1 addition & 0 deletions themes/bootstrap5/scss/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ $fa-font-path: "../../bootstrap5/css/vendor/font-awesome/webfonts";
@import "components/buttons";
@import "components/channels";
@import "components/cookie-consent/index";
@import "components/copy-to-clipboard";
@import "components/devtools";
@import "components/explain";
@import "components/form";
Expand Down
15 changes: 15 additions & 0 deletions themes/bootstrap5/scss/components/copy-to-clipboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.copy-to-clipboard-container {
display: flex;
align-items: center;

pre {
margin-bottom: 0;
}

.copy-to-clipboard {
.toolbar-btn {
padding: .175rem .5rem;
margin-left: 1rem;
}
}
}
3 changes: 2 additions & 1 deletion themes/bootstrap5/scss/components/icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ span.icon-link .icon-link__label,
// Exceptions
.banner .icon-link__label,
.pager .icon-link__label,
.action-toolbar .icon-link__label {
.action-toolbar .icon-link__label,
button .icon-link__label {
text-decoration: none;
}

Expand Down
9 changes: 9 additions & 0 deletions themes/bootstrap5/scss/components/record.scss
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,12 @@ $tag-remove-badge-color: #a94442 !default;
display: none;
}
}

/* Citation */
.citation {
margin-bottom: .5rem;

.copy-to-clipboard {
margin-left: auto;
}
}
66 changes: 27 additions & 39 deletions themes/bootstrap5/templates/Helpers/copy-to-clipboard-button.phtml
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
<?php
$buttonNumber ??= bin2hex(random_bytes(5));
?>
<p>
<button type="button" id="copyToClipboard<?=$buttonNumber?>" class="btn btn-primary hidden" role="button" tabindex="0"><?=$this->translate('copy_to_clipboard_button_label')?></button>
<span id="copySuccessMessage<?=$buttonNumber?>" class="copyMessage hidden text-success"><small><?=$this->translate('copy_to_clipboard_success_message');?></small></span>
<span id="copyFailureMessage<?=$buttonNumber?>" class="copyMessage hidden text-danger"><small><?=$this->translate('copy_to_clipboard_failure_message');?></small></span>
</p>
<?php
$script = <<<JS
$(document).ready(function copyToClipboard() {
if (navigator.clipboard) {
function copySuccess() {
$("#copyFailureMessage{$buttonNumber}").addClass("hidden");
$("#copySuccessMessage{$buttonNumber}").removeClass("hidden");
}
function copyFailure() {
$("#copySuccessMessage{$buttonNumber}").addClass("hidden");
$("#copyFailureMessage{$buttonNumber}").removeClass("hidden");
}
const button = $("#copyToClipboard{$buttonNumber}");
button.removeClass('hidden');
button.click(function copyToClipboard() {
const text = $('{$selector}').text();
if (typeof ClipboardItem === 'undefined') {
navigator.clipboard.writeText(text).then(copySuccess, copyFailure);
return;
}
const html = $('{$selector}').html();
const data = [ new ClipboardItem({
['text/plain']: new Blob([text], {type: 'text/plain'}),
['text/html']: new Blob([html], {type: 'text/html'})
})];
navigator.clipboard.write(data).then(copySuccess, copyFailure);
});
}
});
JS;
// Inline the script for lightbox compatibility
echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET');
$this->buttonNumber ??= bin2hex(random_bytes(5));
$buttonAttributes = [
'type' => 'button',
'class' => ['btn', 'hidden', 'copy-to-clipboard-button', 'toolbar-btn'],
'role' => 'button',
'tabindex' => '0',
'data-number' => $this->buttonNumber,
'data-target' => $this->selector,
];
$buttonText = $this->icon('clone');
if ($this->hideButtonText) {
$buttonAttributes['title'] = $this->transEscAttr('copy_to_clipboard_button_label');
}
?>
<span class="copy-to-clipboard">
<button <?=$this->htmlAttributes($buttonAttributes)?>>
<?=$this->icon('clone')?>
<span class="icon-link__label <?=$this->hideButtonText ? 'visually-hidden' : ''?>">
<?=$this->transEsc('copy_to_clipboard_button_label')?>
</span>
</button>
<span id="copySuccessMessage<?=$this->buttonNumber?>" class="hidden">
<span class="copyMessage text-success"><small><?=$this->translate('copy_to_clipboard_success_message')?></small></span>
</span>
<span id="copyFailureMessage<?=$this->buttonNumber?>" class="hidden">
<span class="copyMessage text-danger"><small><?=$this->translate('copy_to_clipboard_failure_message')?></small></span>
</span>
</span>
3 changes: 2 additions & 1 deletion themes/bootstrap5/templates/devtools/language.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Current filter mode: <?=$includeOptional ? 'Unfiltered' : 'Mandatory strings onl
<p>Translation of Help in directory <?=$this->icon('format-folder') ?> <em><?=$this->escapeHtml($dirHelp)?></em> as .phtml files.</p>

<template id="template-copy-btn">
<div class="float-right"><?=$this->copyToClipboardButton('.translation-output'); ?></div>
<div class="float-right"><?=$this->copyToClipboardButton('.translation-output', false); ?></div>
</template>

<?php
Expand Down Expand Up @@ -122,6 +122,7 @@ Current filter mode: <?=$includeOptional ? 'Unfiltered' : 'Mandatory strings onl
function bindTextareaEvent() {
var copyBtn = document.getElementById("template-copy-btn").content.cloneNode(true);
$(".modal-body h2").after(copyBtn);
VuFind.copyToClipboard.updateContainer({container: document});
$('.translation-output').click(function(e) {
this.select();
});
Expand Down
10 changes: 7 additions & 3 deletions themes/bootstrap5/templates/record/cite.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@
$longCaption = $this->translate($format . ' Edition Citation');
$caption = (strpos($longCaption, 'Citation') > 0 && !str_contains($shortCaption, 'Citation'))
? $shortCaption : $longCaption;
$elementId = 'citation-' . $this->escapeHtmlAttr($format);
?>
<strong><?=$this->escapeHtml($caption)?></strong>
<p class="text-left">
<?=$citation?>
</p>
<div class="citation copy-to-clipboard-container">
<span id="<?=$elementId?>">
<?=$citation?>
</span>
<?=$this->copyToClipboardButton('#' . $elementId)?>
</div>
<?php endforeach; ?>
<div class="text-muted text-center"><?=$this->transEsc('Warning: These citations may not always be 100% accurate')?>.</div>
<?php endif; ?>
6 changes: 3 additions & 3 deletions themes/bootstrap5/templates/record/permalink.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
$link = $this->serverUrl() . $this->recordLinker()->getUrl($this->driver, ['excludeSearchId' => true]);
?>
<h2><?=$this->transEsc('permanent_link') ?></h2>
<p>
<div class="copy-to-clipboard-container">
<pre id="permalink"><?=$this->escapeHtml($link)?></pre>
</p>
<?=$this->copyToClipboardButton('#permalink');?>
<?=$this->copyToClipboardButton('#permalink');?>
</div>
92 changes: 41 additions & 51 deletions themes/bootstrap5/templates/upgrade/criticalfixblowfish.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,57 @@
<h2>Critical Issue: Replace blowfish encryption</h2>

<div class="alert alert-danger">
<p>The <a href="https://en.wikipedia.org/wiki/Blowfish_(cipher)">Blowfish encryption algorithm</a> is vulnerable to certain attacks, particularly in contexts like HTTPS. <a href="https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers">Versions of OpenSSL starting with 3.0 will no longer support blowfish by default</a>, which can break your VuFind installation. You can <a href="https://openlibraryfoundation.atlassian.net/browse/VUFIND-1563">read more about this issue on the VuFind JIRA ticket</a>.</p>
<p>The <a href="https://en.wikipedia.org/wiki/Blowfish_(cipher)">Blowfish encryption algorithm</a> is vulnerable to certain attacks, particularly in contexts like HTTPS. <a href="https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers">Versions of OpenSSL starting with 3.0 will no longer support blowfish by default</a>, which can break your VuFind installation. You can <a href="https://openlibraryfoundation.atlassian.net/browse/VUFIND-1563">read more about this issue on the VuFind JIRA ticket</a>.</p>
</div>

<h3>1. Enable blowfish</h3>

<p>In order to resolve this problem, we will need to enable blowfish encryption so you can convert your encrypted data to a new method. <a href="#conversion">More information on converting below</a>.</p>

<?php if ($this->blowfishIsWorking): ?>
<div class="alert alert-success">Blowfish is enabled on your system</div>
<div class="alert alert-success">Blowfish is enabled on your system</div>
<?php else: ?>
<div class="panel panel-danger">
<div class="panel-heading">
<p class="panel-title">Blowfish is not enabled on your system</p>
</div>

<div class="panel-body">

<p>Your system's version of OpenSSL may not configured to provide the Blowfish algorithm. You may need to <a href="https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers">follow these steps to enable the legacy provider in OpenSSL</a>.

<ol>
<li>
<p><b>Identify the location of your OpenSSL configuration</b></p>

<pre id="find-openssl">php -i | grep "Openssl default config"</pre>
<?=$this->copyToClipboardButton('find-openssl') ?>
</li>

<li>
<p><b>Edit the OpenSSL config</b> (most likely /usr/lib/ssl/openssl.cnf) and find the Providers configuration. Under Ubuntu, you need to find a [providers_sect] section, and add the line:</p>

<pre>legacy = legacy_sect</pre>
</li>

<li>
<p><b>Find the section for the default provider</b> and make sure to uncomment <kbd>activate = 1</kbd> in that section</p>

<pre>
<div class="panel panel-danger">
<div class="panel-heading">
<p class="panel-title">Blowfish is not enabled on your system</p>
</div>

<div class="panel-body">
<p>Your system's version of OpenSSL may not configured to provide the Blowfish algorithm. You may need to <a href="https://wiki.openssl.org/index.php/OpenSSL_3.0#Providers">follow these steps to enable the legacy provider in OpenSSL</a>.
<ol>
<li>
<p><b>Identify the location of your OpenSSL configuration</b></p>
<div class="copy-to-clipboard-container">
<pre id="find-openssl">php -i | grep "Openssl default config"</pre>
<?=$this->copyToClipboardButton('#find-openssl') ?>
</div>
</li>
<li>
<p><b>Edit the OpenSSL config</b> (most likely /usr/lib/ssl/openssl.cnf) and find the Providers configuration. Under Ubuntu, you need to find a [providers_sect] section, and add the line:</p>
<pre>legacy = legacy_sect</pre>
</li>
<li>
<p><b>Find the section for the default provider</b> and make sure to uncomment <kbd>activate = 1</kbd> in that section</p>
<pre>
[provider_sect]
default = default_sect
legacy = legacy_sect

[default_sect]
activate = 1</pre>
</li>

<li>
<p><b>Add a new section</b> somewhere below:</p>

<pre>
</li>
<li>
<p><b>Add a new section</b> somewhere below:</p>
<pre>
[legacy_sect]
activate = 1</pre>
</li>

<li><b>Restart Apache</b></li>

<li>Refresh this page or <a href="#conversion">move on to the conversion step below</a>.</li>
</ol>

<hr>

<p>Here is a complete example of the changes you will need to make to your OpenSSL configuration file.</p>

<pre>
</li>
<li><b>Restart Apache</b></li>
<li>Refresh this page or <a href="#conversion">move on to the conversion step below</a>.</li>
</ol>
<hr>
<p>Here is a complete example of the changes you will need to make to your OpenSSL configuration file.</p>
<pre>
# /usr/lib/ssl/openssl.cnf or /etc/ssl/openssl.cnf

[openssl_init]
Expand All @@ -80,14 +68,16 @@ activate = 1

[legacy_sect]
activate = 1</pre>
</div>
</div>
</div>
</div>
<?php endif; ?>

<h3 id="conversion">2. Convert existing data to a new encryption method</h3>

<p>You can use VuFind's CLI tool <kbd>switch_db_hash</kbd> to convert all passwords from blowfish to another encryption method. Below, you can find a pre-filled example command that will convert your encryption to <?=$this->escapeHtml($newAlgorithm)?> (the new recommended VuFind encryption method) with a random key. You can use <kbd>openssl_get_cipher_methods()</kbd> to see all encryption methods available on your system.</p>

<pre id="example-cmd">php $VUFIND_HOME/public/index.php util switch_db_hash <?=$this->escapeHtml($newAlgorithm)?> "<?=$this->escapeHtml($exampleKey) ?>"</pre>
<div class="copy-to-clipboard-container">
<pre id="example-cmd">php $VUFIND_HOME/public/index.php util switch_db_hash <?=$this->escapeHtml($newAlgorithm)?> "<?=$this->escapeHtml($exampleKey) ?>"</pre>
<?=$this->copyToClipboardButton('#example-cmd') ?>
</div>

<?=$this->copyToClipboardButton('#example-cmd') ?>
1 change: 1 addition & 0 deletions themes/bootstrap5/theme.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
['file' => 'list_item_selection.js', 'priority' => 400],
['file' => 'covers.js', 'priority' => 410],
['file' => 'validation.js', 'priority' => 420],
['file' => 'copy_to_clipboard.js', 'priority' => 430],
],
/**
* Configuration for a single or multiple favicons.
Expand Down
Loading