Add BackPRO News theme and update database schema for article tracking
- Introduced a new WordPress theme "BackPRO News" with a lightweight magazine-style design. - Added columns for tracking retry attempts and timestamps for unpublished/generated articles in the articles table. - Included remote service metadata fields in the sites table for better management. - Created log files for image replacements, installer actions, OpenAI article generation, and publishing processes. - Implemented a dashboard template for site management, including permalink settings and theme installation options.
This commit is contained in:
@@ -95,6 +95,11 @@ class FtpService
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadFile(string $remotePath, string $localPath): bool
|
||||
{
|
||||
return (bool) @ftp_get($this->connection, $localPath, $remotePath, FTP_BINARY);
|
||||
}
|
||||
|
||||
public function ensureDirectory(string $path): void
|
||||
{
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
@@ -106,6 +111,16 @@ class FtpService
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteDirectoryContents(string $remoteDir): void
|
||||
{
|
||||
$remoteDir = $this->normalizePath($remoteDir);
|
||||
$items = $this->listDirectory($remoteDir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$this->deletePathRecursive($item['path'], $item['is_dir']);
|
||||
}
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
if ($this->connection) {
|
||||
@@ -137,4 +152,96 @@ class FtpService
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{path: string, is_dir: bool}>
|
||||
*/
|
||||
private function listDirectory(string $remoteDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (function_exists('ftp_mlsd')) {
|
||||
$entries = @ftp_mlsd($this->connection, $remoteDir);
|
||||
if (is_array($entries)) {
|
||||
foreach ($entries as $entry) {
|
||||
$name = (string) ($entry['name'] ?? '');
|
||||
if ($name === '' || $name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->normalizePath($remoteDir . '/' . $name);
|
||||
$result[] = [
|
||||
'path' => $path,
|
||||
'is_dir' => (($entry['type'] ?? '') === 'dir'),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$entries = @ftp_nlist($this->connection, $remoteDir);
|
||||
if (!is_array($entries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$normalizedEntry = str_replace('\\', '/', (string) $entry);
|
||||
$name = basename($normalizedEntry);
|
||||
if ($name === '' || $name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = str_starts_with($normalizedEntry, '/')
|
||||
? $this->normalizePath($normalizedEntry)
|
||||
: $this->normalizePath($remoteDir . '/' . $normalizedEntry);
|
||||
|
||||
$result[] = [
|
||||
'path' => $path,
|
||||
'is_dir' => $this->isDirectory($path),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function deletePathRecursive(string $path, bool $isDir): void
|
||||
{
|
||||
if ($isDir) {
|
||||
foreach ($this->listDirectory($path) as $child) {
|
||||
$this->deletePathRecursive($child['path'], $child['is_dir']);
|
||||
}
|
||||
|
||||
if (!@ftp_rmdir($this->connection, $path)) {
|
||||
throw new \RuntimeException("FTP remove directory failed: {$path}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!@ftp_delete($this->connection, $path)) {
|
||||
throw new \RuntimeException("FTP delete file failed: {$path}");
|
||||
}
|
||||
}
|
||||
|
||||
private function isDirectory(string $path): bool
|
||||
{
|
||||
$current = @ftp_pwd($this->connection);
|
||||
if ($current === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@ftp_chdir($this->connection, $path)) {
|
||||
@ftp_chdir($this->connection, $current);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$path = str_replace('\\', '/', $path);
|
||||
$path = preg_replace('#/+#', '/', $path) ?? $path;
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Helpers\Logger;
|
||||
use App\Models\Article;
|
||||
use App\Models\Site;
|
||||
use App\Models\Topic;
|
||||
|
||||
class InstallerService
|
||||
{
|
||||
@@ -117,6 +119,18 @@ class InstallerService
|
||||
'wp_admin_email' => $config['admin_email'],
|
||||
]);
|
||||
|
||||
$registeredSite = Site::find($siteId);
|
||||
if (is_array($registeredSite)) {
|
||||
$wpTools = new WordPressService();
|
||||
$remoteInstall = $wpTools->ensureRemoteService($registeredSite);
|
||||
if (empty($remoteInstall['success'])) {
|
||||
Logger::warning(
|
||||
"Remote service install skipped/failed for site_id={$siteId}: " . ($remoteInstall['message'] ?? 'unknown'),
|
||||
'installer'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info("WordPress installed and site registered (ID: {$siteId})", 'installer');
|
||||
|
||||
$this->updateProgress(100, 'Instalacja zakończona pomyślnie!', 'completed');
|
||||
@@ -140,6 +154,106 @@ class InstallerService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, message: string, site_id: int|null}
|
||||
*/
|
||||
public function reinstallSite(array $site, bool $republishPublishedArticles = true, string $progressId = ''): array
|
||||
{
|
||||
set_time_limit(1200);
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
$this->progressId = $progressId;
|
||||
|
||||
$config = $this->buildConfigFromSite($site);
|
||||
$missing = $this->validateReinstallConfig($config);
|
||||
if (!empty($missing)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Brak wymaganych danych do reinstalacji: ' . implode(', ', $missing),
|
||||
'site_id' => (int) ($site['id'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$siteId = (int) ($site['id'] ?? 0);
|
||||
Logger::warning("Starting WordPress reinstall for site_id={$siteId}, url={$config['site_url']}", 'installer');
|
||||
$this->updateProgress(2, 'Przygotowanie reinstalacji WordPress...');
|
||||
|
||||
$articlesToRepublish = $republishPublishedArticles
|
||||
? Article::findPublishedBySiteForRepublish($siteId)
|
||||
: [];
|
||||
|
||||
try {
|
||||
$this->updateProgress(8, 'Pobieranie WordPress...');
|
||||
$zipPath = $this->downloadWordPress($config['language']);
|
||||
|
||||
$this->updateProgress(16, 'Rozpakowywanie archiwum...');
|
||||
$wpSourceDir = $this->extractZip($zipPath);
|
||||
|
||||
$this->updateProgress(24, 'Generowanie wp-config.php...');
|
||||
$this->generateWpConfig($wpSourceDir, $config);
|
||||
|
||||
$this->updateProgress(30, 'Czyszczenie katalogu FTP...');
|
||||
$this->clearRemoteFtpPath($config);
|
||||
|
||||
$this->updateProgress(38, 'Czyszczenie bazy danych...');
|
||||
$this->clearDatabase($config);
|
||||
|
||||
$this->updateProgress(44, 'Wgrywanie plikow WordPress...');
|
||||
$this->uploadViaFtp($wpSourceDir, $config);
|
||||
|
||||
$this->updateProgress(85, 'Uruchamianie instalacji WordPress...');
|
||||
$this->triggerInstallation($config);
|
||||
|
||||
$this->updateProgress(91, 'Tworzenie Application Password...');
|
||||
$appPassword = $this->createApplicationPassword($config);
|
||||
|
||||
Site::update($siteId, [
|
||||
'name' => $config['site_title'],
|
||||
'url' => $config['site_url'],
|
||||
'api_user' => $config['admin_user'],
|
||||
'api_token' => $appPassword,
|
||||
'wp_admin_user' => $config['admin_user'],
|
||||
'wp_admin_pass' => $config['admin_pass'],
|
||||
'wp_admin_email' => $config['admin_email'],
|
||||
'last_published_at' => null,
|
||||
]);
|
||||
|
||||
$runtimeSite = Site::find($siteId);
|
||||
if (!$runtimeSite) {
|
||||
throw new \RuntimeException('Nie udalo sie odczytac strony po aktualizacji danych API.');
|
||||
}
|
||||
|
||||
$this->updateProgress(95, 'Tworzenie kategorii i ponowna publikacja artykulow...');
|
||||
$republishStats = $this->republishArticlesAfterReinstall($runtimeSite, $articlesToRepublish);
|
||||
|
||||
$this->updateProgress(100, 'Reinstalacja zakonczona pomyslnie!', 'completed');
|
||||
$this->cleanup();
|
||||
|
||||
$message = 'Reinstalacja WordPress zakonczona. ';
|
||||
if ($republishPublishedArticles) {
|
||||
$message .= "Artykuly odtworzone: {$republishStats['published']}, bledy: {$republishStats['failed']}.";
|
||||
} else {
|
||||
$message .= 'Pominieto ponowna publikacje artykulow.';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'site_id' => $siteId,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("Reinstallation failed for site_id={$siteId}: " . $e->getMessage(), 'installer');
|
||||
$this->updateProgress(0, 'Blad: ' . $e->getMessage(), 'failed');
|
||||
$this->cleanup();
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Blad reinstalacji: ' . $e->getMessage(),
|
||||
'site_id' => $siteId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadWordPress(string $language): string
|
||||
{
|
||||
Logger::info("Downloading WordPress ({$language})", 'installer');
|
||||
@@ -303,6 +417,44 @@ PHP;
|
||||
}
|
||||
}
|
||||
|
||||
private function clearRemoteFtpPath(array $config): void
|
||||
{
|
||||
Logger::warning("Clearing FTP path {$config['ftp_path']} on {$config['ftp_host']}", 'installer');
|
||||
|
||||
$ftp = new FtpService(
|
||||
$config['ftp_host'],
|
||||
$config['ftp_user'],
|
||||
$config['ftp_pass'],
|
||||
$config['ftp_port'],
|
||||
$config['ftp_ssl']
|
||||
);
|
||||
|
||||
try {
|
||||
$ftp->connect();
|
||||
$ftp->ensureDirectory($config['ftp_path']);
|
||||
$ftp->deleteDirectoryContents($config['ftp_path']);
|
||||
} finally {
|
||||
$ftp->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private function clearDatabase(array $config): void
|
||||
{
|
||||
Logger::warning("Clearing database {$config['db_name']} on {$config['db_host']}", 'installer');
|
||||
|
||||
try {
|
||||
$this->clearDatabaseViaPdo($config);
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
Logger::warning(
|
||||
'Direct DB cleanup failed, trying remote service fallback: ' . $e->getMessage(),
|
||||
'installer'
|
||||
);
|
||||
}
|
||||
|
||||
$this->clearDatabaseViaRemoteService($config);
|
||||
}
|
||||
|
||||
private function triggerInstallation(array $config): void
|
||||
{
|
||||
Logger::info("Triggering WordPress installation at {$config['site_url']}", 'installer');
|
||||
@@ -439,4 +591,334 @@ PHP;
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
private function buildConfigFromSite(array $site): array
|
||||
{
|
||||
return [
|
||||
'ftp_host' => (string) ($site['ftp_host'] ?? ''),
|
||||
'ftp_user' => (string) ($site['ftp_user'] ?? ''),
|
||||
'ftp_pass' => (string) ($site['ftp_pass'] ?? ''),
|
||||
'ftp_path' => rtrim((string) ($site['ftp_path'] ?? ''), '/'),
|
||||
'ftp_port' => (int) ($site['ftp_port'] ?? 21),
|
||||
'ftp_ssl' => false,
|
||||
'db_host' => (string) ($site['db_host'] ?? ''),
|
||||
'db_name' => (string) ($site['db_name'] ?? ''),
|
||||
'db_user' => (string) ($site['db_user'] ?? ''),
|
||||
'db_pass' => (string) ($site['db_pass'] ?? ''),
|
||||
'db_prefix' => (string) ($site['db_prefix'] ?? 'wp_'),
|
||||
'site_url' => rtrim((string) ($site['url'] ?? ''), '/'),
|
||||
'site_title' => (string) ($site['name'] ?? 'WordPress'),
|
||||
'admin_user' => (string) ($site['wp_admin_user'] ?? $site['api_user'] ?? ''),
|
||||
'admin_pass' => (string) ($site['wp_admin_pass'] ?? ''),
|
||||
'admin_email' => (string) ($site['wp_admin_email'] ?? 'admin@example.com'),
|
||||
'language' => 'pl_PL',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function validateReinstallConfig(array $config): array
|
||||
{
|
||||
$required = [
|
||||
'ftp_host',
|
||||
'ftp_user',
|
||||
'ftp_pass',
|
||||
'ftp_path',
|
||||
'db_host',
|
||||
'db_name',
|
||||
'db_user',
|
||||
'db_pass',
|
||||
'site_url',
|
||||
'site_title',
|
||||
'admin_user',
|
||||
'admin_pass',
|
||||
'admin_email',
|
||||
];
|
||||
|
||||
$missing = [];
|
||||
foreach ($required as $key) {
|
||||
if (trim((string) ($config[$key] ?? '')) === '') {
|
||||
$missing[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
private function republishArticlesAfterReinstall(array $site, array $articles): array
|
||||
{
|
||||
$wp = new WordPressService();
|
||||
$topicCategoryMap = $this->recreateTopicCategories($site, $wp);
|
||||
|
||||
$published = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($articles as $article) {
|
||||
$topicId = (int) ($article['topic_id'] ?? 0);
|
||||
$categoryId = $topicCategoryMap[$topicId] ?? null;
|
||||
|
||||
$wpPostId = $wp->createPost(
|
||||
$site,
|
||||
(string) ($article['title'] ?? ''),
|
||||
(string) ($article['content'] ?? ''),
|
||||
$categoryId,
|
||||
null
|
||||
);
|
||||
|
||||
if ($wpPostId) {
|
||||
Article::update((int) $article['id'], [
|
||||
'wp_post_id' => (int) $wpPostId,
|
||||
'status' => 'published',
|
||||
'error_message' => null,
|
||||
]);
|
||||
$published++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$failed++;
|
||||
Logger::error(
|
||||
"Republish failed after reinstall. site_id={$site['id']}, article_id={$article['id']}",
|
||||
'installer'
|
||||
);
|
||||
}
|
||||
|
||||
return ['published' => $published, 'failed' => $failed];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int|null> topic_id => wp_category_id
|
||||
*/
|
||||
private function recreateTopicCategories(array $site, WordPressService $wp): array
|
||||
{
|
||||
$topics = Topic::findBySite((int) $site['id']);
|
||||
$map = [];
|
||||
|
||||
foreach ($topics as $topic) {
|
||||
$created = $wp->createCategory($site, (string) $topic['name'], 0);
|
||||
$wpCategoryId = isset($created['id']) ? (int) $created['id'] : null;
|
||||
|
||||
if ($wpCategoryId !== null && $wpCategoryId > 0) {
|
||||
Topic::update((int) $topic['id'], ['wp_category_id' => $wpCategoryId]);
|
||||
$map[(int) $topic['id']] = $wpCategoryId;
|
||||
} else {
|
||||
$map[(int) $topic['id']] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function clearDatabaseViaPdo(array $config): void
|
||||
{
|
||||
$host = (string) $config['db_host'];
|
||||
$port = null;
|
||||
if (strpos($host, ':') !== false) {
|
||||
[$hostOnly, $portPart] = explode(':', $host, 2);
|
||||
if (is_numeric($portPart)) {
|
||||
$host = $hostOnly;
|
||||
$port = (int) $portPart;
|
||||
}
|
||||
}
|
||||
|
||||
$dsn = "mysql:host={$host};dbname={$config['db_name']};charset=utf8mb4";
|
||||
if ($port !== null && $port > 0) {
|
||||
$dsn .= ";port={$port}";
|
||||
}
|
||||
|
||||
$pdo = new \PDO(
|
||||
$dsn,
|
||||
(string) $config['db_user'],
|
||||
(string) $config['db_pass'],
|
||||
[
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
$tables = $pdo->query("SHOW FULL TABLES")->fetchAll(\PDO::FETCH_NUM);
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||
|
||||
foreach ($tables as $row) {
|
||||
$tableName = (string) ($row[0] ?? '');
|
||||
$tableType = strtoupper((string) ($row[1] ?? 'BASE TABLE'));
|
||||
if ($tableName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($tableType === 'VIEW') {
|
||||
$pdo->exec("DROP VIEW IF EXISTS `{$tableName}`");
|
||||
} else {
|
||||
$pdo->exec("DROP TABLE IF EXISTS `{$tableName}`");
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
}
|
||||
|
||||
private function clearDatabaseViaRemoteService(array $config): void
|
||||
{
|
||||
$token = bin2hex(random_bytes(24));
|
||||
$scriptName = 'backpro-service-' . bin2hex(random_bytes(6)) . '.php';
|
||||
$serviceContent = $this->getRemoteDbServiceScriptContent($token);
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'backpro_service_');
|
||||
|
||||
if ($tmpFile === false) {
|
||||
throw new \RuntimeException('Cannot create temporary file for remote DB service.');
|
||||
}
|
||||
|
||||
$ftp = new FtpService(
|
||||
$config['ftp_host'],
|
||||
$config['ftp_user'],
|
||||
$config['ftp_pass'],
|
||||
$config['ftp_port'],
|
||||
$config['ftp_ssl']
|
||||
);
|
||||
|
||||
$remoteDir = '/' . trim((string) $config['ftp_path'], '/');
|
||||
$remoteScriptPath = rtrim($remoteDir, '/') . '/' . $scriptName;
|
||||
|
||||
try {
|
||||
file_put_contents($tmpFile, $serviceContent);
|
||||
$ftp->connect();
|
||||
$ftp->ensureDirectory($remoteDir);
|
||||
$ftp->uploadFile($tmpFile, $remoteScriptPath);
|
||||
$ftp->disconnect();
|
||||
|
||||
$endpoint = rtrim((string) $config['site_url'], '/') . '/' . $scriptName;
|
||||
|
||||
$response = $this->http->post($endpoint, [
|
||||
'form_params' => [
|
||||
'token' => $token,
|
||||
'action' => 'db_clear',
|
||||
'db_host' => (string) $config['db_host'],
|
||||
'db_name' => (string) $config['db_name'],
|
||||
'db_user' => (string) $config['db_user'],
|
||||
'db_pass' => (string) $config['db_pass'],
|
||||
],
|
||||
'timeout' => 120,
|
||||
'headers' => [
|
||||
'User-Agent' => 'BackPRO/1.0 Remote-Service',
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
if (!is_array($data) || empty($data['success'])) {
|
||||
$message = is_array($data) ? (string) ($data['message'] ?? 'unknown error') : 'invalid response';
|
||||
throw new \RuntimeException('Remote DB cleanup failed: ' . $message);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->http->post($endpoint, [
|
||||
'form_params' => [
|
||||
'token' => $token,
|
||||
'action' => 'cleanup',
|
||||
],
|
||||
'timeout' => 20,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Logger::warning('Remote service cleanup request failed: ' . $e->getMessage(), 'installer');
|
||||
}
|
||||
|
||||
Logger::info('Database cleaned through remote service endpoint.', 'installer');
|
||||
} finally {
|
||||
if (is_file($tmpFile)) {
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getRemoteDbServiceScriptContent(string $token): string
|
||||
{
|
||||
$safeToken = addslashes($token);
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (\$_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
\$expectedToken = '{$safeToken}';
|
||||
\$providedToken = (string) (\$_POST['token'] ?? '');
|
||||
|
||||
if (!hash_equals(\$expectedToken, \$providedToken)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
\$action = (string) (\$_POST['action'] ?? '');
|
||||
if (\$action === 'cleanup') {
|
||||
@unlink(__FILE__);
|
||||
echo json_encode(['success' => true, 'message' => 'service_deleted']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (\$action !== 'db_clear') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_action']);
|
||||
exit;
|
||||
}
|
||||
|
||||
\$dbHost = (string) (\$_POST['db_host'] ?? '');
|
||||
\$dbName = (string) (\$_POST['db_name'] ?? '');
|
||||
\$dbUser = (string) (\$_POST['db_user'] ?? '');
|
||||
\$dbPass = (string) (\$_POST['db_pass'] ?? '');
|
||||
|
||||
if (\$dbHost === '' || \$dbName === '' || \$dbUser === '') {
|
||||
http_response_code(422);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_db_params']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
\$host = \$dbHost;
|
||||
\$port = null;
|
||||
if (strpos(\$host, ':') !== false) {
|
||||
list(\$hostOnly, \$portPart) = explode(':', \$host, 2);
|
||||
if (is_numeric(\$portPart)) {
|
||||
\$host = \$hostOnly;
|
||||
\$port = (int) \$portPart;
|
||||
}
|
||||
}
|
||||
|
||||
\$dsn = "mysql:host={\$host};dbname={\$dbName};charset=utf8mb4";
|
||||
if (\$port !== null && \$port > 0) {
|
||||
\$dsn .= ";port={\$port}";
|
||||
}
|
||||
|
||||
\$pdo = new PDO(\$dsn, \$dbUser, \$dbPass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
\$tables = \$pdo->query("SHOW FULL TABLES")->fetchAll(PDO::FETCH_NUM);
|
||||
\$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||
|
||||
foreach (\$tables as \$row) {
|
||||
\$tableName = (string) (\$row[0] ?? '');
|
||||
\$tableType = strtoupper((string) (\$row[1] ?? 'BASE TABLE'));
|
||||
if (\$tableName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\$tableType === 'VIEW') {
|
||||
\$pdo->exec("DROP VIEW IF EXISTS `{\$tableName}`");
|
||||
} else {
|
||||
\$pdo->exec("DROP TABLE IF EXISTS `{\$tableName}`");
|
||||
}
|
||||
}
|
||||
|
||||
\$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
echo json_encode(['success' => true, 'message' => 'db_cleared']);
|
||||
} catch (Throwable \$e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => \$e->getMessage()]);
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class OpenAIService
|
||||
]);
|
||||
|
||||
$userPrompt = "Napisz artykuł na temat: {$topicName}\n";
|
||||
$userPrompt .= "Tytul ma byc samodzielny i nie moze zaczynac sie od nazwy tematu ani kategorii.\n";
|
||||
if (!empty($topicDescription)) {
|
||||
$userPrompt .= "Wytyczne: {$topicDescription}\n";
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ class PublisherService
|
||||
|
||||
public function publishNext(): array
|
||||
{
|
||||
Logger::info('Rozpoczynam automatyczną publikację', 'publish');
|
||||
Logger::info('Rozpoczynam automatyczna publikacje', 'publish');
|
||||
|
||||
$sites = Site::findDueForPublishing();
|
||||
|
||||
if (empty($sites)) {
|
||||
Logger::info('Brak stron do publikacji', 'publish');
|
||||
return ['success' => false, 'message' => 'Brak stron wymagających publikacji.'];
|
||||
return ['success' => false, 'message' => 'Brak stron wymagajacych publikacji.'];
|
||||
}
|
||||
|
||||
$site = $sites[0];
|
||||
@@ -41,19 +41,42 @@ class PublisherService
|
||||
{
|
||||
Logger::info("Publikacja dla strony: {$site['name']} (ID: {$site['id']})", 'publish');
|
||||
|
||||
// 1. Select topic
|
||||
// 1. Najpierw publikuj gotowe, nieopublikowane artykuly.
|
||||
$retryArticle = Article::findNextRetryableBySite((int) $site['id']);
|
||||
if ($retryArticle) {
|
||||
$topic = Topic::find((int) $retryArticle['topic_id']);
|
||||
if (!$topic) {
|
||||
Logger::error("Nie znaleziono tematu dla artykulu ID {$retryArticle['id']}", 'publish');
|
||||
return ['success' => false, 'message' => 'Nie znaleziono tematu dla oczekujacego artykulu.'];
|
||||
}
|
||||
|
||||
Logger::info("Ponowna proba publikacji artykulu ID {$retryArticle['id']}: {$retryArticle['title']}", 'publish');
|
||||
Article::markRetryAttempt((int) $retryArticle['id']);
|
||||
|
||||
return $this->publishPreparedArticle(
|
||||
$site,
|
||||
$topic,
|
||||
[
|
||||
'title' => (string) $retryArticle['title'],
|
||||
'content' => (string) $retryArticle['content'],
|
||||
'model' => $retryArticle['ai_model'] ?? null,
|
||||
'prompt' => $retryArticle['prompt_used'] ?? null,
|
||||
],
|
||||
(int) $retryArticle['id']
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Gdy brak zaleglych, generuj nowy artykul.
|
||||
$topic = $this->topicBalancer->getNextTopic($site['id']);
|
||||
if (!$topic) {
|
||||
Logger::error("Brak aktywnych tematów dla strony {$site['name']}", 'publish');
|
||||
return ['success' => false, 'message' => "Brak aktywnych tematów dla strony {$site['name']}."];
|
||||
Logger::error("Brak aktywnych tematow dla strony {$site['name']}", 'publish');
|
||||
return ['success' => false, 'message' => "Brak aktywnych tematow dla strony {$site['name']}."];
|
||||
}
|
||||
|
||||
Logger::info("Wybrany temat: {$topic['name']} (ID: {$topic['id']})", 'publish');
|
||||
|
||||
// 2. Get existing titles to avoid repetition
|
||||
$existingTitles = Article::getRecentTitlesByTopic($topic['id'], 20);
|
||||
$existingTitles = Article::getRecentTitlesByTopic((int) $topic['id'], 20);
|
||||
|
||||
// 3. Generate article
|
||||
$article = $this->openAI->generateArticle(
|
||||
$topic['name'],
|
||||
$topic['description'] ?? '',
|
||||
@@ -61,16 +84,22 @@ class PublisherService
|
||||
);
|
||||
|
||||
if (!$article) {
|
||||
$this->saveFailedArticle($site, $topic, 'Nie udało się wygenerować artykułu przez OpenAI.');
|
||||
return ['success' => false, 'message' => 'Błąd generowania artykułu przez AI.'];
|
||||
$this->saveFailedArticle($site, $topic, 'Nie udalo sie wygenerowac artykulu przez OpenAI.');
|
||||
return ['success' => false, 'message' => 'Blad generowania artykulu przez AI.'];
|
||||
}
|
||||
|
||||
Logger::info("Wygenerowano artykuł: {$article['title']}", 'publish');
|
||||
Logger::info("Wygenerowano artykul: {$article['title']}", 'publish');
|
||||
|
||||
// 4. Generate/fetch image
|
||||
$article['title'] = $this->normalizeArticleTitle((string) ($article['title'] ?? ''), (string) $topic['name']);
|
||||
|
||||
return $this->publishPreparedArticle($site, $topic, $article);
|
||||
}
|
||||
|
||||
private function publishPreparedArticle(array $site, array $topic, array $article, ?int $existingArticleId = null): array
|
||||
{
|
||||
$imageUrl = null;
|
||||
$mediaId = null;
|
||||
$image = $this->imageService->generate($article['title'], $topic['name']);
|
||||
$image = $this->imageService->generate((string) $article['title'], (string) $topic['name']);
|
||||
|
||||
if ($image) {
|
||||
$mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']);
|
||||
@@ -78,42 +107,59 @@ class PublisherService
|
||||
Logger::info("Upload obrazka: media_id={$mediaId}", 'publish');
|
||||
}
|
||||
} else {
|
||||
Logger::warning('Nie udało się wygenerować obrazka, publikacja bez obrazka', 'publish');
|
||||
Logger::warning('Nie udalo sie wygenerowac obrazka, publikacja bez obrazka', 'publish');
|
||||
}
|
||||
|
||||
// 5. Publish to WordPress
|
||||
$wpPostId = $this->wordpress->createPost(
|
||||
$site,
|
||||
$article['title'],
|
||||
$article['content'],
|
||||
(string) $article['title'],
|
||||
(string) $article['content'],
|
||||
$topic['wp_category_id'],
|
||||
$mediaId
|
||||
);
|
||||
|
||||
if (!$wpPostId) {
|
||||
$this->saveFailedArticle($site, $topic, 'Nie udało się opublikować posta na WordPress.', $article);
|
||||
return ['success' => false, 'message' => 'Błąd publikacji na WordPress.'];
|
||||
$this->saveFailedArticle(
|
||||
$site,
|
||||
$topic,
|
||||
'Nie udalo sie opublikowac posta na WordPress.',
|
||||
$article,
|
||||
$existingArticleId
|
||||
);
|
||||
return ['success' => false, 'message' => 'Blad publikacji na WordPress.'];
|
||||
}
|
||||
|
||||
Logger::info("Opublikowano post: wp_post_id={$wpPostId}", 'publish');
|
||||
|
||||
// 6. Save article in database
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => $article['title'],
|
||||
'content' => $article['content'],
|
||||
'wp_post_id' => $wpPostId,
|
||||
'image_url' => $imageUrl,
|
||||
'status' => 'published',
|
||||
'ai_model' => $article['model'],
|
||||
'prompt_used' => $article['prompt'],
|
||||
'published_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
if ($existingArticleId !== null) {
|
||||
Article::update($existingArticleId, [
|
||||
'title' => (string) $article['title'],
|
||||
'content' => (string) $article['content'],
|
||||
'wp_post_id' => $wpPostId,
|
||||
'image_url' => $imageUrl,
|
||||
'status' => 'published',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'error_message' => null,
|
||||
'published_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} else {
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => (string) $article['title'],
|
||||
'content' => (string) $article['content'],
|
||||
'wp_post_id' => $wpPostId,
|
||||
'image_url' => $imageUrl,
|
||||
'status' => 'published',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'published_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
// 7. Update counters
|
||||
Topic::incrementArticleCount($topic['id']);
|
||||
Site::updateLastPublished($site['id']);
|
||||
Topic::incrementArticleCount((int) $topic['id']);
|
||||
Site::updateLastPublished((int) $site['id']);
|
||||
|
||||
$message = "Opublikowano: \"{$article['title']}\" na {$site['name']}";
|
||||
Logger::info($message, 'publish');
|
||||
@@ -121,18 +167,51 @@ class PublisherService
|
||||
return ['success' => true, 'message' => $message];
|
||||
}
|
||||
|
||||
private function saveFailedArticle(array $site, array $topic, string $error, ?array $article = null): void
|
||||
private function normalizeArticleTitle(string $title, string $topicName): string
|
||||
{
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
||||
'content' => $article['content'] ?? '',
|
||||
'status' => 'failed',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'error_message' => $error,
|
||||
]);
|
||||
$title = trim($title);
|
||||
$topicName = trim($topicName);
|
||||
|
||||
if ($title === '' || $topicName === '') {
|
||||
return $title;
|
||||
}
|
||||
|
||||
// Remove "<topic name>: " / "<topic name> - " prefixes from generated titles.
|
||||
$topicPattern = preg_quote($topicName, '/');
|
||||
$normalized = preg_replace('/^' . $topicPattern . '\s*[:\-\x{2013}\x{2014}|]\s*/iu', '', $title);
|
||||
$normalized = is_string($normalized) ? trim($normalized) : $title;
|
||||
|
||||
return $normalized !== '' ? $normalized : $title;
|
||||
}
|
||||
|
||||
private function saveFailedArticle(
|
||||
array $site,
|
||||
array $topic,
|
||||
string $error,
|
||||
?array $article = null,
|
||||
?int $existingArticleId = null
|
||||
): void {
|
||||
if ($existingArticleId !== null) {
|
||||
Article::update($existingArticleId, [
|
||||
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
||||
'content' => $article['content'] ?? '',
|
||||
'status' => 'failed',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'error_message' => $error,
|
||||
]);
|
||||
} else {
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
||||
'content' => $article['content'] ?? '',
|
||||
'status' => 'failed',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'error_message' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
Logger::error("Publikacja nieudana: {$error}", 'publish');
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user