-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
b5dcc42
commit 4950edc
Showing
7 changed files
with
369 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
modules/backend/formwidgets/sensitive/assets/css/sensitive.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} |
192 changes: 192 additions & 0 deletions
192
modules/backend/formwidgets/sensitive/assets/js/sensitive.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
10 changes: 10 additions & 0 deletions
10
modules/backend/formwidgets/sensitive/assets/less/sensitive.less
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
modules/backend/formwidgets/sensitive/partials/_sensitive.htm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.