<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\Links\Links as NewsletterLinksService;
use MailPoet\Newsletter\NewslettersRepository;
class NewsletterLinks extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS,
];
private const AUTOMATION_EMAIL_TYPES = [
NewsletterEntity::TYPE_AUTOMATION,
NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL,
];
/** @var NewsletterLinkRepository */
private $newsletterLinkRepository;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterLinksService */
private $newsletterLinks;
public function __construct(
NewsletterLinkRepository $newsletterLinkRepository,
NewslettersRepository $newslettersRepository,
NewsletterLinksService $newsletterLinks
) {
$this->newsletterLinkRepository = $newsletterLinkRepository;
$this->newslettersRepository = $newslettersRepository;
$this->newsletterLinks = $newsletterLinks;
}
public function get($data = []) {
$newsletterId = (int)($data['newsletterId'] ?? 0);
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
if (!$newsletter instanceof NewsletterEntity) {
return $this->successResponse([]);
}
if (in_array($newsletter->getType(), self::AUTOMATION_EMAIL_TYPES, true)) {
return $this->successResponse($this->getAutomationLinks($newsletter));
}
$links = $this->newsletterLinkRepository->findBy(['newsletter' => $newsletterId]);
$response = [];
foreach ($links as $link) {
$response[] = [
'id' => $link->getId(),
'url' => $link->getUrl(),
];
}
return $this->successResponse($response);
}
private function getAutomationLinks(NewsletterEntity $newsletter): array {
$newsletterId = $newsletter->getId();
if (!$newsletterId) {
return [];
}
$urls = [];
foreach ($this->newsletterLinkRepository->findUrlsByNewsletterId($newsletterId) as $url) {
$this->addUrl($urls, $url);
}
foreach ($this->extractUrlsFromNewsletterBody($newsletter) as $url) {
$this->addUrl($urls, $url);
}
return array_map(function(string $url): array {
return [
'id' => $url,
'url' => $url,
];
}, array_values($urls));
}
/**
* @return string[]
*/
private function extractUrlsFromNewsletterBody(NewsletterEntity $newsletter): array {
$body = $newsletter->getBody();
if (!is_array($body)) {
return [];
}
$urls = [];
$this->collectUrlsFromBody($body['content'] ?? $body, $urls);
return array_values($urls);
}
/**
* @param mixed $bodyPart
* @param array<string, string> $urls
*/
private function collectUrlsFromBody($bodyPart, array &$urls): void {
if (!is_array($bodyPart)) {
return;
}
foreach ($bodyPart as $key => $value) {
if (is_string($value)) {
if (in_array($key, ['url', 'link'], true)) {
$this->addUrl($urls, $value);
}
// Links::extract parses HTML and scans for shortcodes; skip when neither marker is present
// to avoid running a DOM parse on every plain-text leaf of the body tree.
if (strpos($value, '<') !== false || strpos($value, '[') !== false) {
foreach ($this->newsletterLinks->extract($value) as $link) {
$this->addUrl($urls, $link['link'] ?? '');
}
}
}
if (is_array($value)) {
$this->collectUrlsFromBody($value, $urls);
}
}
}
/**
* @param array<string, string> $urls
*/
private function addUrl(array &$urls, string $url): void {
$url = trim($url);
if (!$this->isSelectableUrl($url)) {
return;
}
$urls[$url] = $url;
}
private function isSelectableUrl(string $url): bool {
if ($url === '') {
return false;
}
// strpos returns false when '[' is absent and 0 when it's the first char; !== 0 covers both
// "no leading bracket" cases. Allow [link:…] shortcodes through; reject other tokens like [postLink].
return strpos($url, '[') !== 0 || strpos($url, '[link:') === 0;
}
}