diff --git a/src/Feed.php b/src/Feed.php index ba41cf8..9ec6078 100644 --- a/src/Feed.php +++ b/src/Feed.php @@ -12,12 +12,15 @@ final class Feed { private Cache $cache; + private ImageStorage $imageStorage; + private string $expirationTime; - public function __construct(IStorage $storage, string $expirationTime) + public function __construct(IStorage $storage, ImageStorage $imageStorage, string $expirationTime) { $this->cache = new Cache($storage, 'wordpress-post-feed'); + $this->imageStorage = $imageStorage; $this->expirationTime = $expirationTime; } @@ -41,6 +44,7 @@ public function load(string $url): array $this->hydrateValueToString($node, 'link'), $this->hydrateValueToDateTime($node, 'pubDate') )) + ->setImageStorage($this->imageStorage) ->setCreator($this->hydrateValueToString($node, 'creator')) ->setCategories($this->hydrateValue($node, 'category')) ->setMainImageUrl($description['mainImageUrl'] ?? null); @@ -91,6 +95,11 @@ private function hydrateDescription(string $description): array if (preg_match('/(.*)]*?src="([^"]+)"[^>]*?>(.*)/', $description, $parser)) { $description = trim($parser[1] . ' ' . $parser[3]); $mainImageUrl = trim($parser[2]); + try { + $this->imageStorage->save($mainImageUrl); + } catch (\InvalidArgumentException $e) { + trigger_error($e->getMessage()); + } } return [ diff --git a/src/ImageStorage.php b/src/ImageStorage.php new file mode 100644 index 0000000..5ef29cb --- /dev/null +++ b/src/ImageStorage.php @@ -0,0 +1,98 @@ +storagePath = $storagePath; + $this->relativeStoragePath = $relativeStoragePath; + } + + + public function save(string $url): void + { + if (Validators::isUrl($url) === false) { + throw new \InvalidArgumentException('Given input is not valid absolute URL, because "' . $url . '" given.'); + } + if (\is_file($storagePath = $this->getInternalPath($url)) === false) { + FileSystem::copy($url, $storagePath); + } + } + + + public function getInternalPath(string $url): string + { + return $this->storagePath . '/' . $this->getRelativeInternalUrl($url); + } + + + public function getAbsoluteInternalUrl(string $url): string + { + return $this->getBaseUrl() . '/' . $this->relativeStoragePath . '/' . $this->getRelativeInternalUrl($url); + } + + + public function getRelativeInternalUrl(string $url): string + { + $originalFileName = (string) preg_replace_callback( + '/^.*\/([^\/]+)\.([^.]+)$/', + fn(array $match): string => substr(Strings::webalize($match[1]), 0, 64) . '.' . strtolower($match[2]), + $url + ); + $relativeName = substr(md5($url), 0, 7) . '-' . $originalFileName; + + return $this->resolvePrefixDir($url) . '/' . $relativeName; + } + + + private function resolvePrefixDir(string $url): string + { + if ($url === '') { + throw new \InvalidArgumentException('URL can not be empty string.'); + } + if (preg_match('/wp-content.+(\d{4})\/(\d{2})/', $url, $urlParser)) { + return $urlParser[1] . '-' . $urlParser[2]; + } + + return substr(md5($url), 0, 7); + } + + + private function getBaseUrl(): string + { + static $return; + if ($return !== null) { + return $return; + } + if (!isset($_SERVER['REQUEST_URI'], $_SERVER['HTTP_HOST'])) { + throw new \RuntimeException('Can not resolve current URL in CLI mode.'); + } + + $currentUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') + . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + + if (preg_match('/^(https?:\/\/.+)\/www\//', $currentUrl, $localUrlParser)) { + $return = $localUrlParser[0]; + } elseif (preg_match('/^(https?:\/\/[^\/]+)/', $currentUrl, $publicUrlParser)) { + $return = $publicUrlParser[1]; + } else { + throw new \RuntimeException('Can not parse relative URL from "' . $currentUrl . '".'); + } + + return ($return = rtrim($return, '/')); + } +} diff --git a/src/Post.php b/src/Post.php index c3eab09..4ed0120 100644 --- a/src/Post.php +++ b/src/Post.php @@ -7,6 +7,8 @@ class Post { + private ?ImageStorage $imageStorage = null; + private string $title; private string $description; @@ -23,9 +25,6 @@ class Post private ?string $mainImageUrl = null; - /** - * @param string[] $categories - */ public function __construct(string $title, string $description, string $link, \DateTimeImmutable $date) { $this->title = $title; @@ -35,6 +34,14 @@ public function __construct(string $title, string $description, string $link, \D } + public function setImageStorage(?ImageStorage $imageStorage): self + { + $this->imageStorage = $imageStorage; + + return $this; + } + + public function getTitle(): string { return $this->title; @@ -93,6 +100,30 @@ public function setCategories(array $categories): self } + public function getAbsoluteInternalUrl(): ?string + { + if ($this->imageStorage === null) { + throw new \RuntimeException('Image storage does not set. Did you create instance this entity from Feed service?'); + } + + return $this->mainImageUrl !== null + ? $this->imageStorage->getAbsoluteInternalUrl($this->mainImageUrl) + : null; + } + + + public function getRelativeInternalUrl(): ?string + { + if ($this->imageStorage === null) { + throw new \RuntimeException('Image storage does not set. Did you create instance this entity from Feed service?'); + } + + return $this->mainImageUrl !== null + ? $this->imageStorage->getRelativeInternalUrl($this->mainImageUrl) + : null; + } + + public function getMainImageUrl(): ?string { return $this->mainImageUrl; diff --git a/src/WordpressPostFeedExtension.php b/src/WordpressPostFeedExtension.php index 88a9074..ad611cb 100644 --- a/src/WordpressPostFeedExtension.php +++ b/src/WordpressPostFeedExtension.php @@ -8,6 +8,7 @@ use Nette\DI\CompilerExtension; use Nette\Schema\Expect; use Nette\Schema\Schema; +use Nette\Utils\FileSystem; final class WordpressPostFeedExtension extends CompilerExtension { @@ -15,6 +16,8 @@ public function getConfigSchema(): Schema { return Expect::structure([ 'expirationTime' => Expect::string('2 hours'), + 'imageStoragePath' => Expect::string(), + 'imageRelativeStoragePath' => Expect::string(), ])->castTo('array'); } @@ -28,5 +31,39 @@ public function beforeCompile(): void $builder->addDefinition($this->prefix('feed')) ->setFactory(Feed::class) ->setArgument('expirationTime', $config['expirationTime'] ?? '2 hours'); + + $imageStoragePaths = $this->resolveImageStoragePath($config, $builder->parameters); + $builder->addDefinition($this->prefix('imageStorage')) + ->setFactory(ImageStorage::class) + ->setArgument('storagePath', $imageStoragePaths['storagePath'] ?? '') + ->setArgument('relativeStoragePath', $imageStoragePaths['relativeStoragePath'] ?? ''); + } + + + /** + * @param mixed[] $config + * @param mixed[] $parameters + * @return string[] + */ + private function resolveImageStoragePath(array $config, array $parameters): array + { + if (isset($config['imageStoragePath'])) { + $storagePath = (string) $config['imageStoragePath']; + if (isset($config['imageRelativeStoragePath']) === false) { + throw new \RuntimeException('Configuration option "imageRelativeStoragePath" is required when option "imageStoragePath" is declared.'); + } + $relativeStoragePath = (string) $config['imageRelativeStoragePath']; + } elseif (isset($parameters['wwwDir'])) { + $relativeStoragePath = 'wordpress-post-feed'; + $storagePath = $parameters['wwwDir'] . '/' . $relativeStoragePath; + } else { + throw new \RuntimeException('Configuration parameter "wwwDir" does not exist. Did you install Nette correctly?'); + } + FileSystem::createDir($storagePath); + + return [ + 'storagePath' => $storagePath, + 'relativeStoragePath' => $relativeStoragePath, + ]; } }