Removes admin-tools page
parent
a1c585ed05
commit
0bb5fc2dc4
|
|
@ -27,7 +27,6 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Updated credentials pages and removed unused "credentials.php"
|
- Updated credentials pages and removed unused "credentials.php"
|
||||||
- Updated credentials pages and removed unused "credentials.php"
|
|
||||||
- Redesigned admin tools, themes, profile, credentials/2FA, and authentication pages
|
- Redesigned admin tools, themes, profile, credentials/2FA, and authentication pages
|
||||||
- Redesigned sidebar, main elements, menus, and overall CSS
|
- Redesigned sidebar, main elements, menus, and overall CSS
|
||||||
- Updated pagination styling
|
- Updated pagination styling
|
||||||
|
|
@ -36,6 +35,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Replaced "error_log" with "app_log" in 2FA
|
- Replaced "error_log" with "app_log" in 2FA
|
||||||
- Updated index bootstrap to use global "APP_PATH"
|
- Updated index bootstrap to use global "APP_PATH"
|
||||||
- Refactored database migration system and Admin Tools functionality
|
- Refactored database migration system and Admin Tools functionality
|
||||||
|
- Removed "admin-tools" page, all functionality is now in "admin" page
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Database migration reliability issues
|
- Database migration reliability issues
|
||||||
|
|
@ -244,8 +244,6 @@ All notable changes to this project will be documented in this file.
|
||||||
### Changed
|
### Changed
|
||||||
- Changed the layout with bootstrap CSS classes
|
- Changed the layout with bootstrap CSS classes
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0.1 - 2024-07-08
|
## 0.1 - 2024-07-08
|
||||||
|
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Admin tools controller
|
|
||||||
*
|
|
||||||
* Allows superusers to:
|
|
||||||
* - Enable/disable maintenance mode
|
|
||||||
* - Run database migrations
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Security and CSRF
|
|
||||||
require_once __DIR__ . '/../helpers/security.php';
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
|
|
||||||
// Must be logged in
|
|
||||||
if (!Session::isValidSession()) {
|
|
||||||
header('Location: ' . $app_root . '?page=login');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be superuser
|
|
||||||
$canAdmin = false;
|
|
||||||
if (isset($userId) && isset($userObject) && method_exists($userObject, 'hasRight')) {
|
|
||||||
$canAdmin = ($userId === 1) || (bool)$userObject->hasRight($userId, 'superuser');
|
|
||||||
}
|
|
||||||
if (!$canAdmin) {
|
|
||||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
|
||||||
header('Location: ' . $app_root);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get any old feedback messages
|
|
||||||
include_once '../app/helpers/feedback.php';
|
|
||||||
|
|
||||||
// Handle actions
|
|
||||||
$action = $_POST['action'] ?? '';
|
|
||||||
|
|
||||||
// AJAX: view migration file contents
|
|
||||||
if ($action === 'read_migration') {
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// CSRF check
|
|
||||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
|
||||||
$csrfToken = $_POST['csrf_token'] ?? $csrfHeader;
|
|
||||||
if (!$security->verifyCsrfToken($csrfToken)) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission check
|
|
||||||
if (!$canAdmin) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Permission denied']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate filename to avoid traversal
|
|
||||||
$filename = basename($_POST['filename'] ?? '');
|
|
||||||
if ($filename === '' || !preg_match('/^[A-Za-z0-9_\-]+\.sql$/', $filename)) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid filename']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$path = realpath($migrationsDir . '/' . $filename);
|
|
||||||
if ($path === false || strpos($path, realpath($migrationsDir)) !== 0) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'File not found']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = @file_get_contents($path);
|
|
||||||
if ($content === false) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Could not read file']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'name' => $filename, 'content' => $content]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if ($action !== '') {
|
|
||||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
Feedback::flash('SECURITY', 'CSRF_INVALID');
|
|
||||||
header('Location: ' . $app_root . '?page=admin-tools');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($action === 'maintenance_on') {
|
|
||||||
require_once __DIR__ . '/../core/Maintenance.php';
|
|
||||||
$msg = trim($_POST['maintenance_message'] ?? '');
|
|
||||||
\App\Core\Maintenance::enable($msg);
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode enabled.', true);
|
|
||||||
} elseif ($action === 'maintenance_off') {
|
|
||||||
require_once __DIR__ . '/../core/Maintenance.php';
|
|
||||||
\App\Core\Maintenance::disable();
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode disabled.', true);
|
|
||||||
} elseif ($action === 'migrate_up') {
|
|
||||||
require_once __DIR__ . '/../core/MigrationRunner.php';
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
|
||||||
$applied = $runner->applyPendingMigrations();
|
|
||||||
|
|
||||||
// Clean up any test migration files after applying
|
|
||||||
if (!empty($applied)) {
|
|
||||||
foreach ($applied as $migration) {
|
|
||||||
if (strpos($migration, '_test_migration.sql') !== false) {
|
|
||||||
$filepath = $migrationsDir . '/' . $migration;
|
|
||||||
if (file_exists($filepath)) {
|
|
||||||
unlink($filepath);
|
|
||||||
}
|
|
||||||
// Remove from database migrations table to leave no trace
|
|
||||||
$stmt = $db->getConnection()->prepare("DELETE FROM migrations WHERE migration = :migration");
|
|
||||||
$stmt->execute([':migration' => $migration]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($applied)) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'No pending migrations.', true);
|
|
||||||
} else {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Applied migrations: ' . implode(', ', $applied), true);
|
|
||||||
}
|
|
||||||
} elseif ($action === 'migrate_apply_one') {
|
|
||||||
require_once __DIR__ . '/../core/MigrationRunner.php';
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
|
||||||
$migrationName = trim($_POST['migration_name'] ?? '');
|
|
||||||
$applied = $migrationName !== '' ? $runner->applyMigrationByName($migrationName) : [];
|
|
||||||
|
|
||||||
if (empty($applied)) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'No pending migrations.', true);
|
|
||||||
$_SESSION['migration_modal_result'] = [
|
|
||||||
'name' => $migrationName ?: null,
|
|
||||||
'status' => 'info',
|
|
||||||
'message' => 'No pending migrations to apply.'
|
|
||||||
];
|
|
||||||
if (!empty($migrationName)) {
|
|
||||||
$_SESSION['migration_modal_open'] = $migrationName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Applied migration: ' . implode(', ', $applied), true);
|
|
||||||
$_SESSION['migration_modal_result'] = [
|
|
||||||
'name' => $applied[0],
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => 'Migration ' . $applied[0] . ' applied successfully.'
|
|
||||||
];
|
|
||||||
$_SESSION['migration_modal_open'] = $applied[0];
|
|
||||||
}
|
|
||||||
} elseif ($action === 'create_test_migration') {
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$timestamp = date('Ymd_His');
|
|
||||||
$filename = $timestamp . '_test_migration.sql';
|
|
||||||
$filepath = $migrationsDir . '/' . $filename;
|
|
||||||
|
|
||||||
// Create a simple test migration that adds a test setting (MariaDB compatible)
|
|
||||||
$testMigration = "-- Test migration for testing purposes\n";
|
|
||||||
$testMigration .= "-- This migration adds a test setting to settings table\n";
|
|
||||||
$testMigration .= "INSERT INTO settings (`key`, `value`, updated_at) VALUES ('test_migration_flag', '1', NOW())\n";
|
|
||||||
$testMigration .= "ON DUPLICATE KEY UPDATE `value` = '1', updated_at = NOW();\n";
|
|
||||||
|
|
||||||
if (file_put_contents($filepath, $testMigration)) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Test migration created: ' . $filename, true);
|
|
||||||
} else {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to create test migration file', false);
|
|
||||||
}
|
|
||||||
} elseif ($action === 'clear_test_migrations') {
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
|
|
||||||
// Find and remove test migration files
|
|
||||||
$testFiles = glob($migrationsDir . '/*_test_migration.sql');
|
|
||||||
$removedCount = 0;
|
|
||||||
|
|
||||||
foreach ($testFiles as $file) {
|
|
||||||
$filename = basename($file);
|
|
||||||
if (file_exists($file)) {
|
|
||||||
unlink($file);
|
|
||||||
$removedCount++;
|
|
||||||
}
|
|
||||||
// Remove from database migrations table to leave no trace
|
|
||||||
$stmt = $db->getConnection()->prepare("DELETE FROM migrations WHERE migration = :migration");
|
|
||||||
$stmt->execute([':migration' => $filename]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($removedCount > 0) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Cleared ' . $removedCount . ' test migration(s)', true);
|
|
||||||
} else {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'No test migrations to clear', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Action failed: ' . $e->getMessage(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ' . $app_root . '?page=admin-tools');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data for view
|
|
||||||
require_once __DIR__ . '/../core/Maintenance.php';
|
|
||||||
$maintenance_enabled = \App\Core\Maintenance::isEnabled();
|
|
||||||
$maintenance_message = \App\Core\Maintenance::getMessage();
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../core/MigrationRunner.php';
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$pending = [];
|
|
||||||
$applied = [];
|
|
||||||
$next_pending = null;
|
|
||||||
$migration_contents = [];
|
|
||||||
$test_migrations_exist = false;
|
|
||||||
$migration_modal_result = $_SESSION['migration_modal_result'] ?? null;
|
|
||||||
if (isset($_SESSION['migration_modal_result'])) {
|
|
||||||
unset($_SESSION['migration_modal_result']);
|
|
||||||
}
|
|
||||||
$modal_to_open = $_SESSION['migration_modal_open'] ?? null;
|
|
||||||
if (isset($_SESSION['migration_modal_open'])) {
|
|
||||||
unset($_SESSION['migration_modal_open']);
|
|
||||||
}
|
|
||||||
$migration_records = [];
|
|
||||||
try {
|
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
|
||||||
$pending = $runner->listPendingMigrations();
|
|
||||||
$applied = $runner->listAppliedMigrations();
|
|
||||||
|
|
||||||
$sortTestFirst = static function (array $items): array {
|
|
||||||
usort($items, static function ($a, $b) {
|
|
||||||
$aTest = strpos($a, '_test_migration') !== false;
|
|
||||||
$bTest = strpos($b, '_test_migration') !== false;
|
|
||||||
if ($aTest === $bTest) {
|
|
||||||
return strcmp($a, $b);
|
|
||||||
}
|
|
||||||
return $aTest ? -1 : 1;
|
|
||||||
});
|
|
||||||
return $items;
|
|
||||||
};
|
|
||||||
|
|
||||||
$pending = $sortTestFirst($pending);
|
|
||||||
$applied = $sortTestFirst($applied);
|
|
||||||
$next_pending = $pending[0] ?? null;
|
|
||||||
|
|
||||||
// Check if any test migrations exist
|
|
||||||
$test_migrations_exist = !empty(glob($migrationsDir . '/*_test_migration.sql'));
|
|
||||||
|
|
||||||
// Preload contents for billing-admin style modals
|
|
||||||
$all = array_unique(array_merge($pending, $applied));
|
|
||||||
foreach ($all as $fname) {
|
|
||||||
$path = realpath($migrationsDir . '/' . $fname);
|
|
||||||
$content = false;
|
|
||||||
if ($path && strpos($path, realpath($migrationsDir)) === 0) {
|
|
||||||
$content = @file_get_contents($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$record = $runner->getMigrationRecord($fname);
|
|
||||||
if ($record) {
|
|
||||||
$migration_records[$fname] = $record;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($content !== false && $content !== null) {
|
|
||||||
$migration_contents[$fname] = $content;
|
|
||||||
} elseif (!empty($record['content'])) {
|
|
||||||
$migration_contents[$fname] = $record['content'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
// show error in the page
|
|
||||||
$migration_error = $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSRF token
|
|
||||||
$csrf_token = $security->generateCsrfToken();
|
|
||||||
|
|
||||||
// Load the template
|
|
||||||
include __DIR__ . '/../templates/admin-tools.php';
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
<?php
|
|
||||||
/** @var bool $maintenance_enabled */
|
|
||||||
/** @var string $maintenance_message */
|
|
||||||
/** @var array $pending */
|
|
||||||
/** @var array $applied */
|
|
||||||
/** @var string $csrf_token */
|
|
||||||
?>
|
|
||||||
<!-- admin tools page -->
|
|
||||||
<section class="tm-hero">
|
|
||||||
<div class="tm-hero-card">
|
|
||||||
<div class="tm-hero-body">
|
|
||||||
<div class="tm-hero-heading">
|
|
||||||
<h1 class="tm-hero-title">Admin tools</h1>
|
|
||||||
<p class="tm-hero-subtitle">Centralized maintenance and database utilities to keep <?= htmlspecialchars($config['site_name']) ?> healthy.</p>
|
|
||||||
</div>
|
|
||||||
<div class="tm-hero-meta">
|
|
||||||
<span class="tm-hero-pill <?= $maintenance_enabled ? 'pill-danger' : 'pill-success' ?>">
|
|
||||||
<i class="fas fa-power-off"></i>
|
|
||||||
Maintenance <?= $maintenance_enabled ? 'enabled' : 'not enabled' ?>
|
|
||||||
</span>
|
|
||||||
<span class="tm-hero-pill <?= empty($pending) ? 'pill-neutral' : 'pill-danger' ?>">
|
|
||||||
<i class="fas fa-database"></i>
|
|
||||||
<?= count($pending) ?> pending migration<?= count($pending) === 1 ? '' : 's' ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-primary tm-directory-cta" href="<?= htmlspecialchars($app_root) ?>?page=dashboard">
|
|
||||||
<i class="fas fa-arrow-left"></i>
|
|
||||||
Back to dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="tm-admin">
|
|
||||||
|
|
||||||
<div class="tm-admin-grid">
|
|
||||||
<article class="tm-admin-card">
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<h2 class="tm-admin-card-title">Maintenance mode</h2>
|
|
||||||
<p class="tm-admin-card-subtitle">Let your team know when maintenance is in progress.</p>
|
|
||||||
</div>
|
|
||||||
<span class="tm-hero-pill <?= $maintenance_enabled ? 'pill-danger' : 'pill-neutral' ?>"><?= $maintenance_enabled ? 'enabled' : 'disabled' ?></span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="tm-admin-section">
|
|
||||||
<p class="tm-admin-section-title">Message</p>
|
|
||||||
<form method="post" class="tm-admin-controls">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="maintenance_on">
|
|
||||||
<input type="text" id="maintenance_message" name="maintenance_message" class="tm-admin-message-input" value="<?= htmlspecialchars($maintenance_message) ?>" placeholder="Upgrading database">
|
|
||||||
<div class="tm-admin-inline-actions">
|
|
||||||
<button type="submit" class="btn btn-warning" <?= $maintenance_enabled ? 'disabled' : '' ?>>Enable maintenance</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary" <?= $maintenance_enabled ? '' : 'disabled' ?> onclick="document.getElementById('maintenance-disable-form').submit();">Disable maintenance</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form method="post" id="maintenance-disable-form" class="d-none">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="maintenance_off">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="tm-admin-card tm-admin-card--migrations">
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<h2 class="tm-admin-card-title">Database migrations</h2>
|
|
||||||
<p class="tm-admin-card-subtitle">Review pending SQL and apply with confidence.</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<?php if (!empty($migration_error)): ?>
|
|
||||||
<div class="alert alert-danger">Error: <?= htmlspecialchars($migration_error) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="tm-admin-test-tools">
|
|
||||||
<p><strong>Test migration tools</strong></p>
|
|
||||||
<div class="tm-admin-inline-actions">
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="create_test_migration">
|
|
||||||
<button type="submit" class="btn btn-outline-primary btn-sm" <?= !empty($test_migrations_exist) ? 'disabled' : '' ?>>Create test migration</button>
|
|
||||||
</form>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="clear_test_migrations">
|
|
||||||
<button type="submit" class="btn btn-outline-secondary btn-sm" <?= empty($test_migrations_exist) ? 'disabled' : '' ?>>Clear test migrations</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tm-admin-section">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<p class="tm-admin-section-title mb-0">Pending migrations</p>
|
|
||||||
<?php if (!empty($next_pending)): ?>
|
|
||||||
<span class="badge bg-info text-dark">Next: <?= htmlspecialchars($next_pending) ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<ul class="tm-admin-list">
|
|
||||||
<?php if (empty($pending)): ?>
|
|
||||||
<li class="tm-admin-empty">No pending migrations</li>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($pending as $fname): ?>
|
|
||||||
<li>
|
|
||||||
<div class="tm-admin-list-actions">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
||||||
data-toggle="modal" data-target="#migrationModal<?= md5($fname) ?>">
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span><?= htmlspecialchars($fname) ?></span>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tm-admin-section">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<p class="tm-admin-section-title mb-0">Applied migrations</p>
|
|
||||||
<span class="badge bg-secondary"><?= count($applied) ?></span>
|
|
||||||
</div>
|
|
||||||
<ul class="tm-admin-list">
|
|
||||||
<?php if (empty($applied)): ?>
|
|
||||||
<li class="tm-admin-empty">No applied migrations yet</li>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($applied as $fname):
|
|
||||||
if (strpos($fname, '_test_migration') !== false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<li>
|
|
||||||
<div class="tm-admin-list-actions">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
||||||
data-toggle="modal" data-target="#migrationModal<?= md5($fname) ?>">
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span><?= htmlspecialchars($fname) ?></span>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="post" class="tm-confirm" data-confirm="Apply all pending migrations?">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="migrate_up">
|
|
||||||
<button type="submit" class="btn btn-danger w-100" <?= empty($pending) ? 'disabled' : '' ?>>Apply all pending</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Migration viewer modals (one per file) -->
|
|
||||||
<?php if (!empty($migration_contents)):
|
|
||||||
foreach ($migration_contents as $name => $content):
|
|
||||||
$modalId = 'migrationModal' . md5($name);
|
|
||||||
?>
|
|
||||||
<div class="modal fade" id="<?= $modalId ?>" tabindex="-1" aria-labelledby="<?= $modalId ?>Label" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="<?= $modalId ?>Label"><?= htmlspecialchars($name) ?></h5>
|
|
||||||
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
$record = $migration_records[$name] ?? null;
|
|
||||||
$appliedAtRaw = $record['applied_at'] ?? null;
|
|
||||||
$appliedAtFormatted = null;
|
|
||||||
if (!empty($appliedAtRaw)) {
|
|
||||||
$timestamp = strtotime($appliedAtRaw);
|
|
||||||
$appliedAtFormatted = $timestamp ? date('M d, Y H:i', $timestamp) : $appliedAtRaw;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="modal-body p-0">
|
|
||||||
<pre class="tm-admin-modal-code"><code style="border-radius: 0.5rem;"><?= htmlspecialchars($content) ?></code></pre>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
$isModalNext = (!empty($next_pending) && $next_pending === $name);
|
|
||||||
$modalResult = (!empty($migration_modal_result) && ($migration_modal_result['name'] ?? '') === $name) ? $migration_modal_result : null;
|
|
||||||
?>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<?php if ($isModalNext): ?>
|
|
||||||
<form method="post" class="me-auto tm-confirm" data-confirm="Apply migration <?= htmlspecialchars($name) ?>?">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="migrate_apply_one">
|
|
||||||
<input type="hidden" name="migration_name" value="<?= htmlspecialchars($name) ?>">
|
|
||||||
<button type="submit" class="btn btn-danger">Apply migration</button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($modalResult): ?>
|
|
||||||
<div class="alert alert-<?= $modalResult['status'] === 'success' ? 'success' : 'info' ?> mb-0 small">
|
|
||||||
<?= htmlspecialchars($modalResult['message']) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($appliedAtFormatted): ?>
|
|
||||||
<div class="tm-admin-modal-meta">
|
|
||||||
<span class="tm-admin-pill pill-success">
|
|
||||||
<i class="far fa-clock"></i>
|
|
||||||
Applied <?= htmlspecialchars($appliedAtFormatted) ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach;
|
|
||||||
endif; ?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
document.querySelectorAll('form.tm-confirm').forEach(function (form) {
|
|
||||||
form.addEventListener('submit', function (event) {
|
|
||||||
const message = form.getAttribute('data-confirm') || 'Are you sure?';
|
|
||||||
if (!confirm(message)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -71,9 +71,6 @@
|
||||||
<div class="dropdown-menu dropdown-menu-right modern-dropdown">
|
<div class="dropdown-menu dropdown-menu-right modern-dropdown">
|
||||||
<h6 class="dropdown-header modern-dropdown-header">settings</h6>
|
<h6 class="dropdown-header modern-dropdown-header">settings</h6>
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser')) {?>
|
<?php if ($userObject->hasRight($userId, 'superuser')) {?>
|
||||||
<a class="dropdown-item modern-dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=admin-tools">
|
|
||||||
<i class="fas fa-toolbox"></i>Admin tools
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item modern-dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=admin">
|
<a class="dropdown-item modern-dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=admin">
|
||||||
<i class="fas fa-toolbox"></i>Admin
|
<i class="fas fa-toolbox"></i>Admin
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ $allowed_urls = [
|
||||||
'graphs','latest','livejs','agents',
|
'graphs','latest','livejs','agents',
|
||||||
'profile','credentials','config','security',
|
'profile','credentials','config','security',
|
||||||
'settings','theme','theme-asset','plugin-asset',
|
'settings','theme','theme-asset','plugin-asset',
|
||||||
'admin','admin-tools','status',
|
'admin','status',
|
||||||
'help','about',
|
'help','about',
|
||||||
'login','logout',
|
'login','logout',
|
||||||
];
|
];
|
||||||
|
|
@ -196,7 +196,7 @@ try {
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
||||||
if ($runner->hasPendingMigrations()) {
|
if ($runner->hasPendingMigrations()) {
|
||||||
$pending = $runner->listPendingMigrations();
|
$pending = $runner->listPendingMigrations();
|
||||||
$msg = 'Database schema is out of date. There are pending migrations. Run "<code>php scripts/migrate.php up</code>" or use the <a href="?page=admin-tools">Admin tools</a>';
|
$msg = 'Database schema is out of date. There are pending migrations. Run "<code>php scripts/migrate.php up</code>" or use the <a href="?page=admin">Admin center</a>';
|
||||||
// Check if migration message already exists to prevent duplicates
|
// Check if migration message already exists to prevent duplicates
|
||||||
$hasMigrationMessage = false;
|
$hasMigrationMessage = false;
|
||||||
if (isset($_SESSION['flash_messages'])) {
|
if (isset($_SESSION['flash_messages'])) {
|
||||||
|
|
@ -275,7 +275,7 @@ try {
|
||||||
if (!empty($maintMsg)) {
|
if (!empty($maintMsg)) {
|
||||||
$custom .= ' <em>' . htmlspecialchars($maintMsg) . '</em>';
|
$custom .= ' <em>' . htmlspecialchars($maintMsg) . '</em>';
|
||||||
}
|
}
|
||||||
$custom .= ' Control it in <a href="' . htmlspecialchars($app_root) . '?page=admin-tools">Admin tools</a>';
|
$custom .= ' Control it in <a href="' . htmlspecialchars($app_root) . '?page=admin">Admin center</a>';
|
||||||
// Non-dismissible and small, do not sanitize to allow link and <em>
|
// Non-dismissible and small, do not sanitize to allow link and <em>
|
||||||
Feedback::flash('SYSTEM', 'MAINTENANCE_ON', $custom, false, true, false);
|
Feedback::flash('SYSTEM', 'MAINTENANCE_ON', $custom, false, true, false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue