<?php declare(strict_types = 1);
namespace MailPoet\Newsletter\Embed;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\Router\Endpoints\ViewInBrowser as ViewInBrowserEndpoint;
use MailPoet\Router\Router;
use MailPoet\WP\Functions as WPFunctions;
class NewsletterEmbedService {
public const DEFAULT_HEIGHT = 800;
public const MIN_HEIGHT = 200;
public const MAX_HEIGHT = 3000;
public const DEFAULT_WIDTH = 640;
public const MIN_WIDTH = 320;
public const MAX_WIDTH = 1200;
public const DEFAULT_SELECTOR_LIMIT = 20;
public const MAX_SELECTOR_LIMIT = 100;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var NewsletterUrl */
private $newsletterUrl;
/** @var WPFunctions */
private $wp;
public function __construct(
NewslettersRepository $newslettersRepository,
SendingQueuesRepository $sendingQueuesRepository,
NewsletterUrl $newsletterUrl,
WPFunctions $wp
) {
$this->newslettersRepository = $newslettersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->newsletterUrl = $newsletterUrl;
$this->wp = $wp;
}
public function render(array $rawSettings = []): string {
$settings = $this->sanitizeAttributes($rawSettings);
return $this->renderSanitized($settings);
}
/**
* @param array{newsletterId: int, height: int, width: int, showFallbackLink: bool, fallbackLinkAlignment: string, iframeAlignment: string, showEmailBackground: bool, align: string} $settings
*/
private function renderSanitized(array $settings): string {
$newsletterId = $settings['newsletterId'];
if ($newsletterId <= 0) {
return '';
}
$newsletter = $this->getEmbeddableNewsletter($newsletterId);
if (!$newsletter instanceof NewsletterEntity) {
return '';
}
$queue = $this->getLatestCompletedQueue($newsletter);
if (!$queue instanceof SendingQueueEntity) {
return '';
}
$url = $this->getEmbedUrl($newsletter, $queue, $settings);
$height = $settings['height'];
$width = $settings['width'];
$subject = $newsletter->getSubject();
if ($subject !== null && $subject !== '') {
// translators: %s is the newsletter subject.
$title = sprintf(__('MailPoet newsletter: %s', 'mailpoet'), $subject);
} else {
$title = __('MailPoet newsletter', 'mailpoet');
}
$classNames = 'mailpoet-newsletter-embed';
if ($settings['align'] !== '') {
$classNames .= ' align' . $settings['align'];
}
$html = '<div'
. ' class="' . $this->wp->escAttr($classNames) . '"'
. ' style="' . $this->wp->escAttr('text-align:' . $settings['iframeAlignment'] . ';') . '"'
. '>';
$html .= '<iframe'
. ' class="mailpoet-newsletter-embed-iframe"'
. ' src="' . $this->wp->escUrl($url) . '"'
. ' width="' . $this->wp->escAttr((string)$width) . '"'
. ' height="' . $this->wp->escAttr((string)$height) . '"'
. ' title="' . $this->wp->escAttr($title) . '"'
. ' loading="lazy"'
. ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"'
. ' style="' . $this->wp->escAttr('width:100%;max-width:' . $width . 'px;height:' . $height . 'px;border:0;background:transparent;') . '"'
. '></iframe>';
if ($settings['showFallbackLink']) {
$html .= '<p'
. ' class="mailpoet-newsletter-embed-fallback"'
. ' style="' . $this->wp->escAttr('text-align:' . $settings['fallbackLinkAlignment'] . ';') . '"'
. '>'
. '<a href="' . $this->wp->escUrl($url) . '">'
. $this->wp->escHtml(__('View full newsletter', 'mailpoet'))
. '</a>'
. '</p>';
}
$html .= '</div>';
return $html;
}
/**
* @return array{newsletterId: int, height: int, width: int, showFallbackLink: bool, fallbackLinkAlignment: string, iframeAlignment: string, showEmailBackground: bool, align: string}
*/
public function sanitizeAttributes(array $attributes): array {
return [
'newsletterId' => $this->sanitizePositiveId($attributes['newsletterId'] ?? null),
'height' => $this->sanitizeHeight($attributes['height'] ?? null),
'width' => $this->sanitizeWidth($attributes['width'] ?? null),
'showFallbackLink' => $this->sanitizeBoolean($attributes['showFallbackLink'] ?? true),
'fallbackLinkAlignment' => $this->sanitizeTextAlignment($attributes['fallbackLinkAlignment'] ?? 'center'),
'iframeAlignment' => $this->sanitizeTextAlignment($attributes['iframeAlignment'] ?? 'center'),
'showEmailBackground' => $this->sanitizeBoolean($attributes['showEmailBackground'] ?? true),
'align' => $this->sanitizeAlign($attributes['align'] ?? ''),
];
}
public function getEmbeddableNewsletter(int $newsletterId): ?NewsletterEntity {
if ($newsletterId <= 0) {
return null;
}
return $this->newslettersRepository->findEmbeddableNewsletterById($newsletterId);
}
public function getLatestCompletedQueue(NewsletterEntity $newsletter): ?SendingQueueEntity {
return $this->sendingQueuesRepository->findLatestCompletedByNewsletter($newsletter);
}
/**
* @return array<int, array{id: int, label: string, subject: string, sentAt: ?string, type: string, wpPostId?: int}>
*/
public function getSelectorItems(string $search = '', ?int $limit = null): array {
$limit = $this->sanitizeSelectorLimit($limit);
$rows = $this->newslettersRepository->findEmbeddableNewsletterRows($this->wp->sanitizeTextField($search), $limit);
return array_map(function(array $row): array {
$sentAt = $this->formatSentAt($row['sentAt'] ?? null);
$subject = (string)($row['subject'] ?? '');
$label = $subject;
if ($sentAt !== null) {
$label .= ' - ' . $sentAt;
}
$item = [
'id' => (int)$row['id'],
'label' => $label,
'subject' => $subject,
'sentAt' => $sentAt,
'type' => (string)($row['type'] ?? ''),
];
if (!empty($row['wpPostId'])) {
$item['wpPostId'] = (int)$row['wpPostId'];
}
return $item;
}, $rows);
}
/**
* @param mixed $value
*/
private function sanitizePositiveId($value): int {
if (!is_scalar($value) || $value === '' || !is_numeric($value)) {
return 0;
}
$id = (int)$value;
return $id > 0 ? $id : 0;
}
/**
* @param mixed $value
*/
private function sanitizeHeight($value): int {
if (!is_scalar($value) || $value === '' || !is_numeric($value)) {
return self::DEFAULT_HEIGHT;
}
$height = (int)$value;
if ($height <= 0) {
return self::DEFAULT_HEIGHT;
}
if ($height < self::MIN_HEIGHT) {
return self::MIN_HEIGHT;
}
if ($height > self::MAX_HEIGHT) {
return self::MAX_HEIGHT;
}
return $height;
}
/**
* @param mixed $value
*/
private function sanitizeWidth($value): int {
if (!is_scalar($value) || $value === '' || !is_numeric($value)) {
return self::DEFAULT_WIDTH;
}
$width = (int)$value;
if ($width <= 0) {
return self::DEFAULT_WIDTH;
}
if ($width < self::MIN_WIDTH) {
return self::MIN_WIDTH;
}
if ($width > self::MAX_WIDTH) {
return self::MAX_WIDTH;
}
return $width;
}
/**
* @param mixed $value
*/
private function sanitizeBoolean($value): bool {
if (is_bool($value)) {
return $value;
}
if (is_scalar($value)) {
$normalized = strtolower(trim((string)$value));
return !in_array($normalized, ['0', 'false', 'no', 'off'], true);
}
return true;
}
/**
* @param mixed $value
*/
private function sanitizeTextAlignment($value): string {
if (!is_string($value)) {
return 'center';
}
return in_array($value, ['left', 'center', 'right'], true) ? $value : 'center';
}
/**
* @param mixed $value
*/
private function sanitizeAlign($value): string {
if (!is_string($value)) {
return '';
}
return in_array($value, ['wide', 'full'], true) ? $value : '';
}
/**
* @param mixed $value
*/
private function formatSentAt($value): ?string {
if ($value instanceof \DateTimeInterface) {
$timestamp = $value->getTimestamp();
} elseif (is_string($value) && $value !== '') {
$timestamp = strtotime($value);
if ($timestamp === false) {
return $value;
}
} else {
return null;
}
$dateFormat = (string)$this->wp->getOption('date_format', 'F j, Y');
$timeFormat = (string)$this->wp->getOption('time_format', 'g:i a');
return $this->wp->dateI18n(trim($dateFormat . ' ' . $timeFormat), $timestamp);
}
private function sanitizeSelectorLimit(?int $limit): int {
if (!$limit || $limit < 1) {
return self::DEFAULT_SELECTOR_LIMIT;
}
return min($limit, self::MAX_SELECTOR_LIMIT);
}
/**
* @param array{newsletterId: int, height: int, width: int, showFallbackLink: bool, fallbackLinkAlignment: string, iframeAlignment: string, showEmailBackground: bool, align: string} $attributes
*/
private function getEmbedUrl(NewsletterEntity $newsletter, SendingQueueEntity $queue, array $attributes): string {
if ($attributes['showEmailBackground']) {
return $this->newsletterUrl->getViewInBrowserUrl($newsletter, null, $queue, true);
}
return Router::buildRequest(
ViewInBrowserEndpoint::ENDPOINT,
ViewInBrowserEndpoint::ACTION_VIEW,
[
'newsletter_id' => $newsletter->getId(),
'newsletter_hash' => $newsletter->getHash(),
'subscriber_id' => false,
'subscriber_token' => false,
'queue_id' => $queue->getId(),
'preview' => true,
'embed_hide_background' => true,
]
);
}
}