-
Notifications
You must be signed in to change notification settings - Fork 282
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resolves #5043
- Loading branch information
Showing
33 changed files
with
2,227 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Oops, something went wrong.