$site['id']]); $site['published_article_count'] = Article::count( 'site_id = :sid AND status = :status', [ 'sid' => $site['id'], 'status' => 'published', ] ); } $this->view('sites/index', ['sites' => $sites]); } public function create(): void { Auth::requireLogin(); $globalTopics = GlobalTopic::findAllGrouped(); $this->view('sites/create', ['globalTopics' => $globalTopics]); } public function store(): void { Auth::requireLogin(); $validator = new Validator(); $validator ->required('name', $this->input('name'), 'Nazwa') ->required('url', $this->input('url'), 'URL') ->url('url', $this->input('url'), 'URL') ->required('api_user', $this->input('api_user'), 'Użytkownik API') ->required('api_token', $this->input('api_token'), 'Token API'); if (!$validator->isValid()) { $this->flash('danger', $validator->getFirstError()); $this->redirect('/sites/create'); return; } $siteId = Site::create([ 'name' => $this->input('name'), 'url' => rtrim($this->input('url'), '/'), 'semstorm_domain' => $this->input('semstorm_domain') ?: null, 'dataforseo_domain' => $this->input('dataforseo_domain') ?: null, 'api_user' => $this->input('api_user'), 'api_token' => $this->input('api_token'), 'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)), 'is_active' => $this->input('is_active') ? 1 : 0, 'is_multisite' => $this->input('is_multisite') ? 1 : 0, ]); // Create topics from selected global topics $selectedTopics = $this->input('topics'); if (is_array($selectedTopics)) { foreach ($selectedTopics as $globalTopicId) { $globalTopic = GlobalTopic::find((int) $globalTopicId); if ($globalTopic) { Topic::create([ 'site_id' => $siteId, 'global_topic_id' => (int) $globalTopicId, 'name' => $globalTopic['name'], 'description' => $globalTopic['description'] ?? '', 'is_active' => 1, ]); } } } $this->flash('success', 'Strona została dodana.'); $this->redirect("/sites/{$siteId}/edit"); } public function edit(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $topics = Topic::findBySiteWithGlobal((int) $id); $globalTopics = GlobalTopic::findAllGrouped(); $assignedGlobalIds = array_values(array_unique(array_map( static fn($v) => (int) $v, array_filter(array_column($topics, 'global_topic_id')) ))); $assignedTopicNames = array_values(array_unique(array_filter(array_map( static fn($name) => mb_strtolower(trim((string) $name)), array_column($topics, 'name') )))); $this->view('sites/edit', [ 'site' => $site, 'topics' => $topics, 'globalTopics' => $globalTopics, 'assignedGlobalIds' => $assignedGlobalIds, 'assignedTopicNames' => $assignedTopicNames, ]); } public function update(string $id): void { Auth::requireLogin(); $validator = new Validator(); $validator ->required('name', $this->input('name'), 'Nazwa') ->required('url', $this->input('url'), 'URL') ->url('url', $this->input('url'), 'URL') ->required('api_user', $this->input('api_user'), 'Użytkownik API') ->required('api_token', $this->input('api_token'), 'Token API'); if (!$validator->isValid()) { $this->flash('danger', $validator->getFirstError()); $this->redirect("/sites/{$id}/edit"); return; } Site::update((int) $id, [ 'name' => $this->input('name'), 'url' => rtrim($this->input('url'), '/'), 'semstorm_domain' => $this->input('semstorm_domain') ?: null, 'dataforseo_domain' => $this->input('dataforseo_domain') ?: null, 'api_user' => $this->input('api_user'), 'api_token' => $this->input('api_token'), 'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)), 'is_active' => $this->input('is_active') ? 1 : 0, 'is_multisite' => $this->input('is_multisite') ? 1 : 0, 'ftp_host' => $this->input('ftp_host') ?: null, 'ftp_port' => $this->input('ftp_port') ? (int) $this->input('ftp_port') : null, 'ftp_user' => $this->input('ftp_user') ?: null, 'ftp_pass' => $this->input('ftp_pass') ?: null, 'ftp_path' => $this->input('ftp_path') ?: null, 'db_host' => $this->input('db_host') ?: null, 'db_name' => $this->input('db_name') ?: null, 'db_user' => $this->input('db_user') ?: null, 'db_pass' => $this->input('db_pass') ?: null, 'db_prefix' => $this->input('db_prefix') ?: null, 'wp_admin_user' => $this->input('wp_admin_user') ?: null, 'wp_admin_pass' => $this->input('wp_admin_pass') ?: null, 'wp_admin_email' => $this->input('wp_admin_email') ?: null, ]); $this->flash('success', 'Strona została zaktualizowana.'); $this->redirect('/sites'); } public function destroy(string $id): void { Auth::requireLogin(); Site::delete((int) $id); $this->flash('success', 'Strona została usunięta.'); $this->redirect('/sites'); } public function testConnection(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->json(['success' => false, 'message' => 'Strona nie znaleziona.']); return; } $wp = new WordPressService(); $result = $wp->testConnection($site); $this->json($result); } public function dashboard(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $wp = new WordPressService(); $permalinkStatus = $wp->getPermalinkSettings($site); $remoteServiceStatus = $wp->getRemoteServiceStatus($site); $this->view('sites/dashboard', [ 'site' => $site, 'permalinkStatus' => $permalinkStatus, 'remoteServiceStatus' => $remoteServiceStatus, ]); } public function seoPanel(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $seoMetrics = SiteSeoMetric::findBySite((int) $id, 12); $seoLatest = SiteSeoMetric::latestForSite((int) $id); $this->view('sites/seo', [ 'site' => $site, 'seoMetrics' => $seoMetrics, 'seoLatest' => $seoLatest, ]); } public function syncSeoMetrics(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $sync = new SiteSeoSyncService(); $result = $sync->syncSite($site, null, true); if (!empty($result['success'])) { $this->flash('success', (string) ($result['message'] ?? 'Pobrano dane SEO.')); } else { $this->flash('danger', (string) ($result['message'] ?? 'Nie udalo sie pobrac danych SEO.')); } $this->redirect("/sites/{$id}/seo"); } public function syncSeoByToken(): void { $configuredToken = (string) Config::get('SEO_TRIGGER_TOKEN', ''); $providedToken = (string) $this->input('token', ''); if ($providedToken === '') { $providedToken = (string) ($_SERVER['HTTP_X_SEO_TOKEN'] ?? ''); } if ($configuredToken === '') { Logger::warning('Token SEO trigger called, but SEO_TRIGGER_TOKEN is not configured.', 'semstorm'); $this->json(['success' => false, 'message' => 'Token trigger is disabled.'], 503); return; } if ($providedToken === '' || !hash_equals($configuredToken, $providedToken)) { $ip = (string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown'); Logger::warning("Invalid SEO token attempt from {$ip}", 'semstorm'); $this->json(['success' => false, 'message' => 'Forbidden'], 403); return; } $force = ((int) $this->input('force', 0)) === 1; $siteId = (int) $this->input('site_id', 0); $sync = new SiteSeoSyncService(); $saved = 0; $skipped = 0; $failed = 0; $details = []; if ($siteId > 0) { $site = Site::find($siteId); if (!$site) { $this->json(['success' => false, 'message' => 'Strona nie znaleziona.'], 404); return; } $result = $sync->syncSite($site, null, $force); $status = (string) ($result['status'] ?? 'error'); if ($status === 'saved') { $saved++; } elseif ($status === 'skipped') { $skipped++; } else { $failed++; } $details[] = [ 'site_id' => (int) $site['id'], 'site_name' => (string) $site['name'], 'status' => $status, 'message' => (string) ($result['message'] ?? ''), ]; } elseif (!$force) { $metricMonth = (new \DateTimeImmutable('first day of this month'))->format('Y-m-01'); $site = Site::findNextDueForSeoSync($metricMonth); if (!$site) { $this->json([ 'success' => true, 'message' => 'SEMSTORM sync: brak stron do synchronizacji w tym miesiacu.', 'saved' => 0, 'skipped' => 0, 'failed' => 0, 'force' => false, 'details' => [], ], 200); return; } $result = $sync->syncSite($site, null, false); $status = (string) ($result['status'] ?? 'error'); if ($status === 'saved') { $saved++; } elseif ($status === 'skipped') { $skipped++; } else { $failed++; } $details[] = [ 'site_id' => (int) $site['id'], 'site_name' => (string) $site['name'], 'status' => $status, 'message' => (string) ($result['message'] ?? ''), ]; } else { $sites = Site::findActive(); foreach ($sites as $site) { $result = $sync->syncSite($site, null, $force); $status = (string) ($result['status'] ?? 'error'); if ($status === 'saved') { $saved++; } elseif ($status === 'skipped') { $skipped++; } else { $failed++; } $details[] = [ 'site_id' => (int) $site['id'], 'site_name' => (string) $site['name'], 'status' => $status, 'message' => (string) ($result['message'] ?? ''), ]; } } $this->json([ 'success' => $failed === 0, 'message' => "SEMSTORM sync: saved={$saved}, skipped={$skipped}, failed={$failed}", 'saved' => $saved, 'skipped' => $skipped, 'failed' => $failed, 'force' => $force, 'details' => $details, ], 200); } public function enablePrettyPermalinks(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $wp = new WordPressService(); $result = $wp->enablePrettyPermalinks($site); if (!empty($result['success'])) { $this->flash('success', (string) ($result['message'] ?? 'Zaktualizowano strukture linkow permanentnych.')); } else { $this->flash('danger', (string) ($result['message'] ?? 'Nie udalo sie zaktualizowac linkow permanentnych.')); } $this->redirect("/sites/{$id}/dashboard"); } public function updateRemoteService(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $wp = new WordPressService(); $result = $wp->installBackproRemoteService($site); $status = $wp->getRemoteServiceStatus($site); if (!empty($result['success'])) { $this->flash( 'success', 'Zaktualizowano plik serwisowy BackPRO. Lokalna: ' . ($status['local_version'] ?? '-') . ', na serwerze: ' . ($status['remote_version'] ?? '-') ); } else { $this->flash( 'danger', (string) ($result['message'] ?? 'Nie udalo sie zaktualizowac pliku serwisowego.') ); } $this->redirect("/sites/{$id}/dashboard"); } public function installBackproNewsTheme(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->flash('danger', 'Strona nie znaleziona.'); $this->redirect('/sites'); return; } $wp = new WordPressService(); $result = $wp->installBackproNewsTheme($site); if (!empty($result['success'])) { $this->flash('success', (string) ($result['message'] ?? 'Zainstalowano motyw BackPRO News.')); } else { $this->flash('danger', (string) ($result['message'] ?? 'Nie udalo sie zainstalowac motywu.')); } $this->redirect("/sites/{$id}/dashboard"); } public function reinstallWordPress(string $id): void { Auth::requireLogin(); $site = Site::find((int) $id); if (!$site) { $this->json(['success' => false, 'message' => 'Strona nie znaleziona.'], 404); return; } $progressId = (string) $this->input('progress_id', ''); if ($progressId === '' || !preg_match('/^[a-zA-Z0-9]{10,30}$/', $progressId)) { $this->json(['success' => false, 'message' => 'Nieprawidlowy identyfikator postepu.'], 422); return; } $republish = (bool) ((int) $this->input('republish_articles', 1)); $installer = new InstallerService(); $result = $installer->reinstallSite($site, $republish, $progressId); InstallerService::cleanupProgress($progressId); $this->json($result, !empty($result['success']) ? 200 : 500); } }