Files
backPRO/src/Controllers/SiteController.php
Jacek Pyziak b2aead1fbe feat: Integrate DataForSEO for indexed pages tracking
- Updated CRON documentation to include DataForSEO metrics synchronization.
- Enhanced SettingsController to manage DataForSEO API credentials and settings.
- Modified SiteController to handle DataForSEO domain input.
- Updated Site model to accommodate DataForSEO data handling.
- Added methods in SiteSeoMetric model for DataForSEO data retrieval and validation.
- Implemented SiteSeoSyncService to synchronize SEO metrics from both SEMSTORM and DataForSEO.
- Enhanced dashboard templates to display indexed pages data.
- Updated settings and site creation/edit templates to include DataForSEO fields.
- Created migration for adding DataForSEO related columns in the database.
- Developed DataForSeoService to fetch indexed pages count from DataForSEO API.
2026-02-21 11:41:17 +01:00

498 lines
16 KiB
PHP

<?php
namespace App\Controllers;
use App\Core\Auth;
use App\Core\Config;
use App\Core\Controller;
use App\Helpers\Logger;
use App\Helpers\Validator;
use App\Models\Article;
use App\Models\Site;
use App\Models\SiteSeoMetric;
use App\Models\Topic;
use App\Models\GlobalTopic;
use App\Services\InstallerService;
use App\Services\SiteSeoSyncService;
use App\Services\WordPressService;
class SiteController extends Controller
{
public function index(): void
{
Auth::requireLogin();
$sites = Site::findAll('name ASC');
// Attach topic count for each site
foreach ($sites as &$site) {
$site['topic_count'] = Topic::count('site_id = :sid', ['sid' => $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);
}
}