- 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.
498 lines
16 KiB
PHP
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);
|
|
}
|
|
}
|