<?php declare(strict_types = 1);
namespace MailPoet\Newsletter\Sharing;
if (!defined('ABSPATH')) exit;
use InvalidArgumentException;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\Newsletter\ViewInBrowser\ViewInBrowserRenderer;
class PublicEmailController {
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var NewsletterUrl */
private $newsletterUrl;
/** @var ShareVisibility */
private $shareVisibility;
/** @var ViewInBrowserRenderer */
private $viewInBrowserRenderer;
/** @var ShareMetadataBuilder */
private $shareMetadataBuilder;
public function __construct(
NewslettersRepository $newslettersRepository,
SendingQueuesRepository $sendingQueuesRepository,
NewsletterUrl $newsletterUrl,
ShareVisibility $shareVisibility,
ViewInBrowserRenderer $viewInBrowserRenderer,
ShareMetadataBuilder $shareMetadataBuilder
) {
$this->newslettersRepository = $newslettersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->newsletterUrl = $newsletterUrl;
$this->shareVisibility = $shareVisibility;
$this->viewInBrowserRenderer = $viewInBrowserRenderer;
$this->shareMetadataBuilder = $shareMetadataBuilder;
}
public function getNewsletter(string $identifier): NewsletterEntity {
$data = $this->newsletterUrl->parsePublicShareIdentifier($identifier);
if (!$data) {
throw new InvalidArgumentException('Invalid public email identifier.');
}
$newsletter = $this->newslettersRepository->findOneBy(['hash' => $data['hash']]);
if (!$newsletter instanceof NewsletterEntity || !$this->shareVisibility->canShare($newsletter)) {
throw new InvalidArgumentException('Email is not publicly shareable.');
}
return $newsletter;
}
public function isCanonicalIdentifier(string $identifier, NewsletterEntity $newsletter): bool {
return $this->normalizeIdentifierForCanonicalComparison($identifier)
=== $this->normalizeIdentifierForCanonicalComparison($this->newsletterUrl->getPublicShareIdentifier($newsletter));
}
public function getCanonicalUrl(NewsletterEntity $newsletter): string {
return $this->newsletterUrl->getPublicShareUrl($newsletter);
}
public function render(NewsletterEntity $newsletter): string {
$queue = $this->sendingQueuesRepository->findLatestCompletedByNewsletter($newsletter);
$canonicalUrl = $this->getCanonicalUrl($newsletter);
$html = $this->viewInBrowserRenderer->renderPublicShare($newsletter, $queue);
$html = $this->shareMetadataBuilder->injectShareToolbar($html, $newsletter, $canonicalUrl);
return $this->shareMetadataBuilder->injectMetadata($html, $newsletter, $canonicalUrl);
}
private function normalizeIdentifierForCanonicalComparison(string $identifier): string {
// Normalize only URL percent-escape hex digits, e.g. %F0 -> %f0.
// The slug text remains strict so genuinely stale slugs still redirect.
$normalized = preg_replace_callback('/%[0-9A-Fa-f]{2}/', function (array $matches): string {
return strtolower($matches[0]);
}, $identifier);
return is_string($normalized) ? $normalized : $identifier;
}
}