diff --git a/composer.json b/composer.json index 62d3544..2cdf921 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "excelwebzone/recaptcha-bundle": "^1.5", "sensio/framework-extra-bundle": "^5.1", "sentry/sentry-symfony": "^3.4", + "suin/php-rss-writer": "^1.6", "symfony/console": "^4.1", "symfony/dotenv": "^4.1", "symfony/expression-language": "^4.1", diff --git a/composer.lock b/composer.lock index c676ca6..1435d2d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c91e35bbd8ba2b05ea2a98ac74d5b97", + "content-hash": "4d9cea51c5990806109db1a193618598", "packages": [ { "name": "clue/stream-filter", @@ -3440,6 +3440,55 @@ ], "time": "2020-03-16T09:07:07+00:00" }, + { + "name": "suin/php-rss-writer", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/suin/php-rss-writer.git", + "reference": "78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/suin/php-rss-writer/zipball/78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c", + "reference": "78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "eher/phpunit": ">=1.6", + "mockery/mockery": ">=0.7.2", + "suin/xoopsunit": ">=1.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "Suin\\RSSWriter": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "suin", + "email": "suinyeze@gmail.com" + } + ], + "description": "Yet another simple RSS writer library for PHP 5.4 or later.", + "homepage": "https://github.com/suin/php-rss-writer", + "keywords": [ + "feed", + "generator", + "php", + "rss", + "writer" + ], + "time": "2017-07-13T10:47:50+00:00" + }, { "name": "symfony/cache", "version": "v5.0.7", diff --git a/config/routes.yaml b/config/routes.yaml index 4ec8c20..8eeffea 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -89,4 +89,13 @@ api_v1_torrents_show: requirements: method: GET _format: json - id: '\d+' \ No newline at end of file + id: '\d+' + +api_v1_rss_last: + path: /api/v1/feed/rss/last + controller: App\Api\V1\Controller\RssController::last + defaults: + _format: xml + requirements: + method: GET + _format: xml diff --git a/src/Api/V1/Controller/RssController.php b/src/Api/V1/Controller/RssController.php new file mode 100644 index 0000000..5fd4659 --- /dev/null +++ b/src/Api/V1/Controller/RssController.php @@ -0,0 +1,21 @@ +query->get('page', '1'); + + $xml = $generator->generateLast($page); + + return new Response($xml, 200, ['Content-Type' => self::CONTENT_TYPE]); + } +} \ No newline at end of file diff --git a/src/Feed/RssGenerator.php b/src/Feed/RssGenerator.php new file mode 100644 index 0000000..20d2f0a --- /dev/null +++ b/src/Feed/RssGenerator.php @@ -0,0 +1,133 @@ +repo = $repo; + $this->router = $router; + $this->magnetGenerator = $magnetGenerator; + } + + public function generateLast(int $page): string + { + $qb = $this->createLastTorrentsQueryBuilder(); + $pager = new Pagerfanta(new PagelessDoctrineORMAdapter($qb)); + $pager + ->setAllowOutOfRangePages(true) + ->setCurrentPage($page) + ->setMaxPerPage(self::PER_PAGE) + ; + + $feed = $this->createFeedFromTorrents($pager->getCurrentPageResults()); + + return $feed->render(); + } + + private function createFeedFromTorrents(\Traversable $torrents): Feed + { + $feed = new Feed(); + $channel = $this->createChannel(); + $feed->addChannel($channel); + + foreach ($this->createItemsFromTorrents($torrents) as $item) { + $channel->addItem($item); + } + + // TODO feed pagination + + return $feed; + } + + private function createChannel(): Channel + { + $time = time(); + + $channel = new Channel(); + $channel + ->title('Last') + ->description('Last torrents') + ->url($this->generateUrl('index')) + ->feedUrl($this->generateUrl('api_v1_rss_last')) + ->language('en-US') + ->pubDate($time) + ->lastBuildDate($time) + ->ttl(15) + ; + + return $channel; + } + + /** + * @param Torrent[]|\Traversable $torrents + * + * @return Item[] + */ + private function createItemsFromTorrents(\Traversable $torrents): array + { + $items = []; + + foreach ($torrents as $torrent) { + $items[] = $this->createItemFromTorrent($torrent); + } + + return $items; + } + + private function createItemFromTorrent(Torrent $torrent): Item + { + $item = new Item(); + $item + ->title($torrent->getName()) + ->description($torrent->getInfoHash()) + ->url($this->generateUrl('torrents_show', ['id' => $torrent->getId()])) + ->enclosure( + $this->magnetGenerator->generate($torrent->getInfoHash(), $torrent->getName()), + $torrent->getTotalSize(), + self::MIME_TYPE + ) + ->guid($torrent->getInfoHash()) + ; + + return $item; + } + + private function createLastTorrentsQueryBuilder(): QueryBuilder + { + $qb = $this->repo->createQueryBuilder('t'); + $qb + ->select('t') + ->orderBy('t.id', 'DESC'); + + return $qb; + } + + private function generateUrl(string $route, array $parameters = []): string + { + return $this->router->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); + } +} \ No newline at end of file diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 0252b14..9770f40 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -36,13 +36,18 @@ class ApiTokenAuthenticator extends AbstractGuardAuthenticator public function supports(Request $request): bool { - return $request->headers->has(self::TOKEN_HEADER); + // Let's also support cookies and query params for some cases like torrent clients. + return $request->headers->has(self::TOKEN_HEADER) || + $request->cookies->has(self::TOKEN_HEADER) || + $request->query->has(self::TOKEN_HEADER); } public function getCredentials(Request $request) { return [ - 'token' => $request->headers->get(self::TOKEN_HEADER), + 'token' => $request->headers->get(self::TOKEN_HEADER) ?: + $request->cookies->get(self::TOKEN_HEADER) ?: + $request->query->get(self::TOKEN_HEADER), ]; } @@ -101,7 +106,12 @@ class ApiTokenAuthenticator extends AbstractGuardAuthenticator public function createAuthenticatedToken(UserInterface $user, $providerKey) { - $tokenKey = $this->requestStack->getCurrentRequest()->headers->get(self::TOKEN_HEADER); + $request = $this->requestStack->getCurrentRequest(); + + $tokenKey = $request->headers->get(self::TOKEN_HEADER) ?: + $request->cookies->get(self::TOKEN_HEADER) ?: + $request->query->get(self::TOKEN_HEADER) + ; return new AuthenticatedApiToken( $user, diff --git a/src/Security/Token/AuthenticatedApiToken.php b/src/Security/Token/AuthenticatedApiToken.php index f7d1336..68ff1c1 100644 --- a/src/Security/Token/AuthenticatedApiToken.php +++ b/src/Security/Token/AuthenticatedApiToken.php @@ -4,9 +4,10 @@ namespace App\Security\Token; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; /** This token stores ApiToken key even after eraseCredentials() called */ -class AuthenticatedApiToken extends PreAuthenticatedToken +class AuthenticatedApiToken extends PreAuthenticatedToken implements GuardTokenInterface { /** @var string|null This token is stored only for this request and will not be erased by eraseCredentials() or serialized */ private $tokenKey; diff --git a/symfony.lock b/symfony.lock index 11af624..5c83ca6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -210,6 +210,9 @@ "config/packages/sentry.yaml" ] }, + "suin/php-rss-writer": { + "version": "1.6.0" + }, "symfony/cache": { "version": "v4.1.0" },