Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

98 lines
3.8 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* @package MokoSuite
* @subpackage plg_system_mokosuite
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuite
* VERSION: 02.34.83
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
namespace Moko\Plugin\System\MokoSuite\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Session\Session;
/**
* Renders a read-only text input with a "Copy" button, similar to
* Joomla's API token field in the user profile.
*
* @since 02.25.00
*/
class CopyableTokenField extends FormField
{
protected $type = 'CopyableToken';
protected function getInput()
{
$value = htmlspecialchars($this->value ?? '', ENT_QUOTES, 'UTF-8');
$id = $this->id;
if (empty($this->value))
{
return '<div class="alert alert-warning mb-0 py-2">Token will be generated automatically on first save.</div>';
}
$pin = strtoupper(substr($this->value, 0, 4) . '-' . substr($this->value, 4, 4));
$token = Session::getFormToken();
$ajaxUrl = 'index.php?option=com_mokosuite&task=display.sendHeartbeat&format=json';
return <<<HTML
<div class="input-group mb-2">
<input type="text" id="{$id}" name="{$this->name}" value="{$value}"
class="form-control" readonly="readonly" style="font-family:monospace;font-size:0.85em" />
<button type="button" class="btn btn-outline-secondary" onclick="
var inp = document.getElementById('{$id}');
if (navigator.clipboard) {
navigator.clipboard.writeText(inp.value).then(function() {
var btn = inp.nextElementSibling;
var orig = btn.innerHTML;
btn.innerHTML = '<span class=&quot;icon-check&quot; aria-hidden=&quot;true&quot;></span> Copied';
btn.classList.replace('btn-outline-secondary','btn-success');
setTimeout(function(){ btn.innerHTML = orig; btn.classList.replace('btn-success','btn-outline-secondary'); }, 2000);
});
} else {
inp.select(); document.execCommand('copy');
}
"><span class="icon-copy" aria-hidden="true"></span> Copy</button>
<button type="button" class="btn btn-outline-primary" id="mokosuite-send-heartbeat" onclick="
var btn = this;
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class=&quot;icon-spinner icon-spin&quot; aria-hidden=&quot;true&quot;></span> Sending...';
var fd = new FormData();
fd.append('{$token}', '1');
fetch('{$ajaxUrl}', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if(d.success){
btn.innerHTML='<span class=&quot;icon-check&quot; aria-hidden=&quot;true&quot;></span> Sent';
btn.classList.replace('btn-outline-primary','btn-success');
} else {
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Failed';
btn.classList.replace('btn-outline-primary','btn-danger');
}
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
})
.catch(function(){
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Error';
btn.classList.replace('btn-outline-primary','btn-danger');
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
});
"><span class="icon-heart" aria-hidden="true"></span> Send Heartbeat</button>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-dark" style="font-family:monospace;font-size:1rem;letter-spacing:0.1em;">MOKO-{$pin}</span>
<small class="text-muted">Support verification PIN — ask your provider for this code to verify your identity.</small>
</div>
HTML;
}
}