Add sensitive field input (#5201)
A field widget that allows for entering of sensitive information that can be revealed at the user's request - ie. API keys, secrets. When a sensitive field that has been previously populated is loaded again, a placeholder is used instead of the real value, until the user opts to reveal the value. The real value is loaded via AJAX. Credit to @tomaszstrojny for the original implementation. Replaces #5062. Fixes #5061, #1850, perhaps #1061. Co-authored-by: Tomasz Strojny <tomasz@init.biz> Co-authored-by: Luke Towers <github@luketowers.ca>
This commit is contained in:
parent
b5dcc42ed2
commit
4950edc196
|
|
@ -80,6 +80,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
$combiner->registerBundle('~/modules/backend/formwidgets/colorpicker/assets/less/colorpicker.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/permissioneditor/assets/less/permissioneditor.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/markdowneditor/assets/less/markdowneditor.less');
|
||||
$combiner->registerBundle('~/modules/backend/formwidgets/sensitive/assets/less/sensitive.less');
|
||||
|
||||
/*
|
||||
* Rich Editor is protected by DRM
|
||||
|
|
@ -199,6 +200,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
$manager->registerFormWidget('Backend\FormWidgets\TagList', 'taglist');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\MediaFinder', 'mediafinder');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\NestedForm', 'nestedform');
|
||||
$manager->registerFormWidget('Backend\FormWidgets\Sensitive', 'sensitive');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
<?php namespace Backend\FormWidgets;
|
||||
|
||||
use Backend\Classes\FormWidgetBase;
|
||||
|
||||
/**
|
||||
* Sensitive widget.
|
||||
*
|
||||
* Renders a password field that can be optionally made visible
|
||||
*
|
||||
* @package october\backend
|
||||
*/
|
||||
class Sensitive extends FormWidgetBase
|
||||
{
|
||||
/**
|
||||
* @var bool If true, the sensitive field cannot be edited, but can be toggled.
|
||||
*/
|
||||
public $readOnly = false;
|
||||
|
||||
/**
|
||||
* @var bool If true, the sensitive field is disabled.
|
||||
*/
|
||||
public $disabled = false;
|
||||
|
||||
/**
|
||||
* @var bool If true, a button will be available to copy the value.
|
||||
*/
|
||||
public $allowCopy = false;
|
||||
|
||||
/**
|
||||
* @var string The string that will be used as a placeholder for an unrevealed sensitive value.
|
||||
*/
|
||||
public $hiddenPlaceholder = '__hidden__';
|
||||
|
||||
/**
|
||||
* @var bool If true, the sensitive input will be hidden if the user changes to another tab in their browser.
|
||||
*/
|
||||
public $hideOnTabChange = true;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $defaultAlias = 'sensitive';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->fillFromConfig([
|
||||
'readOnly',
|
||||
'disabled',
|
||||
'allowCopy',
|
||||
'hiddenPlaceholder',
|
||||
'hideOnTabChange',
|
||||
]);
|
||||
|
||||
if ($this->formField->disabled || $this->formField->readOnly) {
|
||||
$this->previewMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->prepareVars();
|
||||
|
||||
return $this->makePartial('sensitive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the view data for the widget partial.
|
||||
*/
|
||||
public function prepareVars()
|
||||
{
|
||||
$this->vars['readOnly'] = $this->readOnly;
|
||||
$this->vars['disabled'] = $this->disabled;
|
||||
$this->vars['hasValue'] = !empty($this->getLoadValue());
|
||||
$this->vars['allowCopy'] = $this->allowCopy;
|
||||
$this->vars['hiddenPlaceholder'] = $this->hiddenPlaceholder;
|
||||
$this->vars['hideOnTabChange'] = $this->hideOnTabChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals the value of a hidden, unmodified sensitive field.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function onShowValue()
|
||||
{
|
||||
return [
|
||||
'value' => $this->getLoadValue()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSaveValue($value)
|
||||
{
|
||||
if ($value === $this->hiddenPlaceholder) {
|
||||
$value = $this->getLoadValue();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function loadAssets()
|
||||
{
|
||||
$this->addCss('css/sensitive.css', 'core');
|
||||
$this->addJs('js/sensitive.js', 'core');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
div[data-control="sensitive"] a[data-toggle],
|
||||
div[data-control="sensitive"] a[data-copy] {box-shadow:none;border:1px solid #d1d6d9;border-left:0}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Sensitive field widget plugin.
|
||||
*
|
||||
* Data attributes:
|
||||
* - data-control="sensitive" - enables the plugin on an element
|
||||
*
|
||||
* JavaScript API:
|
||||
* $('div#someElement').sensitive({...})
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
var Base = $.oc.foundation.base,
|
||||
BaseProto = Base.prototype
|
||||
|
||||
var Sensitive = function(element, options) {
|
||||
this.$el = $(element)
|
||||
this.options = options
|
||||
this.clean = Boolean(this.$el.data('clean'))
|
||||
this.hidden = true
|
||||
|
||||
this.$input = this.$el.find('[data-input]').first()
|
||||
this.$toggle = this.$el.find('[data-toggle]').first()
|
||||
this.$icon = this.$el.find('[data-icon]').first()
|
||||
this.$loader = this.$el.find('[data-loader]').first()
|
||||
this.$copy = this.$el.find('[data-copy]').first()
|
||||
|
||||
$.oc.foundation.controlUtils.markDisposable(element)
|
||||
Base.call(this)
|
||||
this.init()
|
||||
}
|
||||
|
||||
Sensitive.DEFAULTS = {
|
||||
readOnly: false,
|
||||
disabled: false,
|
||||
eventHandler: null,
|
||||
hideOnTabChange: false,
|
||||
}
|
||||
|
||||
Sensitive.prototype = Object.create(BaseProto)
|
||||
Sensitive.prototype.constructor = Sensitive
|
||||
|
||||
Sensitive.prototype.init = function() {
|
||||
this.$input.on('keydown', this.proxy(this.onInput))
|
||||
this.$toggle.on('click', this.proxy(this.onToggle))
|
||||
|
||||
if (this.options.hideOnTabChange) {
|
||||
// Watch for tab change or minimise
|
||||
document.addEventListener('visibilitychange', this.proxy(this.onTabChange))
|
||||
}
|
||||
|
||||
if (this.$copy.length) {
|
||||
this.$copy.on('click', this.proxy(this.onCopy))
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.dispose = function () {
|
||||
this.$input.off('keydown', this.proxy(this.onInput))
|
||||
this.$toggle.off('click', this.proxy(this.onToggle))
|
||||
|
||||
if (this.options.hideOnTabChange) {
|
||||
document.removeEventListener('visibilitychange', this.proxy(this.onTabChange))
|
||||
}
|
||||
|
||||
if (this.$copy.length) {
|
||||
this.$copy.off('click', this.proxy(this.onCopy))
|
||||
}
|
||||
|
||||
this.$input = this.$toggle = this.$icon = this.$loader = null
|
||||
this.$el = null
|
||||
|
||||
BaseProto.dispose.call(this)
|
||||
}
|
||||
|
||||
Sensitive.prototype.onInput = function() {
|
||||
if (this.clean) {
|
||||
this.clean = false
|
||||
this.$input.val('')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Sensitive.prototype.onToggle = function() {
|
||||
if (this.$input.val() !== '' && this.clean) {
|
||||
this.reveal()
|
||||
} else {
|
||||
this.toggleVisibility()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Sensitive.prototype.onTabChange = function() {
|
||||
if (document.hidden && !this.hidden) {
|
||||
this.toggleVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.onCopy = function() {
|
||||
var that = this,
|
||||
deferred = $.Deferred(),
|
||||
isHidden = this.hidden
|
||||
|
||||
deferred.then(function () {
|
||||
if (that.hidden) {
|
||||
that.toggleVisibility()
|
||||
}
|
||||
|
||||
that.$input.focus()
|
||||
that.$input.select()
|
||||
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
that.$input.blur()
|
||||
if (isHidden) {
|
||||
that.toggleVisibility()
|
||||
}
|
||||
})
|
||||
|
||||
if (this.$input.val() !== '' && this.clean) {
|
||||
this.reveal(deferred)
|
||||
} else {
|
||||
deferred.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
Sensitive.prototype.toggleVisibility = function() {
|
||||
if (this.hidden) {
|
||||
this.$input.attr('type', 'text')
|
||||
} else {
|
||||
this.$input.attr('type', 'password')
|
||||
}
|
||||
|
||||
this.$icon.toggleClass('icon-eye icon-eye-slash')
|
||||
|
||||
this.hidden = !this.hidden
|
||||
}
|
||||
|
||||
Sensitive.prototype.reveal = function(deferred) {
|
||||
var that = this
|
||||
this.$icon.css({
|
||||
visibility: 'hidden'
|
||||
})
|
||||
this.$loader.removeClass('hide')
|
||||
|
||||
this.$input.request(this.options.eventHandler, {
|
||||
success: function (data) {
|
||||
that.$input.val(data.value)
|
||||
that.clean = false
|
||||
|
||||
that.$icon.css({
|
||||
visibility: 'visible'
|
||||
})
|
||||
that.$loader.addClass('hide')
|
||||
|
||||
that.toggleVisibility()
|
||||
|
||||
if (deferred) {
|
||||
deferred.resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.sensitive
|
||||
|
||||
$.fn.sensitive = function (option) {
|
||||
var args = Array.prototype.slice.call(arguments, 1), result
|
||||
this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('oc.sensitive')
|
||||
var options = $.extend({}, Sensitive.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
if (!data) $this.data('oc.sensitive', (data = new Sensitive(this, options)))
|
||||
if (typeof option == 'string') result = data[option].apply(data, args)
|
||||
if (typeof result != 'undefined') return false
|
||||
})
|
||||
|
||||
return result ? result : this
|
||||
}
|
||||
|
||||
$.fn.sensitive.noConflict = function () {
|
||||
$.fn.sensitive = old
|
||||
return this
|
||||
}
|
||||
|
||||
$(document).render(function () {
|
||||
$('[data-control="sensitive"]').sensitive()
|
||||
});
|
||||
|
||||
}(window.jQuery);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
div[data-control="sensitive"] {
|
||||
a[data-toggle],
|
||||
a[data-copy] {
|
||||
box-shadow: none;
|
||||
border: 1px solid @input-group-addon-border-color;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<div
|
||||
data-control="sensitive"
|
||||
data-clean="true"
|
||||
data-event-handler="<?= $this->getEventHandler('onShowValue') ?>"
|
||||
<?php if ($hideOnTabChange): ?>data-hide-on-tab-change="true"<?php endif ?>
|
||||
>
|
||||
<div class="loading-indicator-container size-form-field">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
name="<?= $this->getFieldName() ?>"
|
||||
id="<?= $this->getId() ?>"
|
||||
value="<?= ($hasValue) ? $hiddenPlaceholder : '' ?>"
|
||||
placeholder="<?= e(trans($this->formField->placeholder)) ?>"
|
||||
class="form-control"
|
||||
<?php if ($this->previewMode): ?>disabled="disabled"<?php endif ?>
|
||||
autocomplete="off"
|
||||
data-input
|
||||
/>
|
||||
<?php if ($allowCopy): ?>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="input-group-addon btn btn-secondary"
|
||||
data-copy
|
||||
>
|
||||
<i class="icon-copy"></i>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="input-group-addon btn btn-secondary"
|
||||
data-toggle
|
||||
>
|
||||
<i class="icon-eye" data-icon></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="loading-indicator hide" data-loader>
|
||||
<span class="p-a"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -79,6 +79,7 @@ tabs:
|
|||
smtp_password:
|
||||
label: system::lang.mail.smtp_password
|
||||
tab: system::lang.mail.general
|
||||
type: sensitive
|
||||
span: right
|
||||
trigger:
|
||||
action: show
|
||||
|
|
@ -107,6 +108,7 @@ tabs:
|
|||
label: system::lang.mail.mailgun_secret
|
||||
commentAbove: system::lang.mail.mailgun_secret_comment
|
||||
tab: system::lang.mail.general
|
||||
type: sensitive
|
||||
trigger:
|
||||
action: show
|
||||
field: send_mode
|
||||
|
|
@ -116,6 +118,7 @@ tabs:
|
|||
label: system::lang.mail.mandrill_secret
|
||||
commentAbove: system::lang.mail.mandrill_secret_comment
|
||||
tab: system::lang.mail.general
|
||||
type: sensitive
|
||||
trigger:
|
||||
action: show
|
||||
field: send_mode
|
||||
|
|
@ -135,6 +138,7 @@ tabs:
|
|||
label: system::lang.mail.ses_secret
|
||||
commentAbove: system::lang.mail.ses_secret_comment
|
||||
tab: system::lang.mail.general
|
||||
type: sensitive
|
||||
span: right
|
||||
trigger:
|
||||
action: show
|
||||
|
|
@ -154,6 +158,7 @@ tabs:
|
|||
sparkpost_secret:
|
||||
label: system::lang.mail.sparkpost_secret
|
||||
commentAbove: system::lang.mail.sparkpost_secret_comment
|
||||
type: sensitive
|
||||
tab: system::lang.mail.general
|
||||
trigger:
|
||||
action: show
|
||||
|
|
|
|||
Loading…
Reference in New Issue