summaryrefslogtreecommitdiffstats
path: root/application/controllers/MigrationsController.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--application/controllers/MigrationsController.php249
1 files changed, 249 insertions, 0 deletions
diff --git a/application/controllers/MigrationsController.php b/application/controllers/MigrationsController.php
new file mode 100644
index 0000000..5229f06
--- /dev/null
+++ b/application/controllers/MigrationsController.php
@@ -0,0 +1,249 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\MigrationManager;
+use Icinga\Common\Database;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Forms\MigrationForm;
+use Icinga\Web\Notification;
+use Icinga\Web\Widget\ItemList\MigrationList;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\SubmitButtonElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Widget\ActionLink;
+
+class MigrationsController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ Icinga::app()->getModuleManager()->loadModule('setup');
+ }
+
+ public function indexAction(): void
+ {
+ $mm = MigrationManager::instance();
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->addTitleTab($this->translate('Migrations'));
+
+ $canApply = $this->hasPermission('application/migrations');
+ if (! $canApply) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ }
+
+ $migrateListForm = new MigrationForm();
+ $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form'));
+ $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());
+
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [
+ 'form' => $migrateListForm->getAttribute('id')->getValue(),
+ 'label' => $this->translate('Migrate All'),
+ 'title' => $this->translate('Migrate all pending migrations')
+ ]);
+
+ // Is the first button, so will be cloned and that the visible
+ // button is outside the form doesn't matter for Web's JS
+ $migrateListForm->registerElement($migrateAllButton);
+
+ // Make sure it looks familiar, even if not inside a form
+ $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls'])));
+
+ $this->controls->getAttributes()->add('class', 'default-layout');
+ $this->addControl($migrateAllButton);
+ }
+
+ $this->handleFormatRequest($mm->toArray());
+
+ $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
+ $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
+ $frameworkListControl->addHtml($frameworkList);
+
+ $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
+ $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
+ $moduleListControl->addHtml($moduleList);
+
+ $migrateListForm->addHtml($frameworkListControl, $moduleListControl);
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $frameworkList->ensureAssembled();
+ $moduleList->ensureAssembled();
+
+ $this->handleMigrateRequest($migrateListForm);
+ }
+
+ $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrations->addHtml($migrateListForm);
+
+ $this->addContent($migrations);
+ }
+
+ public function hintAction(): void
+ {
+ // The forwarded request doesn't modify the original server query string, but adds the migration param to the
+ // request param instead. So, there is no way to access the migration param other than via the request instance.
+ /** @var ?string $module */
+ $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM);
+ if ($module === null) {
+ throw new MissingParameterException(
+ $this->translate('Required parameter \'%s\' missing'),
+ DbMigrationHook::MIGRATION_PARAM
+ );
+ }
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($module)) {
+ $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
+ }
+
+ $migration = $mm->getMigration($module);
+ $this->addTitleTab($this->translate('Error'));
+ $this->addContent(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'pending-migrations-hint']),
+ new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
+ new HtmlElement(
+ 'p',
+ null,
+ Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
+ ),
+ new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
+ new ActionLink($this->translate('View pending Migrations'), 'migrations')
+ )
+ );
+ }
+
+ public function migrationAction(): void
+ {
+ /** @var string $name */
+ $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM);
+
+ $this->addTitleTab($this->translate('Migration'));
+ $this->getTabs()->disableLegacyExtensions();
+ $this->controls->getAttributes()->add('class', 'default-layout');
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($name)) {
+ $migrations = [];
+ } else {
+ $hook = $mm->getMigration($name);
+ $migrations = array_reverse($hook->getMigrations());
+ if (! $this->hasPermission('application/migrations')) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ } else {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-controls']),
+ new HtmlElement('span', null, Text::create($hook->getName()))
+ )
+ );
+ }
+ }
+
+ $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
+ $this->addContent($migrationWidget);
+ }
+
+ public function handleMigrateRequest(MigrationForm $form): void
+ {
+ $this->assertPermission('application/migrations');
+
+ $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
+ $mm = MigrationManager::instance();
+
+ /** @var array<string, string> $elevatedPrivileges */
+ $elevatedPrivileges = $form->getValue('database_setup');
+ if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
+ $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
+ }
+
+ $pressedButton = $form->getPressedSubmitElement();
+ if ($pressedButton) {
+ $name = substr($pressedButton->getName(), 8);
+ switch ($name) {
+ case DbMigrationHook::ALL_MIGRATIONS:
+ if ($mm->applyAll($elevatedPrivileges)) {
+ Notification::success($this->translate('Applied all migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate(
+ 'Applied migrations successfully. Though, one or more migration hooks'
+ . ' failed to run. See logs for details'
+ )
+ );
+ }
+ break;
+ default:
+ $migration = $mm->getMigration($name);
+ if ($mm->apply($migration, $elevatedPrivileges)) {
+ Notification::success($this->translate('Applied pending migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate('Failed to apply pending migration(s). See logs for details')
+ );
+ }
+ }
+ }
+
+ $this->sendExtraUpdates(['#col2' => '__CLOSE__']);
+
+ $this->redirectNow('migrations');
+ })->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * Handle exports
+ *
+ * @param array<string, mixed> $data
+ */
+ protected function handleFormatRequest(array $data): void
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($data)
+ ->sendResponse();
+ }
+}