Files
proxmox-selfservice/support-provisioning-portal/includes/class-spp-admin-page.php
Sven Steinert 118809bfae - Added template typing so approved templates can represent either QEMU VMs or LXC containers.
- Added LXC template discovery from Proxmox storage `vztmpl` content in the admin template manager.
- Added live LXC container provisioning through the Proxmox API with configurable rootfs storage and optional DHCP bridge.
- Routed start, stop, delete, expiration, status, and IP refresh operations through typed Proxmox VM/LXC API paths.
- Added Proxmox tags to newly created VMs and containers, including a sanitized per-user tag for easier PVE administration.
- Updated the admin and portal UI to show VM versus LXC template/deployment types and generic Proxmox resource IDs.
- Added schema upgrades for template provisioning type, LXC template references, and deployment resource type.
- Documented LXC setup, storage permissions, and the new Proxmox settings.
2026-04-24 17:11:39 +02:00

810 lines
40 KiB
PHP

<?php
if (!defined('ABSPATH')) {
exit;
}
final class SPP_Admin_Page
{
public function __construct(
private SPP_Repository $repository,
private SPP_Permissions $permissions,
private SPP_Proxmox_Client $proxmox
) {
}
public function register_hooks(): void
{
add_action('admin_menu', [$this, 'register_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_spp_save_settings', [$this, 'save_settings']);
add_action('admin_post_spp_save_user_access', [$this, 'save_user_access']);
add_action('admin_post_spp_save_template', [$this, 'save_template']);
}
public function register_menu(): void
{
add_menu_page(
'Support Provisioning',
'Support Provisioning',
'exist',
'support-provisioning-portal',
[$this, 'render'],
'dashicons-cloud',
58
);
}
public function enqueue_assets(string $hook): void
{
if ($hook !== 'toplevel_page_support-provisioning-portal') {
return;
}
wp_enqueue_style('spp-portal', SPP_PLUGIN_URL . 'assets/portal.css', [], SPP_VERSION);
wp_enqueue_script('spp-portal', SPP_PLUGIN_URL . 'assets/portal.js', [], SPP_VERSION, true);
}
public function render(): void
{
if (!$this->permissions->current_user_has_any()) {
wp_die(esc_html__('You do not have permission to access this page.', 'support-provisioning-portal'));
}
$can_view_portal = $this->permissions->current_user_has(SPP_Permissions::VIEW_PORTAL);
$can_manage_templates = $this->permissions->current_user_has(SPP_Permissions::MANAGE_TEMPLATES);
$can_manage_settings = $this->permissions->current_user_has(SPP_Permissions::MANAGE_SETTINGS);
$can_manage_permissions = $this->permissions->current_user_has(SPP_Permissions::MANAGE_PERMISSIONS);
?>
<div class="wrap spp-admin-wrap">
<h1><?php echo esc_html__('Support Provisioning Portal', 'support-provisioning-portal'); ?></h1>
<?php $this->render_notices(); ?>
<div class="<?php echo esc_attr($can_view_portal && $can_manage_settings ? 'spp-admin-grid' : 'spp-admin-stack'); ?>">
<div>
<?php
if ($can_view_portal) {
$this->render_app_root();
} else {
$this->render_admin_only_notice();
}
?>
</div>
<?php if ($can_manage_settings) : ?>
<div class="spp-admin-side">
<?php $this->render_settings(); ?>
</div>
<?php endif; ?>
</div>
<?php
if ($can_manage_templates) {
$this->render_template_management();
}
if ($can_manage_permissions) {
$this->render_user_access_management();
}
?>
</div>
<?php
}
private function render_notices(): void
{
$notice = isset($_GET['spp_notice']) ? sanitize_key((string) wp_unslash($_GET['spp_notice'])) : '';
if ($notice === '') {
return;
}
$messages = [
'settings_saved' => __('Settings saved.', 'support-provisioning-portal'),
'template_saved' => __('Template saved.', 'support-provisioning-portal'),
'template_removed' => __('Template removed from new provisioning.', 'support-provisioning-portal'),
'template_error' => __('Template could not be saved. Check the fields and confirm the selected Proxmox VM or LXC template exists on the configured node.', 'support-provisioning-portal'),
'user_access_saved' => __('User rights saved.', 'support-provisioning-portal'),
'manager_required' => __('At least one user must keep the Manage user rights permission.', 'support-provisioning-portal'),
];
if (!isset($messages[$notice])) {
return;
}
$class = in_array($notice, ['manager_required', 'template_error'], true) ? 'notice notice-error' : 'notice notice-success';
printf(
'<div class="%s"><p>%s</p></div>',
esc_attr($class),
esc_html($messages[$notice])
);
}
private function render_admin_only_notice(): void
{
?>
<div class="spp-panel spp-admin-notice">
<p><?php echo esc_html__('Portal access is not enabled for your user. Administrative sections available to you are shown on this page.', 'support-provisioning-portal'); ?></p>
</div>
<?php
}
private function render_app_root(): void
{
?>
<div
id="spp-portal-root"
class="spp-portal"
data-rest-url="<?php echo esc_url_raw(rest_url('support-provisioning/v1')); ?>"
data-nonce="<?php echo esc_attr(wp_create_nonce('wp_rest')); ?>"
data-permissions="<?php echo esc_attr((string) wp_json_encode($this->permissions->current_user_permissions())); ?>"
></div>
<?php
}
private function render_settings(): void
{
?>
<form class="spp-settings" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="spp_save_settings">
<?php wp_nonce_field('spp_save_settings'); ?>
<h2><?php echo esc_html__('Proxmox Settings', 'support-provisioning-portal'); ?></h2>
<label>
<span>Mode</span>
<select name="spp_proxmox_mode">
<option value="mock" <?php selected(get_option('spp_proxmox_mode', 'mock'), 'mock'); ?>>Mock</option>
<option value="http" <?php selected(get_option('spp_proxmox_mode', 'mock'), 'http'); ?>>HTTP token auth</option>
</select>
</label>
<label>
<span>Base URL</span>
<input name="spp_proxmox_base_url" type="url" value="<?php echo esc_attr(get_option('spp_proxmox_base_url', '')); ?>" placeholder="https://proxmox.example.internal:8006">
</label>
<label>
<span>Token ID</span>
<input name="spp_proxmox_token_id" type="text" value="<?php echo esc_attr(get_option('spp_proxmox_token_id', '')); ?>" placeholder="user@realm!token-name">
</label>
<label>
<span>Token Secret</span>
<input name="spp_proxmox_token_secret" type="password" value="" placeholder="Leave blank to keep existing secret">
</label>
<label>
<span>Node</span>
<input name="spp_proxmox_node" type="text" value="<?php echo esc_attr(get_option('spp_proxmox_node', 'pve-01')); ?>">
</label>
<label>
<span>LXC rootfs storage</span>
<input name="spp_lxc_rootfs_storage" type="text" value="<?php echo esc_attr(get_option('spp_lxc_rootfs_storage', '')); ?>" placeholder="local-lvm">
</label>
<label>
<span>LXC network bridge</span>
<input name="spp_lxc_bridge" type="text" value="<?php echo esc_attr(get_option('spp_lxc_bridge', 'vmbr0')); ?>" placeholder="vmbr0">
</label>
<h2><?php echo esc_html__('RAM Contingents', 'support-provisioning-portal'); ?></h2>
<p class="description"><?php echo esc_html__('Set 0 for unlimited. Active allocations include provisioning, stopped, running, and deleting deployments.', 'support-provisioning-portal'); ?></p>
<label>
<span>Default per-user RAM limit (MB)</span>
<input name="spp_quota_user_memory_mb" type="number" min="0" step="256" value="<?php echo esc_attr((string) get_option('spp_quota_user_memory_mb', 0)); ?>">
</label>
<label>
<span>Global RAM limit (MB)</span>
<input name="spp_quota_global_memory_mb" type="number" min="0" step="256" value="<?php echo esc_attr((string) get_option('spp_quota_global_memory_mb', 0)); ?>">
</label>
<?php submit_button('Save Settings'); ?>
</form>
<?php
}
private function render_template_management(): void
{
$approved_templates = $this->repository->admin_templates();
$active_template_keys = $this->repository->active_template_identity_keys();
$proxmox_error = null;
try {
$proxmox_templates = $this->proxmox->list_templates();
} catch (Throwable $error) {
$proxmox_templates = [];
$proxmox_error = $error->getMessage();
}
?>
<section class="spp-settings spp-template-admin">
<div class="spp-section-header">
<div>
<h2><?php echo esc_html__('Templates', 'support-provisioning-portal'); ?></h2>
<p class="description"><?php echo esc_html__('Approved templates are stored by the plugin and used for new deployments. Proxmox templates can be imported from the configured node.', 'support-provisioning-portal'); ?></p>
</div>
</div>
<h3><?php echo esc_html__('Approved Templates', 'support-provisioning-portal'); ?></h3>
<div class="spp-template-list">
<?php if (empty($approved_templates)) : ?>
<div class="spp-panel spp-admin-notice">
<p><?php echo esc_html__('No plugin templates are approved yet. Import one from Proxmox below.', 'support-provisioning-portal'); ?></p>
</div>
<?php endif; ?>
<?php foreach ($approved_templates as $template) : ?>
<form class="spp-template-row" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="spp_save_template">
<input type="hidden" name="spp_template_id" value="<?php echo esc_attr((string) $template['id']); ?>">
<?php wp_nonce_field('spp_save_template'); ?>
<div class="spp-template-row-head">
<div>
<strong><?php echo esc_html((string) $template['name']); ?></strong>
<span><?php echo esc_html($this->template_identity_label($template)); ?></span>
</div>
<?php if (!empty($template['isActive'])) : ?>
<span class="spp-badge RUNNING"><?php echo esc_html__('Active', 'support-provisioning-portal'); ?></span>
<?php else : ?>
<span class="spp-badge STOPPED"><?php echo esc_html__('Inactive', 'support-provisioning-portal'); ?></span>
<?php endif; ?>
</div>
<div class="spp-template-fields">
<input type="hidden" name="spp_provisioning_type" value="<?php echo esc_attr((string) $template['provisioningType']); ?>">
<label>Name
<input name="spp_template_name" type="text" required maxlength="160" value="<?php echo esc_attr((string) $template['name']); ?>">
</label>
<label>Type
<input type="text" readonly value="<?php echo esc_attr($this->template_type_label((string) $template['provisioningType'])); ?>">
</label>
<?php if ((string) $template['provisioningType'] === 'lxc') : ?>
<input type="hidden" name="spp_proxmox_template_id" value="0">
<label>LXC template ref
<input name="spp_proxmox_template_ref" type="text" readonly required value="<?php echo esc_attr((string) $template['proxmoxTemplateRef']); ?>">
</label>
<?php else : ?>
<input type="hidden" name="spp_proxmox_template_ref" value="">
<label>PVE template VMID
<input name="spp_proxmox_template_id" type="number" min="1" required value="<?php echo esc_attr((string) $template['proxmoxTemplateId']); ?>">
</label>
<?php endif; ?>
<label>OS type
<?php $this->render_os_type_select((string) $template['osType']); ?>
</label>
<label>CPU cores
<input name="spp_cpu_cores" type="number" min="1" required value="<?php echo esc_attr((string) $template['cpuCores']); ?>">
</label>
<label>Memory MB
<input name="spp_memory_mb" type="number" min="128" step="128" required value="<?php echo esc_attr((string) $template['memoryMb']); ?>">
</label>
<label>Disk GB
<input name="spp_disk_gb" type="number" min="1" required value="<?php echo esc_attr((string) $template['diskGb']); ?>">
</label>
<label>Default TTL hours
<input name="spp_default_ttl_hours" type="number" min="1" max="720" required value="<?php echo esc_attr((string) $template['defaultTtlHours']); ?>">
</label>
<label class="spp-check">
<input name="spp_is_active" type="checkbox" value="1" <?php checked(!empty($template['isActive'])); ?>>
<span>Active for new deployments</span>
</label>
<label class="spp-template-description">Description
<textarea name="spp_template_description" rows="2" required><?php echo esc_textarea((string) $template['description']); ?></textarea>
</label>
</div>
<div class="spp-template-actions">
<button class="button button-primary" name="spp_template_action" value="save" type="submit"><?php echo esc_html__('Save Template', 'support-provisioning-portal'); ?></button>
<button class="button" name="spp_template_action" value="remove" type="submit"><?php echo esc_html__('Remove', 'support-provisioning-portal'); ?></button>
</div>
</form>
<?php endforeach; ?>
</div>
<h3><?php echo esc_html__('Proxmox Templates On Configured Node', 'support-provisioning-portal'); ?></h3>
<?php if ($proxmox_error !== null) : ?>
<p class="spp-error"><?php echo esc_html($proxmox_error); ?></p>
<?php elseif (empty($proxmox_templates)) : ?>
<div class="spp-panel spp-admin-notice">
<p><?php echo esc_html__('No QEMU VM or LXC templates were returned by Proxmox for the configured node.', 'support-provisioning-portal'); ?></p>
</div>
<?php else : ?>
<div class="spp-pve-template-grid">
<?php foreach ($proxmox_templates as $template) : ?>
<?php
$provisioning_type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
$template_ref = (string) ($template['templateRef'] ?? '');
$is_imported = in_array(
$this->template_identity_key($provisioning_type, (int) ($template['vmId'] ?? 0), $template_ref),
$active_template_keys,
true
);
?>
<form class="spp-pve-template" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="spp_save_template">
<input type="hidden" name="spp_template_action" value="import">
<input type="hidden" name="spp_provisioning_type" value="<?php echo esc_attr($provisioning_type); ?>">
<input type="hidden" name="spp_template_name" value="<?php echo esc_attr((string) $template['name']); ?>">
<input type="hidden" name="spp_proxmox_template_id" value="<?php echo esc_attr((string) ($template['vmId'] ?? 0)); ?>">
<input type="hidden" name="spp_proxmox_template_ref" value="<?php echo esc_attr($template_ref); ?>">
<input type="hidden" name="spp_cpu_cores" value="<?php echo esc_attr((string) $template['cpuCores']); ?>">
<input type="hidden" name="spp_memory_mb" value="<?php echo esc_attr((string) $template['memoryMb']); ?>">
<input type="hidden" name="spp_disk_gb" value="<?php echo esc_attr((string) $template['diskGb']); ?>">
<?php wp_nonce_field('spp_save_template'); ?>
<div class="spp-template-row-head">
<div>
<strong><?php echo esc_html((string) $template['name']); ?></strong>
<span><?php echo esc_html($this->template_identity_label([
'provisioningType' => $provisioning_type,
'proxmoxTemplateId' => (int) ($template['vmId'] ?? 0),
'proxmoxTemplateRef' => $template_ref,
])); ?></span>
</div>
<?php if ($is_imported) : ?>
<span class="spp-badge RUNNING"><?php echo esc_html__('Imported', 'support-provisioning-portal'); ?></span>
<?php endif; ?>
</div>
<div class="spp-meta">
<span>Type<strong><?php echo esc_html($this->template_type_label($provisioning_type)); ?></strong></span>
<span>CPU<strong><?php echo esc_html((string) $template['cpuCores']); ?> cores</strong></span>
<span>Memory<strong><?php echo esc_html((string) $template['memoryMb']); ?> MB</strong></span>
<span>Disk<strong><?php echo esc_html((string) $template['diskGb']); ?> GB</strong></span>
<span>Status<strong><?php echo esc_html((string) $template['status']); ?></strong></span>
</div>
<?php if (!$is_imported) : ?>
<label>OS type
<?php $this->render_os_type_select('LINUX'); ?>
</label>
<label>Default TTL hours
<input name="spp_default_ttl_hours" type="number" min="1" max="720" value="168" required>
</label>
<label>Description
<textarea name="spp_template_description" rows="2" required><?php echo esc_textarea($provisioning_type === 'lxc' ? sprintf('Imported from Proxmox LXC template %s.', $template_ref) : sprintf('Imported from Proxmox template VMID %d.', (int) $template['vmId'])); ?></textarea>
</label>
<button class="button button-primary" type="submit"><?php echo esc_html__('Import Template', 'support-provisioning-portal'); ?></button>
<?php endif; ?>
</form>
<?php endforeach; ?>
</div>
<?php endif; ?>
<h3><?php echo esc_html__('Add QEMU Template Manually', 'support-provisioning-portal'); ?></h3>
<form class="spp-template-row" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="spp_save_template">
<input type="hidden" name="spp_template_action" value="import">
<input type="hidden" name="spp_provisioning_type" value="qemu">
<input type="hidden" name="spp_proxmox_template_ref" value="">
<?php wp_nonce_field('spp_save_template'); ?>
<div class="spp-template-fields">
<label>Name
<input name="spp_template_name" type="text" required maxlength="160">
</label>
<label>PVE template VMID
<input name="spp_proxmox_template_id" type="number" min="1" required>
</label>
<label>OS type
<?php $this->render_os_type_select('LINUX'); ?>
</label>
<label>CPU cores
<input name="spp_cpu_cores" type="number" min="1" value="2" required>
</label>
<label>Memory MB
<input name="spp_memory_mb" type="number" min="128" step="128" value="2048" required>
</label>
<label>Disk GB
<input name="spp_disk_gb" type="number" min="1" value="32" required>
</label>
<label>Default TTL hours
<input name="spp_default_ttl_hours" type="number" min="1" max="720" value="168" required>
</label>
<label class="spp-template-description">Description
<textarea name="spp_template_description" rows="2" required></textarea>
</label>
</div>
<div class="spp-template-actions">
<button class="button button-primary" type="submit"><?php echo esc_html__('Add Template', 'support-provisioning-portal'); ?></button>
</div>
</form>
</section>
<?php
}
private function render_user_access_management(): void
{
$search = isset($_GET['spp_user_search']) ? sanitize_text_field((string) wp_unslash($_GET['spp_user_search'])) : '';
$users = $this->users_for_access_table($search);
$definitions = SPP_Permissions::definitions();
?>
<section class="spp-settings spp-user-access">
<div class="spp-section-header">
<div>
<h2><?php echo esc_html__('User Rights', 'support-provisioning-portal'); ?></h2>
<p class="description"><?php echo esc_html__('SSO users appear here after their first WordPress sign-in.', 'support-provisioning-portal'); ?></p>
</div>
<form class="spp-user-search" method="get">
<input type="hidden" name="page" value="support-provisioning-portal">
<input name="spp_user_search" type="search" value="<?php echo esc_attr($search); ?>" placeholder="Search users">
<button class="button" type="submit"><?php echo esc_html__('Search', 'support-provisioning-portal'); ?></button>
</form>
</div>
<?php if (empty($this->permissions->user_ids_with_permission(SPP_Permissions::MANAGE_PERMISSIONS))) : ?>
<p class="spp-warning"><?php echo esc_html__('Assign Manage user rights to at least one user to finish permission bootstrap.', 'support-provisioning-portal'); ?></p>
<?php endif; ?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="spp_save_user_access">
<?php wp_nonce_field('spp_save_user_access'); ?>
<table class="widefat striped spp-user-access-table">
<thead>
<tr>
<th><?php echo esc_html__('User', 'support-provisioning-portal'); ?></th>
<th><?php echo esc_html__('RAM override', 'support-provisioning-portal'); ?></th>
<th><?php echo esc_html__('Rights', 'support-provisioning-portal'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($users)) : ?>
<tr>
<td colspan="3"><?php echo esc_html__('No users found.', 'support-provisioning-portal'); ?></td>
</tr>
<?php endif; ?>
<?php foreach ($users as $user) : ?>
<?php
$user_permissions = $this->permissions->for_user((int) $user->ID);
$quota = get_user_meta((int) $user->ID, 'spp_memory_quota_mb', true);
?>
<tr>
<td>
<input type="hidden" name="spp_user_ids[]" value="<?php echo esc_attr((string) $user->ID); ?>">
<strong><?php echo esc_html($user->display_name !== '' ? $user->display_name : $user->user_login); ?></strong>
<span class="spp-user-login"><?php echo esc_html($user->user_login); ?></span>
<span class="spp-user-login"><?php echo esc_html($user->user_email); ?></span>
</td>
<td>
<input
class="small-text"
name="spp_memory_quota_mb[<?php echo esc_attr((string) $user->ID); ?>]"
type="number"
min="0"
step="256"
value="<?php echo esc_attr((string) $quota); ?>"
placeholder="Default"
>
</td>
<td>
<div class="spp-permission-groups">
<?php foreach (SPP_Permissions::groups() as $group => $permissions) : ?>
<fieldset>
<legend><?php echo esc_html($group); ?></legend>
<?php foreach ($permissions as $permission) : ?>
<label>
<input
type="checkbox"
name="spp_permissions[<?php echo esc_attr((string) $user->ID); ?>][]"
value="<?php echo esc_attr($permission); ?>"
<?php checked(in_array($permission, $user_permissions, true)); ?>
>
<span><?php echo esc_html($definitions[$permission]); ?></span>
</label>
<?php endforeach; ?>
</fieldset>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php submit_button('Save User Rights'); ?>
</form>
</section>
<?php
}
public function save_settings(): void
{
if (!$this->permissions->current_user_has(SPP_Permissions::MANAGE_SETTINGS)) {
wp_die(esc_html__('You do not have permission to save these settings.', 'support-provisioning-portal'));
}
check_admin_referer('spp_save_settings');
update_option('spp_proxmox_mode', $this->posted_string('spp_proxmox_mode') === 'http' ? 'http' : 'mock');
update_option('spp_proxmox_base_url', $this->sanitize_proxmox_base_url($this->posted_string('spp_proxmox_base_url')));
update_option('spp_proxmox_token_id', sanitize_text_field($this->posted_string('spp_proxmox_token_id')));
$token_secret = sanitize_text_field($this->posted_string('spp_proxmox_token_secret'));
if ($token_secret !== '') {
update_option('spp_proxmox_token_secret', $token_secret);
}
update_option('spp_proxmox_node', sanitize_text_field($this->posted_string('spp_proxmox_node')));
update_option('spp_lxc_rootfs_storage', $this->sanitize_proxmox_identifier($this->posted_string('spp_lxc_rootfs_storage')));
update_option('spp_lxc_bridge', $this->sanitize_proxmox_identifier($this->posted_string('spp_lxc_bridge')));
update_option('spp_quota_user_memory_mb', max(0, absint($this->posted_string('spp_quota_user_memory_mb'))));
update_option('spp_quota_global_memory_mb', max(0, absint($this->posted_string('spp_quota_global_memory_mb'))));
$this->redirect_to_admin_page('settings_saved');
}
public function save_template(): void
{
if (!$this->permissions->current_user_has(SPP_Permissions::MANAGE_TEMPLATES)) {
wp_die(esc_html__('You do not have permission to save templates.', 'support-provisioning-portal'));
}
check_admin_referer('spp_save_template');
$template_id = absint($this->posted_string('spp_template_id'));
$action = sanitize_key($this->posted_string('spp_template_action'));
if ($action === 'remove') {
if ($template_id < 1) {
$this->redirect_to_admin_page('template_error');
}
$this->repository->deactivate_template($template_id, get_current_user_id());
$this->redirect_to_admin_page('template_removed');
}
$data = $this->posted_template_data();
if ($data === null) {
$this->redirect_to_admin_page('template_error');
}
if (!$this->proxmox_template_exists(
(string) $data['provisioning_type'],
(int) $data['proxmox_template_id'],
(string) $data['proxmox_template_ref']
)) {
$this->redirect_to_admin_page('template_error');
}
if ($template_id > 0) {
$this->repository->update_template($template_id, $data, get_current_user_id());
} else {
$this->repository->upsert_template($data, get_current_user_id());
}
$this->redirect_to_admin_page('template_saved');
}
public function save_user_access(): void
{
if (!$this->permissions->current_user_has(SPP_Permissions::MANAGE_PERMISSIONS)) {
wp_die(esc_html__('You do not have permission to save user rights.', 'support-provisioning-portal'));
}
check_admin_referer('spp_save_user_access');
$user_ids = $this->posted_user_ids();
$posted_permissions = isset($_POST['spp_permissions']) ? (array) wp_unslash($_POST['spp_permissions']) : [];
$posted_quotas = isset($_POST['spp_memory_quota_mb']) ? (array) wp_unslash($_POST['spp_memory_quota_mb']) : [];
if (!$this->save_keeps_permission_manager($user_ids, $posted_permissions)) {
$this->redirect_to_admin_page('manager_required');
}
foreach ($user_ids as $user_id) {
$permissions = isset($posted_permissions[$user_id]) && is_array($posted_permissions[$user_id])
? SPP_Permissions::sanitize_permissions($posted_permissions[$user_id])
: [];
$quota_raw = isset($posted_quotas[$user_id]) ? sanitize_text_field((string) $posted_quotas[$user_id]) : '';
$memory_quota_mb = $quota_raw === '' ? null : max(0, absint($quota_raw));
$this->repository->update_user_access($user_id, $permissions, $memory_quota_mb, get_current_user_id());
}
$this->redirect_to_admin_page('user_access_saved');
}
/**
* @return array<int, WP_User>
*/
private function users_for_access_table(string $search): array
{
$args = [
'fields' => 'all',
'orderby' => 'display_name',
'order' => 'ASC',
'number' => 200,
];
if ($search !== '') {
$args['search'] = '*' . $search . '*';
$args['search_columns'] = ['user_login', 'user_email', 'user_nicename', 'display_name'];
}
$users = get_users($args);
return is_array($users) ? $users : [];
}
private function render_os_type_select(string $selected): void
{
$options = [
'LINUX' => 'Linux',
'WINDOWS' => 'Windows',
'APPLIANCE' => 'Appliance',
'OTHER' => 'Other',
];
?>
<select name="spp_os_type">
<?php foreach ($options as $value => $label) : ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($selected, $value); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
<?php
}
private function posted_string(string $key): string
{
if (!isset($_POST[$key])) {
return '';
}
$value = wp_unslash($_POST[$key]);
return is_scalar($value) ? (string) $value : '';
}
private function sanitize_proxmox_base_url(string $value): string
{
$url = esc_url_raw($value);
if ($url === '') {
return '';
}
return wp_parse_url($url, PHP_URL_SCHEME) === 'https' ? $url : '';
}
private function sanitize_proxmox_identifier(string $value): string
{
$value = sanitize_text_field($value);
$value = preg_replace('/[^A-Za-z0-9_.:-]/', '', $value);
return $value === null ? '' : $value;
}
private function proxmox_template_exists(string $provisioning_type, int $vm_id, string $template_ref): bool
{
$provisioning_type = $this->normalise_template_type($provisioning_type);
try {
foreach ($this->proxmox->list_templates() as $template) {
$candidate_type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
if ($candidate_type !== $provisioning_type) {
continue;
}
if ($candidate_type === 'lxc' && (string) ($template['templateRef'] ?? '') === $template_ref) {
return true;
}
if ($candidate_type === 'qemu' && (int) ($template['vmId'] ?? 0) === $vm_id) {
return true;
}
}
} catch (Throwable) {
return false;
}
return false;
}
/**
* @return array<string, mixed>|null
*/
private function posted_template_data(): ?array
{
$name = sanitize_text_field($this->posted_string('spp_template_name'));
$provisioning_type = $this->normalise_template_type($this->posted_string('spp_provisioning_type'));
$proxmox_template_id = absint($this->posted_string('spp_proxmox_template_id'));
$proxmox_template_ref = sanitize_text_field($this->posted_string('spp_proxmox_template_ref'));
$description = sanitize_textarea_field($this->posted_string('spp_template_description'));
if ($name === '' || $description === '') {
return null;
}
if ($provisioning_type === 'qemu' && $proxmox_template_id < 1) {
return null;
}
if ($provisioning_type === 'lxc' && $proxmox_template_ref === '') {
return null;
}
return [
'template_key' => $this->template_identity_key($provisioning_type, $proxmox_template_id, $proxmox_template_ref) . '-' . sanitize_title($name),
'name' => $name,
'description' => $description,
'os_type' => $this->posted_os_type(),
'cpu_cores' => max(1, absint($this->posted_string('spp_cpu_cores'))),
'memory_mb' => max(128, absint($this->posted_string('spp_memory_mb'))),
'disk_gb' => max(1, absint($this->posted_string('spp_disk_gb'))),
'default_ttl_hours' => max(1, min(720, absint($this->posted_string('spp_default_ttl_hours')))),
'provisioning_type' => $provisioning_type,
'proxmox_template_id' => $proxmox_template_id,
'proxmox_template_ref' => $proxmox_template_ref,
'is_active' => $this->posted_string('spp_is_active') === '1',
];
}
private function posted_os_type(): string
{
$os_type = strtoupper(sanitize_key($this->posted_string('spp_os_type')));
return in_array($os_type, ['LINUX', 'WINDOWS', 'APPLIANCE', 'OTHER'], true) ? $os_type : 'OTHER';
}
/**
* @param array<string, mixed> $template
*/
private function template_identity_label(array $template): string
{
$type = $this->normalise_template_type((string) ($template['provisioningType'] ?? 'qemu'));
if ($type === 'lxc') {
return 'LXC ' . (string) ($template['proxmoxTemplateRef'] ?? $template['templateRef'] ?? '');
}
return sprintf('PVE VMID %d', (int) ($template['proxmoxTemplateId'] ?? $template['vmId'] ?? 0));
}
private function template_type_label(string $type): string
{
return $this->normalise_template_type($type) === 'lxc' ? 'LXC container' : 'QEMU VM';
}
private function template_identity_key(string $provisioning_type, int $proxmox_template_id, string $proxmox_template_ref): string
{
return $this->normalise_template_type($provisioning_type) === 'lxc'
? 'lxc:' . $proxmox_template_ref
: 'qemu:' . $proxmox_template_id;
}
private function normalise_template_type(string $type): string
{
return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
}
/**
* @return array<int, int>
*/
private function posted_user_ids(): array
{
$raw_user_ids = isset($_POST['spp_user_ids']) ? (array) wp_unslash($_POST['spp_user_ids']) : [];
$user_ids = [];
foreach ($raw_user_ids as $user_id) {
$user_id = absint($user_id);
if ($user_id > 0 && !in_array($user_id, $user_ids, true)) {
$user_ids[] = $user_id;
}
}
return $user_ids;
}
/**
* @param array<int, int> $user_ids
* @param array<mixed> $posted_permissions
*/
private function save_keeps_permission_manager(array $user_ids, array $posted_permissions): bool
{
$updated_user_ids = array_flip($user_ids);
foreach ($this->permissions->user_ids_with_permission(SPP_Permissions::MANAGE_PERMISSIONS) as $manager_id) {
if (!isset($updated_user_ids[$manager_id])) {
return true;
}
}
foreach ($user_ids as $user_id) {
$permissions = isset($posted_permissions[$user_id]) && is_array($posted_permissions[$user_id])
? SPP_Permissions::sanitize_permissions($posted_permissions[$user_id])
: [];
if (in_array(SPP_Permissions::MANAGE_PERMISSIONS, $permissions, true)) {
return true;
}
}
return false;
}
private function redirect_to_admin_page(string $notice): void
{
wp_safe_redirect(add_query_arg([
'page' => 'support-provisioning-portal',
'spp_notice' => $notice,
], admin_url('admin.php')));
exit;
}
}