Files
kupshop/bundles/KupShop/FeedsBundle/Controller/FeedController.php
2025-08-02 16:30:27 +02:00

254 lines
10 KiB
PHP

<?php
namespace KupShop\FeedsBundle\Controller;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\FeedsBundle\Feed\IFeed;
use KupShop\FeedsBundle\FeedLocator;
use KupShop\FeedsBundle\FeedsBundle;
use KupShop\FeedsBundle\Utils\FeedRenderer;
use KupShop\FeedsBundle\Utils\FeedUtils;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\CachingStreamedResponse;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class FeedController extends AbstractController
{
use \DatabaseCommunication;
public function __construct(
private FeedLocator $feedLocator,
private LanguageContext $languageContext,
private CurrencyContext $currencyContext,
private CountryContext $countryContext,
private DomainContext $domainContext,
private FeedUtils $feedUtils,
private FeedRenderer $feedRenderer,
private LoggerInterface $logger,
private RequestStack $requestStack,
) {
}
/**
* @Route("/feed/{feedID}/{hash}", requirements={"feedID":"[0-9]+", "hash":"[0-9a-zA-Z_-]+"})
* @Route("/feed/{feedID}/{hash}.{format}", requirements={"feedID":"[0-9]+", "hash":".*", "format":"xml|json|csv|xlsx"})
*
* @return Response
*
* @throws \KupShop\FeedsBundle\Exceptions\UnknownFeedTypeException
*/
public function exportAction(SentryLogger $sentryLogger, Request $request)
{
$feedID = $request->get('feedID');
$hash = $request->get('hash');
$limit = $request->get('limit');
// output feed as a file (simple presence of this parameter means yes - might be empty)
$file = $request->get('file');
$format = $request->get('format', 'xml');
$pretty = $request->get('pretty');
$date_from = $request->get('date_from');
$date_to = $request->get('date_to');
$updateDownload = in_array($request->getRealMethod(), ['GET', 'POST']);
$feedRow = sqlQueryBuilder()->select('*')->from('feeds')
->where('id = :id')->setParameter('id', (int) $feedID)
->execute()->fetch();
if ($feedRow && $feedRow['hash'] === $hash) {
if (!$feedRow['active'] && !getAdminUser()) {
throw new NotFoundHttpException('Feed not active');
}
$feedRow['data'] = json_decode($feedRow['data'] ?? '{}', true);
$feedRow['pretty'] = (($feedRow['data']['format'] ?? 'xml') == 'xml_pretty') || $pretty || getAdminUser();
increaseMemoryLimit(1500);
ini_set('max_execution_time', 600);
ignore_user_abort(true);
// clean & disable output buffering
while (ob_get_level()) {
ob_end_clean();
}
return $this->outputFeed(
$this->feedLocator->getServiceByType($feedRow['type']),
$feedRow,
$limit,
isset($file),
(bool) getVal('skip_cache', null, false),
$format,
$updateDownload,
$date_from,
$date_to
);
} else {
$data = ['Feed ID' => $feedID, 'Hash' => $hash, 'IP' => $_SERVER['REMOTE_ADDR']];
if ($feedRow) {
$data['Type'] = $feedRow['type'];
$data['Name'] = $feedRow['name'];
$data['Active'] = $feedRow['active'];
$data['Language'] = $feedRow['id_language'] ?? 'independent';
$data['Currency'] = $feedRow['id_currency'] ?? 'independent';
}
throw new NotFoundHttpException('Feed not found');
}
}
public function outputFeed(
IFeed $feed,
array $feedRow,
?int $limit = null,
bool $file = false,
bool $skipCache = false,
$format = 'xml',
$updateDownload = true,
$date_from = null,
$date_to = null,
): Response {
if ($feed::getType() == 'deprecated') {
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_IMPORT,
"Stáhl se již ukončený (nekonfigurovatelný) feed '{$feedRow['name']}' (ID = {$feedRow['id']}).
Je nutné předělat feed na konfigurovatelný, stávající feed už se neaktualizuje.",
tags: [FeedsBundle::LOG_TAG_FEED]);
}
// use mocked session storage for feeds to avoid errors "Headers already sent by ..."
$this->requestStack->getCurrentRequest()?->setSession(
new Session(new MockArraySessionStorage())
);
$feedName = ['feed', $feedRow['id']];
$this->feedUtils->prepareContexts($feedRow);
$feedName[] = $this->languageContext->getActiveId();
$feedName[] = $this->currencyContext->getActiveId();
$feedName[] = $this->countryContext->getActiveId();
$feedName[] = $this->domainContext->getActiveId();
$feedName = implode('_', $feedName).'.'.$format;
// dates will differ between requests so we cant use cache
if ($date_from || $date_to) {
$skipCache = true;
}
// update feed info
if (!getAdminUser() && !$skipCache && $updateDownload) {
$this->updateSQL('feeds', ['last_download' => (new \DateTime())->format('Y-m-d H:i:s')], ['id' => $feedRow['id']]);
}
$response = new CachingStreamedResponse(function () use ($feed, $feedRow, $feedName, $limit, $format, $skipCache, $updateDownload) {
$uid = '';
if (is_null($limit)) {
$uid = uniqid('', true);
$this->logger->notice('Feed started generating ['.$feedRow['id'].'] "'.$feedName.'"', [
'uid' => $uid,
'id' => $feedRow['id'],
'cacheName' => $feedName,
'name' => $feedRow['name'],
'isAdmin' => getAdminUser() ? 'true' : 'false',
'skipCache' => $skipCache ? 'true' : 'false',
]);
}
$timeStart = getScriptTime();
$this->feedRenderer->render($feed, $feedRow, $limit, $format);
// update feed info
$timestamp = (new \DateTime())->format('Y-m-d H:i:s');
$updateData = [];
// if not cached and without limit
if (is_null($limit)) {
$updateData['regenerated'] = $timestamp;
}
if (!getAdminUser() && !$skipCache && $updateDownload) {
$updateData['last_download'] = $timestamp;
}
if (count($updateData)) {
$this->updateSQL('feeds', $updateData, ['id' => $feedRow['id']]);
}
if (is_null($limit)) {
$this->logger->notice('Feed finished ['.$feedRow['id'].'] "'.$feedName.'"', [
'uid' => $uid,
'id' => $feedRow['id'],
'cacheName' => $feedName,
'name' => $feedRow['name'],
'isAdmin' => getAdminUser() ? 'true' : 'false',
'skipCache' => $skipCache ? 'true' : 'false',
'format' => $format,
'duration' => getScriptTime() - $timeStart,
'peakMemory' => memory_get_peak_usage(true), // in bytes
]);
}
});
if ($limit) {
$response->setSkipCache(1);
$response->setIsLimited(true);
}
if ($skipCache) {
$response->setSkipCache(1);
}
if ($feedRow['expensive'] ?? false) {
$response->setExpensive();
} elseif (getAdminUser()) {
// skip cache for admins with non-expensive feeds only
$response->setSkipCache(1);
}
$response->setCacheName($feedName);
if (isset($feedRow['data']['ttl']) && is_numeric($feedRow['data']['ttl'])) {
// min TTL is 30 minutes
$ttl = max(60 * 30, 60 * $feedRow['data']['ttl']);
} else {
$ttl = $feed->getTTL();
}
$response->setTtl($ttl);
// get correct content type from formatter
$contentType = $this->feedUtils->getFeedFormatter($format)?->getContentType() ?: "text/{$format}";
// cannot use symfony Request::getMethod() nor getRealMethod() because Symfony\Component\HttpKernel\HttpCache\HttpCache::fetch() replaces HEAD with GET for some reason
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD') {
$headResponse = new Response();
$headResponse->headers->set('Content-Type', $contentType);
$cacheFileName = $response->getCacheName();
if (file_exists($cacheFileName) && (time() - filemtime($cacheFileName)) < $ttl) {
$headResponse->headers->set('Content-Length', filesize($cacheFileName));
$headResponse->headers->set('Last-Modified', gmdate('D, d M Y H:i:s ', filemtime($cacheFileName)).'GMT');
} else {
$headResponse->headers->set('Last-Modified', gmdate('D, d M Y H:i:s ', time()).'GMT');
}
return $headResponse;
}
$response->initialize();
$response->headers->set('Content-Type', $contentType);
if ($file) {
$response->headers->set('Content-Description', 'File Transfer');
$response->headers->set('Content-Disposition', "attachment; filename={$feedName}");
$response->headers->set('Connection', 'Keep-Alive');
$response->headers->set('Expires', '0');
}
return $response;
}
}