['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); } }