2026-06-04 20:30:28 -05:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @package MokoJoomBackup
|
2026-06-06 14:52:27 -05:00
|
|
|
* @subpackage com_mokojoombackup
|
2026-06-04 20:30:28 -05:00
|
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
|
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
|
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
2026-06-06 14:52:27 -05:00
|
|
|
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
2026-06-04 20:30:28 -05:00
|
|
|
|
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
|
|
|
|
|
|
use Joomla\CMS\Form\FormField;
|
|
|
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
|
|
|
|
|
|
class FolderPickerField extends FormField
|
|
|
|
|
{
|
|
|
|
|
protected $type = 'FolderPicker';
|
|
|
|
|
|
|
|
|
|
protected function getInput(): string
|
|
|
|
|
{
|
|
|
|
|
$value = htmlspecialchars($this->value ?: $this->default, ENT_QUOTES, 'UTF-8');
|
|
|
|
|
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
|
|
|
|
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
|
|
|
|
$jRoot = JPATH_ROOT;
|
|
|
|
|
|
|
|
|
|
// Resolve to absolute for display
|
|
|
|
|
$rawValue = $this->value ?: $this->default;
|
|
|
|
|
|
|
|
|
|
if ($rawValue && $rawValue[0] !== '/') {
|
|
|
|
|
$absPath = $jRoot . '/' . $rawValue;
|
|
|
|
|
} else {
|
|
|
|
|
$absPath = $rawValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$exists = is_dir($absPath);
|
|
|
|
|
$statusClass = $exists ? 'text-success' : 'text-danger';
|
|
|
|
|
$statusIcon = $exists ? 'icon-publish' : 'icon-unpublish';
|
|
|
|
|
$statusText = $exists
|
2026-06-06 14:52:27 -05:00
|
|
|
? Text::_('COM_MOKOJOOMBACKUP_FOLDER_EXISTS')
|
|
|
|
|
: Text::_('COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND');
|
2026-06-04 20:30:28 -05:00
|
|
|
$absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
return <<<HTML
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input type="text" name="{$name}" id="{$id}" value="{$value}"
|
|
|
|
|
class="form-control" maxlength="512"
|
2026-06-06 14:52:27 -05:00
|
|
|
placeholder="/home/user/backups or administrator/components/com_mokojoombackup/backups" />
|
2026-06-04 20:30:28 -05:00
|
|
|
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
|
|
|
|
|
<span class="icon-folder-open" aria-hidden="true"></span>
|
|
|
|
|
Browse
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-1">
|
|
|
|
|
<small class="{$statusClass}">
|
|
|
|
|
<span class="{$statusIcon}" aria-hidden="true"></span>
|
|
|
|
|
{$statusText}: <code>{$absPathSafe}</code>
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
|
|
|
|
|
<div class="card-body p-2">
|
|
|
|
|
<div id="{$id}_tree"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
var fieldId = '{$id}';
|
|
|
|
|
var btn = document.getElementById(fieldId + '_btn');
|
|
|
|
|
var browser = document.getElementById(fieldId + '_browser');
|
|
|
|
|
var tree = document.getElementById(fieldId + '_tree');
|
|
|
|
|
var input = document.getElementById(fieldId);
|
|
|
|
|
|
|
|
|
|
btn.addEventListener('click', function() {
|
|
|
|
|
if (browser.style.display !== 'none') {
|
|
|
|
|
browser.style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
browser.style.display = 'block';
|
|
|
|
|
loadDir(input.value || '/');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function loadDir(path) {
|
|
|
|
|
tree.textContent = 'Loading...';
|
|
|
|
|
|
|
|
|
|
var form = new URLSearchParams();
|
|
|
|
|
form.append('task', 'ajax.browseDir');
|
|
|
|
|
form.append('path', path);
|
|
|
|
|
|
|
|
|
|
var tokenName = Joomla.getOptions('csrf.token') || '';
|
|
|
|
|
if (tokenName) form.append(tokenName, '1');
|
|
|
|
|
|
2026-06-06 14:52:27 -05:00
|
|
|
fetch('index.php?option=com_mokojoombackup&format=json', {
|
2026-06-04 20:30:28 -05:00
|
|
|
method: 'POST',
|
|
|
|
|
body: form,
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
|
|
})
|
|
|
|
|
.then(function(r) { return r.json(); })
|
|
|
|
|
.then(function(data) {
|
|
|
|
|
if (data.error) {
|
|
|
|
|
tree.textContent = data.message || 'Error loading directory';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renderTree(data, path);
|
|
|
|
|
})
|
|
|
|
|
.catch(function(err) {
|
|
|
|
|
tree.textContent = 'Error: ' + err.message;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTree(data, path) {
|
|
|
|
|
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
|
|
|
|
|
|
|
|
|
var list = document.createElement('div');
|
|
|
|
|
list.className = 'list-group list-group-flush';
|
|
|
|
|
|
|
|
|
|
if (data.parent) {
|
|
|
|
|
var up = document.createElement('a');
|
|
|
|
|
up.href = '#';
|
|
|
|
|
up.className = 'list-group-item list-group-item-action py-1';
|
|
|
|
|
var upIcon = document.createElement('span');
|
|
|
|
|
upIcon.className = 'icon-arrow-up-4';
|
|
|
|
|
upIcon.setAttribute('aria-hidden', 'true');
|
|
|
|
|
up.appendChild(upIcon);
|
|
|
|
|
up.appendChild(document.createTextNode(' ..'));
|
|
|
|
|
up.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
loadDir(data.parent);
|
|
|
|
|
});
|
|
|
|
|
list.appendChild(up);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(data.dirs || []).forEach(function(dir) {
|
|
|
|
|
var item = document.createElement('a');
|
|
|
|
|
item.href = '#';
|
|
|
|
|
item.className = 'list-group-item list-group-item-action py-1';
|
|
|
|
|
var icon = document.createElement('span');
|
|
|
|
|
icon.className = 'icon-folder';
|
|
|
|
|
icon.setAttribute('aria-hidden', 'true');
|
|
|
|
|
item.appendChild(icon);
|
|
|
|
|
item.appendChild(document.createTextNode(' ' + dir.name));
|
|
|
|
|
item.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
input.value = dir.path;
|
|
|
|
|
loadDir(dir.path);
|
|
|
|
|
});
|
|
|
|
|
item.addEventListener('dblclick', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
input.value = dir.path;
|
|
|
|
|
browser.style.display = 'none';
|
|
|
|
|
});
|
|
|
|
|
list.appendChild(item);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tree.appendChild(list);
|
|
|
|
|
|
|
|
|
|
var info = document.createElement('div');
|
|
|
|
|
info.className = 'mt-2 p-1';
|
|
|
|
|
var small = document.createElement('small');
|
|
|
|
|
small.className = 'text-muted';
|
|
|
|
|
small.textContent = 'Current: ' + (data.current || path);
|
|
|
|
|
info.appendChild(small);
|
|
|
|
|
tree.appendChild(info);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
HTML;
|
|
|
|
|
}
|
|
|
|
|
}
|