216 lines
8.2 KiB
PHP
216 lines
8.2 KiB
PHP
<?php
|
|
namespace Grav\Plugin;
|
|
|
|
use Grav\Common\Plugin;
|
|
use Grav\Plugin\MigrateGrav\HtaccessSecurity;
|
|
use Grav\Plugin\MigrateGrav\Kickoff;
|
|
use RocketTheme\Toolbox\Event\Event;
|
|
use RuntimeException;
|
|
|
|
class MigrateGravPlugin extends Plugin
|
|
{
|
|
public static function getSubscribedEvents(): array
|
|
{
|
|
return [
|
|
'onPluginsInitialized' => ['onPluginsInitialized', 0],
|
|
];
|
|
}
|
|
|
|
public function onPluginsInitialized(): void
|
|
{
|
|
if (!$this->isAdmin()) {
|
|
return;
|
|
}
|
|
|
|
$this->enable([
|
|
'onAdminMenu' => ['onAdminMenu', 0],
|
|
'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', 0],
|
|
'onAdminTaskExecute' => ['onAdminTaskExecute', 0],
|
|
'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
|
|
]);
|
|
}
|
|
|
|
public function onAdminMenu(): void
|
|
{
|
|
$this->grav['twig']->plugins_hooked_nav['Migrate to Grav 2.0'] = [
|
|
'route' => 'migrate-grav',
|
|
'icon' => 'fa-rocket',
|
|
];
|
|
}
|
|
|
|
public function onAdminTwigTemplatePaths(Event $event): void
|
|
{
|
|
$paths = $event['paths'];
|
|
$paths[] = __DIR__ . '/admin/templates';
|
|
$event['paths'] = $paths;
|
|
}
|
|
|
|
public function onAdminTaskExecute(Event $event): void
|
|
{
|
|
$task = $event['method'] ?? null;
|
|
|
|
$controller = $event['controller'] ?? null;
|
|
$authorized = !$controller
|
|
|| !method_exists($controller, 'isAuthorizedFunction')
|
|
|| $controller->isAuthorizedFunction('admin.super');
|
|
|
|
if ($task === 'taskMigrateGravInit') {
|
|
if (!$authorized) {
|
|
$this->grav['admin']->setMessage('Super admin required to start migration.', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
$payload = $this->runKickoff('admin');
|
|
} catch (RuntimeException $e) {
|
|
$this->grav['admin']->setMessage('Migration kickoff failed: ' . $e->getMessage(), 'error');
|
|
return;
|
|
}
|
|
$this->grav->redirect($payload['wizard_url'], 302);
|
|
return;
|
|
}
|
|
|
|
if ($task === 'taskMigrateGravSecureHtaccess') {
|
|
if (!$authorized) {
|
|
$this->grav['admin']->setMessage('Super admin required to secure the webserver config.', 'error');
|
|
return;
|
|
}
|
|
$result = $this->newHtaccessSecurity()->applyFix();
|
|
if ($result['errors']) {
|
|
$this->grav['admin']->setMessage(
|
|
'Could not fully secure the user/ folders: ' . implode('; ', $result['errors']),
|
|
'error'
|
|
);
|
|
} else {
|
|
$done = [];
|
|
if ($result['patched']) {
|
|
$done[] = 'added the folder block to .htaccess';
|
|
}
|
|
if ($result['created']) {
|
|
$done[] = 'created ' . implode(', ', $result['created']);
|
|
}
|
|
$this->grav['admin']->setMessage(
|
|
$done ? 'Secured the sensitive user/ folders: ' . implode('; ', $done) . '.'
|
|
: 'The sensitive user/ folders were already protected.',
|
|
'info'
|
|
);
|
|
}
|
|
$this->grav->redirect($this->grav['admin']->getAdminRoute('/migrate-grav'), 302);
|
|
return;
|
|
}
|
|
|
|
if ($task === 'taskMigrateGravReset' || $task === 'taskMigrateGravRestart') {
|
|
$isRestart = $task === 'taskMigrateGravRestart';
|
|
$verb = $isRestart ? 'restart wizard' : 'reset migration';
|
|
if (!$authorized) {
|
|
$this->grav['admin']->setMessage("Super admin required to {$verb}.", 'error');
|
|
return;
|
|
}
|
|
$result = $this->runReset($isRestart ? 'restart' : 'full');
|
|
if ($result['errors']) {
|
|
$label = $isRestart ? 'Restart' : 'Reset';
|
|
$this->grav['admin']->setMessage("{$label} incomplete: " . implode('; ', $result['errors']), 'error');
|
|
} else {
|
|
if (!$result['removed']) {
|
|
$msg = $isRestart ? 'Nothing to restart — no migration is staged.' : 'Nothing to reset.';
|
|
} else {
|
|
$msg = ($isRestart ? 'Wizard restarted. ' : 'Migration reset. ')
|
|
. 'Removed: ' . implode(', ', $result['removed']);
|
|
}
|
|
$this->grav['admin']->setMessage($msg, 'info');
|
|
}
|
|
|
|
// Restart preserves .migrating, so send the user back into the
|
|
// wizard at the staged step. Full reset has nothing to resume —
|
|
// return to the migrate-grav admin page.
|
|
if ($isRestart && !$result['errors']) {
|
|
$state = $this->newKickoff()->readFlag();
|
|
if (!empty($state['wizard_url'])) {
|
|
$this->grav->redirect($state['wizard_url'], 302);
|
|
return;
|
|
}
|
|
}
|
|
// Pass the Route object directly — Grav::redirect() handles
|
|
// Route instances via toString(true), which already includes the
|
|
// install base and admin route (no doubling, no manual stitching).
|
|
$this->grav->redirect($this->grav['admin']->getAdminRoute('/migrate-grav'), 302);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shared kickoff entry point used by both admin and CLI.
|
|
*/
|
|
public function runKickoff(string $trigger, ?string $adminUser = null): array
|
|
{
|
|
return $this->newKickoff()->run([
|
|
'grav_version' => GRAV_VERSION,
|
|
'admin_user' => $adminUser,
|
|
'trigger' => $trigger,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Shared reset entry point used by admin and CLI.
|
|
*
|
|
* @param string $mode 'full' nukes everything; 'restart' keeps the staged
|
|
* zip + flag and rewinds the wizard to step='staged'.
|
|
*/
|
|
public function runReset(string $mode = 'full'): array
|
|
{
|
|
return $this->newKickoff()->reset($mode);
|
|
}
|
|
|
|
/**
|
|
* Expose the current .migrating state to admin twig templates, so the
|
|
* migrate-grav page can switch between "Start" and "Continue/Reset" UI
|
|
* without round-tripping through an AJAX call.
|
|
*/
|
|
public function onTwigSiteVariables(): void
|
|
{
|
|
// Only attach the flag state on the migrate-grav admin page. Checking
|
|
// the request URI is more reliable than poking admin internals.
|
|
$path = (string) $this->grav['uri']->path();
|
|
if (!str_ends_with(rtrim($path, '/'), '/migrate-grav')) {
|
|
return;
|
|
}
|
|
|
|
$state = $this->newKickoff()->readFlag();
|
|
$this->grav['twig']->twig_vars['migrate_grav_state'] = $state;
|
|
|
|
$security = $this->newHtaccessSecurity();
|
|
$status = $security->status();
|
|
$status['snippet'] = $status['protected'] ? '' : $security->manualSnippet();
|
|
$this->grav['twig']->twig_vars['migrate_grav_security'] = $status;
|
|
}
|
|
|
|
private function newHtaccessSecurity(): HtaccessSecurity
|
|
{
|
|
require_once __DIR__ . '/classes/HtaccessSecurity.php';
|
|
|
|
$webroot = defined('GRAV_WEBROOT') ? GRAV_WEBROOT : GRAV_ROOT;
|
|
|
|
return new HtaccessSecurity($webroot);
|
|
}
|
|
|
|
private function newKickoff(): Kickoff
|
|
{
|
|
require_once __DIR__ . '/classes/Kickoff.php';
|
|
|
|
$config = (array) $this->config->get('plugins.migrate-grav', []);
|
|
$webroot = defined('GRAV_WEBROOT') ? GRAV_WEBROOT : GRAV_ROOT;
|
|
|
|
// Forward the site's GPM channel so Kickoff can match the release
|
|
// channel the rest of the admin uses (?testing vs stable).
|
|
$config['gpm_channel'] = (string) $this->grav['config']->get('system.gpm.releases', 'stable');
|
|
|
|
// Forward Grav's proxy config so both the Kickoff's own zip download
|
|
// AND the standalone wizard (via the .migrating flag) honor it.
|
|
// Sites behind a corporate proxy were silently breaking on every
|
|
// outbound call (GPM catalog, GitHub release lookups, the 2.0 zip).
|
|
$config['proxy_url'] = (string) $this->grav['config']->get('system.http.proxy_url', '');
|
|
$config['proxy_cert_path'] = (string) $this->grav['config']->get('system.http.proxy_cert_path', '');
|
|
|
|
return new Kickoff($webroot, $config);
|
|
}
|
|
}
|