Files
shopPRO/autoload/Domain/Article/ArticleRepository.php
Jacek 394d09d3e1 security: faza 2 - safeUnlink() i escaping XSS w szablonach artykulow
- ProductRepository: dodano safeUnlink() z walidacja realpath() - zapobiega path traversal
- ArticleRepository: to samo, 4 metody usuwania plikow zaktualizowane
- templates/articles/article-full.php: htmlspecialchars() na tytule, SERVER_NAME i $url
- templates/articles/article-entry.php: htmlspecialchars() na tytule i $url (3 miejsca)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:22:32 +01:00

1183 lines
39 KiB
PHP

<?php
namespace Domain\Article;
/**
* Repository odpowiedzialny za dostep do danych artykulow
*/
class ArticleRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Pobiera artykul po ID wraz z tlumaczeniami, obrazami, plikami i powiazanymi stronami
*/
public function find(int $articleId): ?array
{
$article = $this->db->get('pp_articles', '*', ['id' => $articleId]);
if (!$article) {
return null;
}
$results = $this->db->select('pp_articles_langs', '*', ['article_id' => $articleId]);
if (is_array($results)) {
foreach ($results as $row) {
$article['languages'][$row['lang_id']] = $row;
}
}
$article['images'] = $this->db->select('pp_articles_images', '*', [
'article_id' => $articleId,
'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
]);
try {
$article['files'] = $this->db->select('pp_articles_files', '*', [
'article_id' => $articleId,
'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
]);
} catch (\Throwable $e) {
// Fallback for instances where pp_articles_files does not yet have "o" column.
$article['files'] = $this->db->select('pp_articles_files', '*', ['article_id' => $articleId]);
}
$article['pages'] = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]);
return $article;
}
/**
* Zapisuje artykul (tworzy nowy lub aktualizuje istniejacy).
* Zwraca ID artykulu.
*/
public function save(int $articleId, array $data, int $userId): int
{
if (!$articleId) {
return $this->createArticle($data, $userId);
}
return $this->updateArticle($articleId, $data, $userId);
}
private function createArticle(array $data, int $userId): int
{
$this->db->insert('pp_articles', $this->buildArticleRow($data, $userId, true));
$id = $this->db->id();
if (!$id) {
return 0;
}
$this->saveTranslations($id, $data, true);
$this->savePages($id, $data['pages'] ?? null, true);
$this->assignTempFiles($id);
$this->assignTempImages($id);
$this->applyGalleryOrderIfProvided($id, $data);
$this->applyFilesOrderIfProvided($id, $data);
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return (int)$id;
}
private function updateArticle(int $articleId, array $data, int $userId): int
{
$this->db->update('pp_articles', $this->buildArticleRow($data, $userId, false), [
'id' => $articleId
]);
$this->saveTranslations($articleId, $data, false);
$this->savePages($articleId, $data['pages'] ?? null, false);
$this->assignTempFiles($articleId);
$this->assignTempImages($articleId);
$this->applyGalleryOrderIfProvided($articleId, $data);
$this->applyFilesOrderIfProvided($articleId, $data);
$this->deleteMarkedImages($articleId);
$this->deleteMarkedFiles($articleId);
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $articleId;
}
private function buildArticleRow(array $data, int $userId, bool $isNew): array
{
$row = [
'show_title' => $this->isCheckedValue($data['show_title'] ?? null) ? 1 : 0,
'show_date_add' => $this->isCheckedValue($data['show_date_add'] ?? null) ? 1 : 0,
'show_date_modify' => $this->isCheckedValue($data['show_date_modify'] ?? null) ? 1 : 0,
'date_modify' => date('Y-m-d H:i:s'),
'modify_by' => $userId,
'layout_id' => !empty($data['layout_id']) ? (int)$data['layout_id'] : null,
'status' => $this->isCheckedValue($data['status'] ?? null) ? 1 : 0,
'repeat_entry' => $this->isCheckedValue($data['repeat_entry'] ?? null) ? 1 : 0,
'social_icons' => $this->isCheckedValue($data['social_icons'] ?? null) ? 1 : 0,
'show_table_of_contents' => $this->isCheckedValue($data['show_table_of_contents'] ?? null) ? 1 : 0,
];
if ($isNew) {
$row['date_add'] = date('Y-m-d H:i:s');
}
return $row;
}
private function buildLangRow($langId, array $data): array
{
return [
'lang_id' => $langId,
'title' => ($data['title'][$langId] ?? '') != '' ? $data['title'][$langId] : null,
'main_image' => ($data['main_image'][$langId] ?? '') != '' ? $data['main_image'][$langId] : null,
'entry' => ($data['entry'][$langId] ?? '') != '' ? $data['entry'][$langId] : null,
'text' => ($data['text'][$langId] ?? '') != '' ? $data['text'][$langId] : null,
'table_of_contents' => ($data['table_of_contents'][$langId] ?? '') != '' ? $data['table_of_contents'][$langId] : null,
'meta_title' => ($data['meta_title'][$langId] ?? '') != '' ? $data['meta_title'][$langId] : null,
'meta_description' => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$langId] : null,
'meta_keywords' => ($data['meta_keywords'][$langId] ?? '') != '' ? $data['meta_keywords'][$langId] : null,
'seo_link' => \Shared\Helpers\Helpers::seo($data['seo_link'][$langId] ?? '') != '' ? \Shared\Helpers\Helpers::seo($data['seo_link'][$langId]) : null,
'noindex' => $this->isCheckedValue($data['noindex'][$langId] ?? null) ? 1 : 0,
'copy_from' => ($data['copy_from'][$langId] ?? '') != '' ? $data['copy_from'][$langId] : null,
'block_direct_access' => $this->isCheckedValue($data['block_direct_access'][$langId] ?? null) ? 1 : 0,
];
}
private function applyGalleryOrderIfProvided(int $articleId, array $data): void
{
$order = trim((string)($data['gallery_order'] ?? ''));
if ($order === '') {
return;
}
$this->saveGalleryOrder($articleId, $order);
}
private function applyFilesOrderIfProvided(int $articleId, array $data): void
{
$order = trim((string)($data['files_order'] ?? ''));
if ($order === '') {
return;
}
$this->saveFilesOrder($articleId, $order);
}
private function saveTranslations(int $articleId, array $data, bool $isNew): void
{
$titles = $data['title'] ?? [];
foreach ($titles as $langId => $val) {
$langRow = $this->buildLangRow($langId, $data);
if ($isNew) {
$langRow['article_id'] = $articleId;
$this->db->insert('pp_articles_langs', $langRow);
} else {
$translationId = $this->db->get('pp_articles_langs', 'id', [
'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
]);
if ($translationId) {
$this->db->update('pp_articles_langs', $langRow, ['id' => $translationId]);
} else {
$langRow['article_id'] = $articleId;
$this->db->insert('pp_articles_langs', $langRow);
}
}
}
}
private function savePages(int $articleId, $pages, bool $isNew): void
{
if (!$isNew) {
$notIn = [0];
if (is_array($pages)) {
foreach ($pages as $page) {
$notIn[] = $page;
}
} elseif ($pages) {
$notIn[] = $pages;
}
$this->db->delete('pp_articles_pages', [
'AND' => ['article_id' => $articleId, 'page_id[!]' => $notIn]
]);
$existingPages = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]);
if (!is_array($pages)) {
$pages = [$pages];
}
$pages = array_diff($pages, is_array($existingPages) ? $existingPages : []);
} else {
if (!is_array($pages)) {
$pages = $pages ? [$pages] : [];
}
}
if (is_array($pages)) {
foreach ($pages as $page) {
$order = $this->maxPageOrder() + 1;
$this->db->insert('pp_articles_pages', [
'article_id' => $articleId,
'page_id' => (int)$page,
'o' => $order,
]);
}
}
}
private function assignTempFiles(int $articleId): void
{
$results = $this->db->select('pp_articles_files', '*', ['article_id' => null]);
if (!is_array($results)) {
return;
}
$created = false;
$dir = '/upload/article_files/article_' . $articleId;
foreach ($results as $row) {
$newFileName = str_replace('/upload/article_files/tmp', $dir, $row['src']);
if (file_exists('..' . $row['src'])) {
if (!is_dir('../' . $dir) && $created !== true) {
if (mkdir('../' . $dir, 0755, true)) {
$created = true;
}
}
rename('..' . $row['src'], '..' . $newFileName);
}
$this->db->update('pp_articles_files', [
'src' => $newFileName,
'article_id' => $articleId,
], ['id' => $row['id']]);
}
}
private function assignTempImages(int $articleId): void
{
$results = $this->db->select('pp_articles_images', '*', ['article_id' => null]);
if (!is_array($results)) {
return;
}
$created = false;
$dir = '/upload/article_images/article_' . $articleId;
foreach ($results as $row) {
$newFileName = str_replace('/upload/article_images/tmp', $dir, $row['src']);
if (file_exists('../' . $newFileName)) {
$ext = strrpos($newFileName, '.');
$fileNameA = substr($newFileName, 0, $ext);
$fileNameB = substr($newFileName, $ext);
$count = 1;
while (file_exists('../' . $fileNameA . '_' . $count . $fileNameB)) {
$count++;
}
$newFileName = $fileNameA . '_' . $count . $fileNameB;
}
if (file_exists('..' . $row['src'])) {
if (!is_dir('../' . $dir) && $created !== true) {
if (mkdir('../' . $dir, 0755, true)) {
$created = true;
}
}
rename('..' . $row['src'], '..' . $newFileName);
}
$this->db->update('pp_articles_images', [
'src' => $newFileName,
'article_id' => $articleId,
], ['id' => $row['id']]);
}
}
private function deleteMarkedImages(int $articleId): void
{
$results = $this->db->select('pp_articles_images', '*', [
'AND' => ['article_id' => $articleId, 'to_delete' => 1]
]);
if (is_array($results)) {
foreach ($results as $row) {
$this->safeUnlink($row['src']);
}
}
$this->db->delete('pp_articles_images', [
'AND' => ['article_id' => $articleId, 'to_delete' => 1]
]);
}
private function deleteMarkedFiles(int $articleId): void
{
$results = $this->db->select('pp_articles_files', '*', [
'AND' => ['article_id' => $articleId, 'to_delete' => 1]
]);
if (is_array($results)) {
foreach ($results as $row) {
$this->safeUnlink($row['src']);
}
}
$this->db->delete('pp_articles_files', [
'AND' => ['article_id' => $articleId, 'to_delete' => 1]
]);
}
private function maxPageOrder(): int
{
$max = $this->db->max('pp_articles_pages', 'o');
return $max ? (int)$max : 0;
}
/**
* Archiwizuje artykul (ustawia status = -1).
*/
public function archive(int $articleId): bool
{
$result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]);
if ($result) {
$this->db->delete('pp_routes', ['article_id' => $articleId]);
}
return (bool)$result;
}
/**
* Przywraca artykul z archiwum (status = 0).
*/
public function restore(int $articleId): bool
{
$result = $this->db->update('pp_articles', ['status' => 0], ['id' => $articleId]);
return (bool)$result;
}
/**
* Trwale usuwa artykul wraz z relacjami i plikami z dysku.
*/
public function deletePermanently(int $articleId): bool
{
$this->db->delete('pp_articles_pages', ['article_id' => $articleId]);
$this->db->delete('pp_articles_langs', ['article_id' => $articleId]);
$this->db->delete('pp_articles_images', ['article_id' => $articleId]);
$this->db->delete('pp_articles_files', ['article_id' => $articleId]);
$this->db->delete('pp_routes', ['article_id' => $articleId]);
$this->db->delete('pp_articles', ['id' => $articleId]);
\Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/');
\Shared\Helpers\Helpers::delete_dir('../upload/article_files/article_' . $articleId . '/');
return true;
}
/**
* Zwraca liste artykulow do panelu admin z filtrowaniem, sortowaniem i paginacja.
*
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'date_add',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
$sortColumn = trim($sortColumn);
$sortDir = strtoupper(trim($sortDir));
$allowedSortColumns = [
'title' => 'title',
'status' => 'pa.status',
'date_add' => 'pa.date_add',
'date_modify' => 'pa.date_modify',
'user' => 'user',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pa.date_add';
$sortDir = $sortDir === 'ASC' ? 'ASC' : 'DESC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['pa.status != -1'];
$params = [];
$title = trim((string)($filters['title'] ?? ''));
if (strlen($title) > 255) {
$title = substr($title, 0, 255);
}
if ($title !== '') {
$where[] = "(
SELECT title
FROM pp_articles_langs AS pal, pp_langs AS pl
WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
ORDER BY o ASC
LIMIT 1
) LIKE :title";
$params[':title'] = '%' . $title . '%';
}
if (($filters['status'] ?? '') !== '' && ($filters['status'] === '0' || $filters['status'] === '1')) {
$where[] = 'pa.status = :status';
$params[':status'] = (int)$filters['status'];
}
$this->appendDateRangeFilter($where, $params, 'pa.date_add', 'date_add_from', 'date_add_to', $filters);
$this->appendDateRangeFilter($where, $params, 'pa.date_modify', 'date_modify_from', 'date_modify_to', $filters);
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_articles AS pa
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pa.id,
pa.date_add,
pa.date_modify,
pa.status,
(
SELECT title
FROM pp_articles_langs AS pal, pp_langs AS pl
WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
ORDER BY o ASC
LIMIT 1
) AS title,
(
SELECT login
FROM pp_users AS pu
WHERE pu.id = pa.modify_by
) AS user
FROM pp_articles AS pa
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pa.id {$sortDir}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
/**
* Zwraca liste artykulow z archiwum do panelu admin z filtrowaniem, sortowaniem i paginacja.
*
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listArchivedForAdmin(
array $filters,
string $sortColumn = 'date_add',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
$sortColumn = trim($sortColumn);
$sortDir = strtoupper(trim($sortDir));
$allowedSortColumns = [
'title' => 'title',
'date_add' => 'pa.date_add',
'date_modify' => 'pa.date_modify',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pa.date_add';
$sortDir = $sortDir === 'ASC' ? 'ASC' : 'DESC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['pa.status = -1'];
$params = [];
$title = trim((string)($filters['title'] ?? ''));
if (strlen($title) > 255) {
$title = substr($title, 0, 255);
}
if ($title !== '') {
$where[] = "(
SELECT title
FROM pp_articles_langs AS pal, pp_langs AS pl
WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
ORDER BY o ASC
LIMIT 1
) LIKE :title";
$params[':title'] = '%' . $title . '%';
}
$this->appendDateRangeFilter($where, $params, 'pa.date_add', 'date_add_from', 'date_add_to', $filters);
$this->appendDateRangeFilter($where, $params, 'pa.date_modify', 'date_modify_from', 'date_modify_to', $filters);
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_articles AS pa
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pa.id,
pa.date_add,
pa.date_modify,
(
SELECT title
FROM pp_articles_langs AS pal, pp_langs AS pl
WHERE lang_id = pl.id AND article_id = pa.id AND title != ''
ORDER BY o ASC
LIMIT 1
) AS title
FROM pp_articles AS pa
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pa.id {$sortDir}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
/**
* Zapisuje kolejnosc zdjec galerii artykulu.
*/
public function saveGalleryOrder(int $articleId, string $order): bool
{
$imageIds = explode(';', $order);
if (!is_array($imageIds) || empty($imageIds)) {
return true;
}
$position = 0;
foreach ($imageIds as $imageId) {
if ($imageId === '' || $imageId === null) {
continue;
}
$this->db->update('pp_articles_images', [
'o' => $position++,
], [
'AND' => [
'article_id' => $articleId,
'id' => (int)$imageId,
],
]);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
/**
* Zapisuje kolejnosc zalacznikow artykulu.
*/
public function saveFilesOrder(int $articleId, string $order): bool
{
$fileIds = explode(';', $order);
if (!is_array($fileIds) || empty($fileIds)) {
return true;
}
$position = 0;
foreach ($fileIds as $fileId) {
if ($fileId === '' || $fileId === null) {
continue;
}
try {
$this->db->update('pp_articles_files', [
'o' => $position++,
], [
'AND' => [
'article_id' => $articleId,
'id' => (int)$fileId,
],
]);
} catch (\Throwable $e) {
// Fallback for instances where pp_articles_files does not yet have "o" column.
return true;
}
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
/**
* Zwraca mape: article_id => etykieta stron (np. " - Strona A / Strona B").
*
* @param array<int, int> $articleIds
* @return array<int, string>
*/
public function pagesSummaryForArticles(array $articleIds): array
{
$normalizedIds = [];
foreach ($articleIds as $articleId) {
$id = (int)$articleId;
if ($id > 0) {
$normalizedIds[$id] = $id;
}
}
if (empty($normalizedIds)) {
return [];
}
$placeholders = [];
$params = [];
foreach (array_values($normalizedIds) as $index => $id) {
$key = ':article_id_' . $index;
$placeholders[] = $key;
$params[$key] = $id;
}
$sql = "
SELECT
ap.article_id,
ap.page_id,
(
SELECT title
FROM pp_pages_langs AS ppl, pp_langs AS pl
WHERE ppl.lang_id = pl.id AND ppl.page_id = ap.page_id AND ppl.title != ''
ORDER BY pl.o ASC
LIMIT 1
) AS title
FROM pp_articles_pages AS ap
WHERE ap.article_id IN (" . implode(', ', $placeholders) . ")
ORDER BY ap.article_id ASC, ap.o ASC, ap.page_id ASC
";
$stmt = $this->db->query($sql, $params);
$rows = $stmt ? $stmt->fetchAll() : [];
if (!is_array($rows)) {
return [];
}
$titlesByArticle = [];
foreach ($rows as $row) {
$articleId = (int)($row['article_id'] ?? 0);
if ($articleId <= 0) {
continue;
}
$title = trim((string)($row['title'] ?? ''));
if ($title === '') {
continue;
}
$titlesByArticle[$articleId][] = $title;
}
$summary = [];
foreach (array_values($normalizedIds) as $articleId) {
if (empty($titlesByArticle[$articleId])) {
$summary[$articleId] = '';
continue;
}
$summary[$articleId] = ' - ' . implode(' / ', $titlesByArticle[$articleId]);
}
return $summary;
}
public function updateImageAlt(int $imageId, string $imageAlt): bool
{
$result = $this->db->update('pp_articles_images', [
'alt' => $imageAlt,
], [
'id' => $imageId,
]);
\Shared\Helpers\Helpers::delete_cache();
return (bool)$result;
}
public function updateFileName(int $fileId, string $fileName): bool
{
$result = $this->db->update('pp_articles_files', [
'name' => $fileName,
], [
'id' => $fileId,
]);
return (bool)$result;
}
public function markFileToDelete(int $fileId): bool
{
$result = $this->db->update('pp_articles_files', [
'to_delete' => 1,
], [
'id' => $fileId,
]);
return (bool)$result;
}
public function markImageToDelete(int $imageId): bool
{
$result = $this->db->update('pp_articles_images', [
'to_delete' => 1,
], [
'id' => $imageId,
]);
return (bool)$result;
}
private function isCheckedValue($value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_numeric($value)) {
return ((int)$value) === 1;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true);
}
return false;
}
private function appendDateRangeFilter(
array &$where,
array &$params,
string $column,
string $fromKey,
string $toKey,
array $filters
): void {
$from = trim((string)($filters[$fromKey] ?? ''));
$to = trim((string)($filters[$toKey] ?? ''));
if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
$fromParam = ':' . str_replace('.', '_', $column) . '_from';
$where[] = "{$column} >= {$fromParam}";
$params[$fromParam] = $from . ' 00:00:00';
}
if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
$toParam = ':' . str_replace('.', '_', $column) . '_to';
$where[] = "{$column} <= {$toParam}";
$params[$toParam] = $to . ' 23:59:59';
}
}
/**
* Usuwa nieprzypisane pliki artykulow (article_id = null) wraz z plikami z dysku.
*/
public function deleteNonassignedFiles(): void
{
$results = $this->db->select('pp_articles_files', '*', ['article_id' => null]);
if (is_array($results)) {
foreach ($results as $row) {
$this->safeUnlink($row['src']);
}
}
$this->db->delete('pp_articles_files', ['article_id' => null]);
}
/**
* Usuwa nieprzypisane zdjecia artykulow (article_id = null) wraz z plikami z dysku.
*/
public function deleteNonassignedImages(): void
{
$results = $this->db->select('pp_articles_images', '*', ['article_id' => null]);
if (is_array($results)) {
foreach ($results as $row) {
$this->safeUnlink($row['src']);
}
}
$this->db->delete('pp_articles_images', ['article_id' => null]);
}
/**
* Usuwa plik z dysku tylko jeśli ścieżka pozostaje wewnątrz katalogu upload/.
* Zapobiega path traversal przy danych z bazy.
*/
private function safeUnlink(string $src): void
{
$base = realpath('../upload');
if (!$base) {
return;
}
$full = realpath('../' . ltrim($src, '/'));
if ($full && strpos($full, $base . DIRECTORY_SEPARATOR) === 0 && is_file($full)) {
unlink($full);
}
}
/**
* Pobiera artykuly opublikowane w podanym zakresie dat.
*/
public function articlesByDateAdd( string $dateStart, string $dateEnd, string $langId = 'pl' ): array
{
$stmt = $this->db->query(
'SELECT id FROM pp_articles '
. 'WHERE status = 1 '
. 'AND date_add BETWEEN :date_start AND :date_end '
. 'ORDER BY date_add DESC',
[':date_start' => $dateStart, ':date_end' => $dateEnd]
);
$articles = [];
$rows = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
if ( is_array( $rows ) ) {
foreach ( $rows as $row ) {
$articles[] = $this->articleDetailsFrontend( $row['id'], $langId );
}
}
return $articles;
}
// =========================================================================
// FRONTEND METHODS (z Redis cache)
// =========================================================================
/**
* Pobiera szczegoly artykulu dla frontendu (z copy_from fallback + Redis cache).
*/
public function articleDetailsFrontend(int $articleId, string $langId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ArticleRepository::articleDetailsFrontend:{$articleId}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$article = $this->db->get('pp_articles', '*', ['id' => $articleId]);
if (!$article) {
return null;
}
$langRow = $this->db->get('pp_articles_langs', '*', [
'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
]);
if ($langRow) {
if ($langRow['copy_from']) {
$copyRow = $this->db->get('pp_articles_langs', '*', [
'AND' => ['article_id' => $articleId, 'lang_id' => $langRow['copy_from']]
]);
$article['language'] = $copyRow ? $copyRow : $langRow;
} else {
$article['language'] = $langRow;
}
}
$article['images'] = $this->db->select('pp_articles_images', '*', [
'article_id' => $articleId,
'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
]);
$article['files'] = $this->db->select('pp_articles_files', '*', [
'article_id' => $articleId
]);
$article['pages'] = $this->db->select('pp_articles_pages', 'page_id', [
'article_id' => $articleId
]);
$cacheHandler->set($cacheKey, $article);
return $article;
}
/**
* Pobiera ID artykulow ze strony z sortowaniem i paginacja (z Redis cache).
*/
public function articlesIds(int $pageId, string $langId, int $limit, int $sortType, int $from): ?array
{
$output = null;
switch ($sortType) {
case 0: $order = 'date_add ASC'; break;
case 1: $order = 'date_add DESC'; break;
case 2: $order = 'date_modify ASC'; break;
case 3: $order = 'date_modify DESC'; break;
case 4: $order = 'o ASC'; break;
case 5: $order = 'title ASC'; break;
case 6: $order = 'title DESC'; break;
default: $order = 'id ASC'; break;
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ArticleRepository::articlesIds:{$pageId}:{$langId}:{$limit}:{$sortType}:{$from}:{$order}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$stmt = $this->db->query(
'SELECT * FROM ( '
. 'SELECT '
. 'a.id, date_modify, date_add, o, '
. '( CASE '
. 'WHEN copy_from IS NULL THEN title '
. 'WHEN copy_from IS NOT NULL THEN ( '
. 'SELECT title FROM pp_articles_langs '
. 'WHERE lang_id = al.copy_from AND article_id = a.id '
. ') '
. 'END ) AS title '
. 'FROM '
. 'pp_articles_pages AS ap '
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
. 'WHERE '
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
. ') AS q1 '
. 'WHERE q1.title IS NOT NULL '
. 'ORDER BY q1.' . $order . ' '
. 'LIMIT ' . (int)$from . ',' . (int)$limit,
[':lang_id' => $langId]
);
$results = $stmt ? $stmt->fetchAll() : [];
if (is_array($results) && !empty($results)) {
foreach ($results as $row) {
$output[] = $row['id'];
}
}
$cacheHandler->set($cacheKey, $output);
return $output;
}
/**
* Zlicza artykuly na stronie (z Redis cache).
*/
public function pageArticlesCount(int $pageId, string $langId): int
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ArticleRepository::pageArticlesCount:{$pageId}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return (int)unserialize($objectData);
}
$stmt = $this->db->query(
'SELECT COUNT(0) FROM ( '
. 'SELECT '
. 'a.id, '
. '( CASE '
. 'WHEN copy_from IS NULL THEN title '
. 'WHEN copy_from IS NOT NULL THEN ( '
. 'SELECT title FROM pp_articles_langs '
. 'WHERE lang_id = al.copy_from AND article_id = a.id '
. ') '
. 'END ) AS title '
. 'FROM '
. 'pp_articles_pages AS ap '
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
. 'WHERE '
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
. ') AS q1 '
. 'WHERE q1.title IS NOT NULL',
[':lang_id' => $langId]
);
$results = $stmt ? $stmt->fetchAll() : [];
$count = isset($results[0][0]) ? (int)$results[0][0] : 0;
$cacheHandler->set($cacheKey, $count);
return $count;
}
/**
* Pobiera paginowane artykuly ze strony.
*
* @return array{articles: ?array, ls: int}
*/
public function pageArticles(array $page, string $langId, int $bs): array
{
$count = $this->pageArticlesCount((int)$page['id'], $langId);
$articlesLimit = (int)($page['articles_limit'] ?: 10);
$ls = (int)ceil($count / $articlesLimit);
if ($bs < 1) {
$bs = 1;
} elseif ($bs > $ls) {
$bs = $ls;
}
$from = $articlesLimit * ($bs - 1);
if ($from < 0) {
$from = 0;
}
return [
'articles' => $this->articlesIds((int)$page['id'], $langId, $articlesLimit, (int)($page['sort_type'] ?? 0), $from),
'ls' => $ls,
];
}
/**
* Pobiera artykuly-aktualnosci ze strony (z sortowaniem wg page_sort).
*/
public function news(int $pageId, int $limit, string $langId): ?array
{
$sort = (int)$this->db->get('pp_pages', 'sort_type', ['id' => $pageId]);
$articlesIds = $this->articlesIds($pageId, $langId, $limit, $sort, 0);
$articles = null;
if (is_array($articlesIds) && !empty($articlesIds)) {
foreach ($articlesIds as $articleId) {
$articles[] = $this->articleDetailsFrontend($articleId, $langId);
}
}
return $articles;
}
/**
* Sprawdza czy artykul ma flage noindex (z Redis cache).
*/
public function articleNoindex(int $articleId, string $langId): bool
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ArticleRepository::articleNoindex:{$articleId}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return (bool)unserialize($objectData);
}
$noindex = $this->db->get('pp_articles_langs', 'noindex', [
'AND' => ['article_id' => $articleId, 'lang_id' => $langId]
]);
$cacheHandler->set($cacheKey, $noindex);
return (bool)$noindex;
}
/**
* Pobiera najpopularniejsze artykuly ze strony (wg views DESC, z Redis cache).
*/
public function topArticles(int $pageId, int $limit, string $langId): ?array
{
return $this->fetchArticlesByPage('topArticles', $pageId, $limit, $langId, 'views DESC');
}
/**
* Pobiera najnowsze artykuly ze strony (wg date_add DESC, z Redis cache).
*/
public function newsListArticles(int $pageId, int $limit, string $langId): ?array
{
return $this->fetchArticlesByPage('newsListArticles', $pageId, $limit, $langId, 'date_add DESC');
}
/**
* Wspolna logika dla topArticles/newsListArticles (z Redis cache).
*/
private function fetchArticlesByPage(string $cachePrefix, int $pageId, int $limit, string $langId, string $orderBy): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ArticleRepository::{$cachePrefix}:{$pageId}:{$limit}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if (!$objectData) {
$stmt = $this->db->query(
'SELECT * FROM ( '
. 'SELECT '
. 'a.id, date_add, views, '
. '( CASE '
. 'WHEN copy_from IS NULL THEN title '
. 'WHEN copy_from IS NOT NULL THEN ( '
. 'SELECT title FROM pp_articles_langs '
. 'WHERE lang_id = al.copy_from AND article_id = a.id '
. ') '
. 'END ) AS title '
. 'FROM '
. 'pp_articles_pages AS ap '
. 'INNER JOIN pp_articles AS a ON a.id = ap.article_id '
. 'INNER JOIN pp_articles_langs AS al ON al.article_id = ap.article_id '
. 'WHERE '
. 'status = 1 AND page_id = ' . (int)$pageId . ' AND lang_id = :lang_id '
. ') AS q1 '
. 'WHERE q1.title IS NOT NULL '
. 'ORDER BY q1.' . $orderBy . ' '
. 'LIMIT 0, ' . (int)$limit,
[':lang_id' => $langId]
);
$articlesData = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
$cacheHandler->set($cacheKey, $articlesData);
} else {
$articlesData = unserialize($objectData);
}
$articles = null;
if (\Shared\Helpers\Helpers::is_array_fix($articlesData)) {
foreach ($articlesData as $row) {
$articles[] = $this->articleDetailsFrontend((int)$row['id'], $langId);
}
}
return $articles;
}
}