This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
<?php
if (!defined('ABSPATH')) exit;
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit9957467448f215809ac9dc8fbcab2b00::getLoader();
@@ -0,0 +1,303 @@
<?php
namespace Composer\Autoload;
if (!defined('ABSPATH')) exit;
class ClassLoader
{
private static $includeFile;
private $vendorDir;
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
private static $registeredLoaders = array();
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
public function getUseIncludePath()
{
return $this->useIncludePath;
}
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
@@ -0,0 +1,183 @@
<?php
namespace Composer;
if (!defined('ABSPATH')) exit;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
class InstalledVersions
{
private static $installed;
private static $canGetVendors;
private static $installedByVendor = array();
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
public static function getAllRawData()
{
return self::getInstalled();
}
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
<?php
if (!defined('ABSPATH')) exit;
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'1f155afceeab454b94183cdd8d5248a6' => $vendorDir . '/mixpanel/mixpanel-php/lib/Mixpanel.php',
'e65c52a9094ad098d9369d5cad5421fa' => $baseDir . '/lib/exceptions.php',
'4407d0da6f795e11445a92772e35fd6a' => $baseDir . '/vendor-prefixed/symfony/polyfill-ctype/bootstrap.php',
'766fdd1f275feb3306ba0045c4fcf600' => $baseDir . '/vendor-prefixed/symfony/polyfill-iconv/bootstrap.php',
'24027dad1c10324ab40c497c832a3fa2' => $baseDir . '/vendor-prefixed/symfony/polyfill-mbstring/bootstrap.php',
'30cb785a37f15f59296ced870db7effb' => $baseDir . '/vendor-prefixed/symfony/polyfill-php80/bootstrap.php',
'01435227b0ecc712eca8e297034a558a' => $baseDir . '/vendor-prefixed/symfony/polyfill-php81/bootstrap.php',
'720b32d405dc3da98c8232fcdb9b60ba' => $baseDir . '/vendor-prefixed/symfony/polyfill-intl-idn/bootstrap.php',
'a39f667d2b6290b6a94a9a0abd6bc10e' => $baseDir . '/vendor-prefixed/symfony/polyfill-intl-normalizer/bootstrap.php',
);
@@ -0,0 +1,7 @@
<?php
if (!defined('ABSPATH')) exit;
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);
@@ -0,0 +1,12 @@
<?php
if (!defined('ABSPATH')) exit;
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'MailPoet\\' => array($baseDir . '/lib'),
'MailPoetVendor\\' => array($baseDir . '/vendor-prefixed'),
'MailPoetGenerated\\' => array($baseDir . '/generated'),
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
);
@@ -0,0 +1,37 @@
<?php
if (!defined('ABSPATH')) exit;
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit9957467448f215809ac9dc8fbcab2b00
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit9957467448f215809ac9dc8fbcab2b00', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit9957467448f215809ac9dc8fbcab2b00', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit9957467448f215809ac9dc8fbcab2b00::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit9957467448f215809ac9dc8fbcab2b00::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,273 @@
{
"packages": [
{
"name": "dragonmantank/cron-expression",
"version": "v3.3.3",
"version_normalized": "3.3.3.0",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
"webmozart/assert": "^1.0"
},
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"time": "2023-08-10T19:36:49+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Tankersley",
"email": "chris@ctankersley.com",
"homepage": "https://github.com/dragonmantank"
}
],
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": [
"cron",
"schedule"
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3"
},
"funding": [
{
"url": "https://github.com/dragonmantank",
"type": "github"
}
],
"install-path": "../dragonmantank/cron-expression"
},
{
"name": "mailpoet/email-editor",
"version": "dev-trunk",
"version_normalized": "dev-trunk",
"dist": {
"type": "path",
"url": "../packages/php/email-editor",
"reference": "311798cfd57b26bb5df1fc7f97b5732e45603419"
},
"require": {
"php": ">=7.4"
},
"type": "library",
"installation-source": "dist",
"autoload": {
"classmap": [
"src/"
]
},
"autoload-dev": {
"classmap": [
"tests/unit/"
]
},
"scripts": {
"unit-test": [
"../../../tests_env/vendor/bin/codecept run unit"
],
"integration-test": [
"cd ../../../tests_env/docker && COMPOSE_HTTP_TIMEOUT=200 docker compose run -e SKIP_DEPS=1 -e SKIP_PLUGINS=1 -e PACKAGE_NAME=email-editor codeception_integration"
],
"code-style": [
"../../../mailpoet/tasks/code_sniffer/vendor/bin/phpcs -ps"
],
"code-style-fix": [
"../../../mailpoet/tasks/code_sniffer/vendor/bin/phpcbf -p"
]
},
"description": "Email editor based on WordPress Gutenberg package.",
"transport-options": {
"relative": true
},
"install-path": "../mailpoet/email-editor"
},
{
"name": "mixpanel/mixpanel-php",
"version": "2.11.0",
"version_normalized": "2.11.0.0",
"source": {
"type": "git",
"url": "https://github.com/mixpanel/mixpanel-php.git",
"reference": "4b0fafacf2129eff5d50721e129b07f0c32687e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mixpanel/mixpanel-php/zipball/4b0fafacf2129eff5d50721e129b07f0c32687e7",
"reference": "4b0fafacf2129eff5d50721e129b07f0c32687e7",
"shasum": ""
},
"require": {
"php": ">=5.0"
},
"require-dev": {
"phpdocumentor/phpdocumentor": "2.9.*",
"phpunit/phpunit": "5.6.*"
},
"time": "2023-04-11T23:03:57+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/Mixpanel.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Mixpanel <dev@mixpanel.com>",
"homepage": "https://mixpanel.com/"
}
],
"description": "The Official PHP library for Mixpanel",
"homepage": "https://mixpanel.com/help/reference/php",
"keywords": [
"mixpanel",
"mixpanel php"
],
"support": {
"issues": "https://github.com/mixpanel/mixpanel-php/issues",
"source": "https://github.com/mixpanel/mixpanel-php/tree/2.11.0"
},
"install-path": "../mixpanel/mixpanel-php"
},
{
"name": "webmozart/assert",
"version": "1.11.0",
"version_normalized": "1.11.0.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"php": "^7.2 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
},
"time": "2022-06-03T18:03:27+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
},
"install-path": "../webmozart/assert"
},
{
"name": "woocommerce/action-scheduler",
"version": "3.8.0",
"version_normalized": "3.8.0.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/action-scheduler.git",
"reference": "99cd7981f51c98883082534d4852491858d72834"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/99cd7981f51c98883082534d4852491858d72834",
"reference": "99cd7981f51c98883082534d4852491858d72834",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "^7.5",
"woocommerce/woocommerce-sniffs": "0.1.0",
"wp-cli/wp-cli": "~2.5.0",
"yoast/phpunit-polyfills": "^2.0"
},
"time": "2024-05-22T13:50:29+00:00",
"type": "wordpress-plugin",
"extra": {
"scripts-description": {
"test": "Run unit tests",
"phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer",
"phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier"
}
},
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-or-later"
],
"description": "Action Scheduler for WordPress and WooCommerce",
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/3.8.0"
},
"install-path": "../woocommerce/action-scheduler"
}
],
"dev": false,
"dev-package-names": []
}
@@ -0,0 +1,76 @@
<?php
if (!defined('ABSPATH')) exit;
return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-trunk',
'version' => 'dev-trunk',
'reference' => '6b7f6af6aa82d76ed2d7b4772298c76ae1bac7f4',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => false,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-trunk',
'version' => 'dev-trunk',
'reference' => '6b7f6af6aa82d76ed2d7b4772298c76ae1bac7f4',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'dragonmantank/cron-expression' => array(
'pretty_version' => 'v3.3.3',
'version' => '3.3.3.0',
'reference' => 'adfb1f505deb6384dc8b39804c5065dd3c8c8c0a',
'type' => 'library',
'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
'aliases' => array(),
'dev_requirement' => false,
),
'mailpoet/email-editor' => array(
'pretty_version' => 'dev-trunk',
'version' => 'dev-trunk',
'reference' => '311798cfd57b26bb5df1fc7f97b5732e45603419',
'type' => 'library',
'install_path' => __DIR__ . '/../mailpoet/email-editor',
'aliases' => array(),
'dev_requirement' => false,
),
'mixpanel/mixpanel-php' => array(
'pretty_version' => '2.11.0',
'version' => '2.11.0.0',
'reference' => '4b0fafacf2129eff5d50721e129b07f0c32687e7',
'type' => 'library',
'install_path' => __DIR__ . '/../mixpanel/mixpanel-php',
'aliases' => array(),
'dev_requirement' => false,
),
'mtdowling/cron-expression' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '^1.0',
),
),
'webmozart/assert' => array(
'pretty_version' => '1.11.0',
'version' => '1.11.0.0',
'reference' => '11cb2199493b2f8a3b53e7f19068fc6aac760991',
'type' => 'library',
'install_path' => __DIR__ . '/../webmozart/assert',
'aliases' => array(),
'dev_requirement' => false,
),
'woocommerce/action-scheduler' => array(
'pretty_version' => '3.8.0',
'version' => '3.8.0.0',
'reference' => '99cd7981f51c98883082534d4852491858d72834',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../woocommerce/action-scheduler',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
@@ -0,0 +1,23 @@
<?php
if (!defined('ABSPATH')) exit;
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70400)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
abstract class AbstractField implements FieldInterface
{
protected $fullRange = [];
protected $literals = [];
protected $rangeStart;
protected $rangeEnd;
public function __construct()
{
$this->fullRange = range($this->rangeStart, $this->rangeEnd);
}
public function isSatisfied(int $dateValue, string $value): bool
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
}
if ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return '*' === $value || $dateValue === (int) $value;
}
public function isRange(string $value): bool
{
return false !== strpos($value, '-');
}
public function isIncrementsOfRanges(string $value): bool
{
return false !== strpos($value, '/');
}
public function isInRange(int $dateValue, $value): bool
{
$parts = array_map(
function ($value) {
$value = trim($value);
return $this->convertLiterals($value);
},
explode('-', $value, 2)
);
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
public function isInIncrementsOfRanges(int $dateValue, string $value): bool
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = $chunks[1] ?? 0;
// No step or 0 steps aren't cool
if (null === $step || '0' === $step || 0 === $step) {
return false;
}
// Expand the * to a full range
if ('*' === $range) {
$range = $this->rangeStart . '-' . $this->rangeEnd;
}
// Generate the requested small range
$rangeChunks = explode('-', $range, 2);
$rangeStart = (int) $rangeChunks[0];
$rangeEnd = $rangeChunks[1] ?? $rangeStart;
$rangeEnd = (int) $rangeEnd;
if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
throw new \OutOfRangeException('Invalid range start requested');
}
if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) {
throw new \OutOfRangeException('Invalid range end requested');
}
// Steps larger than the range need to wrap around and be handled
// slightly differently than smaller steps
// UPDATE - This is actually false. The C implementation will allow a
// larger step as valid syntax, it never wraps around. It will stop
// once it hits the end. Unfortunately this means in future versions
// we will not wrap around. However, because the logic exists today
// per the above documentation, fixing the bug from #89
if ($step > $this->rangeEnd) {
$thisRange = [$this->fullRange[$step % \count($this->fullRange)]];
} else {
if ($step > ($rangeEnd - $rangeStart)) {
$thisRange[$rangeStart] = (int) $rangeStart;
} else {
$thisRange = range($rangeStart, $rangeEnd, (int) $step);
}
}
return \in_array($dateValue, $thisRange, true);
}
public function getRangeForExpression(string $expression, int $max): array
{
$values = [];
$expression = $this->convertLiterals($expression);
if (false !== strpos($expression, ',')) {
$ranges = explode(',', $expression);
$values = [];
foreach ($ranges as $range) {
$expanded = $this->getRangeForExpression($range, $this->rangeEnd);
$values = array_merge($values, $expanded);
}
return $values;
}
if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
if (!$this->isIncrementsOfRanges($expression)) {
[$offset, $to] = explode('-', $expression);
$offset = $this->convertLiterals($offset);
$to = $this->convertLiterals($to);
$stepSize = 1;
} else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = $range[1] ?? 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = $range[1] ?? $max;
}
$offset = '*' === $offset ? $this->rangeStart : $offset;
if ($stepSize >= $this->rangeEnd) {
$values = [$this->fullRange[$stepSize % \count($this->fullRange)]];
} else {
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = (int) $i;
}
}
sort($values);
} else {
$values = [$expression];
}
return $values;
}
protected function convertLiterals(string $value): string
{
if (\count($this->literals)) {
$key = array_search(strtoupper($value), $this->literals, true);
if (false !== $key) {
return (string) $key;
}
}
return $value;
}
public function validate(string $value): bool
{
$value = $this->convertLiterals($value);
// All fields allow * as a valid value
if ('*' === $value) {
return true;
}
// Validate each chunk of a list individually
if (false !== strpos($value, ',')) {
foreach (explode(',', $value) as $listItem) {
if (!$this->validate($listItem)) {
return false;
}
}
return true;
}
if (false !== strpos($value, '/')) {
[$range, $step] = explode('/', $value);
// Don't allow numeric ranges
if (is_numeric($range)) {
return false;
}
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
if (false !== strpos($value, '-')) {
if (substr_count($value, '-') > 1) {
return false;
}
$chunks = explode('-', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
$chunks[1] = $this->convertLiterals($chunks[1]);
if ('*' === $chunks[0] || '*' === $chunks[1]) {
return false;
}
return $this->validate($chunks[0]) && $this->validate($chunks[1]);
}
if (!is_numeric($value)) {
return false;
}
if (false !== strpos($value, '.')) {
return false;
}
// We should have a numeric by now, so coerce this into an integer
$value = (int) $value;
return \in_array($value, $this->fullRange, true);
}
protected function timezoneSafeModify(DateTimeInterface $dt, string $modification): DateTimeInterface
{
$timezone = $dt->getTimezone();
$dt = $dt->setTimezone(new \DateTimeZone("UTC"));
$dt = $dt->modify($modification);
$dt = $dt->setTimezone($timezone);
return $dt;
}
protected function setTimeHour(DateTimeInterface $date, bool $invert, int $originalTimestamp): DateTimeInterface
{
$date = $date->setTime((int)$date->format('H'), ($invert ? 59 : 0));
// setTime caused the offset to change, moving time in the wrong direction
$actualTimestamp = $date->format('U');
if ((! $invert) && ($actualTimestamp <= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "+1 hour");
} elseif ($invert && ($actualTimestamp >= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "-1 hour");
}
return $date;
}
}
@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
use Webmozart\Assert\Assert;
class CronExpression
{
public const MINUTE = 0;
public const HOUR = 1;
public const DAY = 2;
public const MONTH = 3;
public const WEEKDAY = 4;
public const YEAR = 5;
public const MAPPINGS = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@midnight' => '0 0 * * *',
'@hourly' => '0 * * * *',
];
protected $cronParts;
protected $fieldFactory;
protected $maxIterationCount = 1000;
protected static $order = [
self::YEAR,
self::MONTH,
self::DAY,
self::WEEKDAY,
self::HOUR,
self::MINUTE,
];
private static $registeredAliases = self::MAPPINGS;
public static function registerAlias(string $alias, string $expression): void
{
try {
new self($expression);
} catch (InvalidArgumentException $exception) {
throw new LogicException("The expression `$expression` is invalid", 0, $exception);
}
$shortcut = strtolower($alias);
if (1 !== preg_match('/^@\w+$/', $shortcut)) {
throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}
if (isset(self::$registeredAliases[$shortcut])) {
throw new LogicException("The alias `$alias` is already registered.");
}
self::$registeredAliases[$shortcut] = $expression;
}
public static function unregisterAlias(string $alias): bool
{
$shortcut = strtolower($alias);
if (isset(self::MAPPINGS[$shortcut])) {
throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered.");
}
if (!isset(self::$registeredAliases[$shortcut])) {
return false;
}
unset(self::$registeredAliases[$shortcut]);
return true;
}
public static function supportsAlias(string $alias): bool
{
return isset(self::$registeredAliases[strtolower($alias)]);
}
public static function getAliases(): array
{
return self::$registeredAliases;
}
public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression
{
return new static($expression, $fieldFactory);
}
public static function isValidExpression(string $expression): bool
{
try {
new CronExpression($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null)
{
$shortcut = strtolower($expression);
$expression = self::$registeredAliases[$shortcut] ?? $expression;
$this->fieldFactory = $fieldFactory ?: new FieldFactory();
$this->setExpression($expression);
}
public function setExpression(string $value): CronExpression
{
$split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
Assert::isArray($split);
$notEnoughParts = \count($split) < 5;
$questionMarkInInvalidPart = array_key_exists(0, $split) && $split[0] === '?'
|| array_key_exists(1, $split) && $split[1] === '?'
|| array_key_exists(3, $split) && $split[3] === '?';
$tooManyQuestionMarks = array_key_exists(2, $split) && $split[2] === '?'
&& array_key_exists(4, $split) && $split[4] === '?';
if ($notEnoughParts || $questionMarkInInvalidPart || $tooManyQuestionMarks) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
$this->cronParts = $split;
foreach ($this->cronParts as $position => $part) {
$this->setPart($position, $part);
}
return $this;
}
public function setPart(int $position, string $value): CronExpression
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
'Invalid CRON field value ' . $value . ' at position ' . $position
);
}
$this->cronParts[$position] = $value;
return $this;
}
public function setMaxIterationCount(int $maxIterationCount): CronExpression
{
$this->maxIterationCount = $maxIterationCount;
return $this;
}
public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
}
public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
Assert::isInstanceOf($currentTime, DateTime::class);
$currentTime->setTimezone(new DateTimeZone($timeZone));
$matches = [];
for ($i = 0; $i < $total; ++$i) {
try {
$result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
$allowCurrentDate = false;
$currentTime = clone $result;
$matches[] = $result;
}
return $matches;
}
public function getExpression($part = null): ?string
{
if (null === $part) {
return implode(' ', $this->cronParts);
}
if (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
public function getParts()
{
return $this->cronParts;
}
public function __toString(): string
{
return (string) $this->getExpression();
}
public function isDue($currentTime = 'now', $timeZone = null): bool
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
Assert::isInstanceOf($currentTime, DateTime::class);
$currentTime->setTimezone(new DateTimeZone($timeZone));
// drop the seconds to 0
$currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);
try {
return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
} catch (Exception $e) {
return false;
}
}
protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentDate = new DateTime($currentTime);
} else {
$currentDate = new DateTime('now');
}
Assert::isInstanceOf($currentDate, DateTime::class);
$currentDate->setTimezone(new DateTimeZone($timeZone));
// Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074
$currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone());
if ($currentDate === false) {
throw new \RuntimeException('Unable to create date from format');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
$nextRun = clone $currentDate;
// We don't have to satisfy * or null fields
$parts = [];
$fields = [];
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) {
$domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3));
$dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4));
$domExpression = new self($domExpression);
$dowExpression = new self($dowExpression);
$domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') {
$domRunDates = [];
}
if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') {
$dowRunDates = [];
}
$combined = array_merge($domRunDates, $dowRunDates);
usort($combined, function ($a, $b) {
return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s');
});
if ($invert) {
$combined = array_reverse($combined);
}
return $combined[$nth];
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; ++$i) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (false === strpos($part, ',')) {
$satisfied = $field->isSatisfiedBy($nextRun, $part, $invert);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
protected function determineTimeZone($currentTime, ?string $timeZone): string
{
if (null !== $timeZone) {
return $timeZone;
}
if ($currentTime instanceof DateTimeInterface) {
return $currentTime->getTimezone()->getName();
}
return date_default_timezone_get();
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTime;
use DateTimeInterface;
class DayOfMonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 31;
private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime
{
$tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}");
if ($target === false) {
return null;
}
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
return $target;
}
$lastDayOfMonth = $target->format('t');
foreach ([-1, 1, -2, 2] as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ((int) $target->format('N') < 6 && (int) $target->format('m') === $currentMonth) {
return $target;
}
}
}
return null;
}
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
// ? states that the field value is to be skipped
if ('?' === $value) {
return true;
}
$fieldValue = $date->format('d');
// Check to see if this is the last day of the month
if ('L' === $value) {
return $fieldValue === $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if ($wPosition = strpos($value, 'W')) {
// Parse the target day
$targetDay = (int) substr($value, 0, $wPosition);
// Find out if the current day is the nearest day of the week
$nearest = self::getNearestWeekday(
(int) $date->format('Y'),
(int) $date->format('m'),
$targetDay
);
if ($nearest) {
return $date->format('j') === $nearest->format('j');
}
throw new \RuntimeException('Unable to return nearest weekday');
}
return $this->isSatisfied((int) $date->format('d'), $value);
}
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
// Validate that a list don't have W or L
if (false !== strpos($value, ',') && (false !== strpos($value, 'W') || false !== strpos($value, 'L'))) {
return false;
}
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
if ('L' === $value) {
return true;
}
if (preg_match('/^(.*)W$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
use InvalidArgumentException;
class DayOfWeekField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 7;
protected $nthRange;
protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN'];
public function __construct()
{
$this->nthRange = range(1, 5);
parent::__construct();
}
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ('?' === $value) {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = (int) $date->format('Y');
$currentMonth = (int) $date->format('m');
$lastDayOfMonth = (int) $date->format('t');
// Find out if this is the last specific weekday of the month
if ($lPosition = strpos($value, 'L')) {
$weekday = $this->convertLiterals(substr($value, 0, $lPosition));
$weekday %= 7;
$daysInMonth = (int) $date->format('t');
$remainingDaysInMonth = $daysInMonth - (int) $date->format('d');
return (($weekday === (int) $date->format('w')) && ($remainingDaysInMonth < 7));
}
// Handle # hash tokens
if (strpos($value, '#')) {
[$weekday, $nth] = explode('#', $value);
if (!is_numeric($nth)) {
throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given");
} else {
$nth = (int) $nth;
}
// 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
if ('0' === $weekday) {
$weekday = 7;
}
$weekday = (int) $this->convertLiterals((string) $weekday);
// Validate the hash fields
if ($weekday < 0 || $weekday > 7) {
throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
}
if (!\in_array($nth, $this->nthRange, true)) {
throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given");
}
// The current weekday must match the targeted weekday to proceed
if ((int) $date->format('N') !== $weekday) {
return false;
}
$tdate = clone $date;
$tdate = $tdate->setDate($currentYear, $currentMonth, 1);
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ((int) $tdate->format('N') === $weekday) {
if (++$dayCount >= $nth) {
break;
}
}
$tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return (int) $date->format('j') === $currentDay;
}
// Handle day of the week values
if (false !== strpos($value, '-')) {
$parts = explode('-', $value);
if ('7' === $parts[0]) {
$parts[0] = 0;
} elseif ('0' === $parts[1]) {
$parts[1] = 7;
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = \in_array(7, array_map(function ($value) {
return (int) $value;
}, str_split($value)), true) ? 'N' : 'w';
$fieldValue = (int) $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
// Handle the # value
if (false !== strpos($value, '#')) {
$chunks = explode('#', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && \in_array((int) $chunks[1], $this->nthRange, true)) {
return true;
}
}
if (preg_match('/^(.*)L$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use InvalidArgumentException;
class FieldFactory implements FieldFactoryInterface
{
private $fields = [];
public function getField(int $position): FieldInterface
{
return $this->fields[$position] ?? $this->fields[$position] = $this->instantiateField($position);
}
private function instantiateField(int $position): FieldInterface
{
switch ($position) {
case CronExpression::MINUTE:
return new MinutesField();
case CronExpression::HOUR:
return new HoursField();
case CronExpression::DAY:
return new DayOfMonthField();
case CronExpression::MONTH:
return new MonthField();
case CronExpression::WEEKDAY:
return new DayOfWeekField();
}
throw new InvalidArgumentException(
($position + 1) . ' is not a valid position'
);
}
}
@@ -0,0 +1,7 @@
<?php
namespace Cron;
if (!defined('ABSPATH')) exit;
interface FieldFactoryInterface
{
public function getField(int $position): FieldInterface;
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
interface FieldInterface
{
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool;
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface;
public function validate(string $value): bool;
}
@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
use DateTimeZone;
class HoursField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 23;
protected $transitions = [];
protected $transitionsStart = null;
protected $transitionsEnd = null;
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
$checkValue = (int) $date->format('H');
$retval = $this->isSatisfied($checkValue, $value);
if ($retval) {
return $retval;
}
// Are we on the edge of a transition
$lastTransition = $this->getPastTransition($date);
if (($lastTransition !== null) && ($lastTransition["ts"] > ((int) $date->format('U') - 3600))) {
$dtLastOffset = clone $date;
$this->timezoneSafeModify($dtLastOffset, "-1 hour");
$lastOffset = $dtLastOffset->getOffset();
$dtNextOffset = clone $date;
$this->timezoneSafeModify($dtNextOffset, "+1 hour");
$nextOffset = $dtNextOffset->getOffset();
$offsetChange = $nextOffset - $lastOffset;
if ($offsetChange >= 3600) {
$checkValue -= 1;
return $this->isSatisfied($checkValue, $value);
}
if ((! $invert) && ($offsetChange <= -3600)) {
$checkValue += 1;
return $this->isSatisfied($checkValue, $value);
}
}
return $retval;
}
public function getPastTransition(DateTimeInterface $date): ?array
{
$currentTimestamp = (int) $date->format('U');
if (
($this->transitions === null)
|| ($this->transitionsStart < ($currentTimestamp + 86400))
|| ($this->transitionsEnd > ($currentTimestamp - 86400))
) {
// We start a day before current time so we can differentiate between the first transition entry
// and a change that happens now
$dtLimitStart = clone $date;
$dtLimitStart = $dtLimitStart->modify("-12 months");
$dtLimitEnd = clone $date;
$dtLimitEnd = $dtLimitEnd->modify('+12 months');
$this->transitions = $date->getTimezone()->getTransitions(
$dtLimitStart->getTimestamp(),
$dtLimitEnd->getTimestamp()
);
if (empty($this->transitions)) {
return null;
}
$this->transitionsStart = $dtLimitStart->getTimestamp();
$this->transitionsEnd = $dtLimitEnd->getTimestamp();
}
$nextTransition = null;
foreach ($this->transitions as $transition) {
if ($transition["ts"] > $currentTimestamp) {
continue;
}
if (($nextTransition !== null) && ($transition["ts"] < $nextTransition["ts"])) {
continue;
}
$nextTransition = $transition;
}
return ($nextTransition ?? null);
}
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
$originalTimestamp = (int) $date->format('U');
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (null === $parts || '*' === $parts) {
if ($invert) {
$date = $date->sub(new \DateInterval('PT1H'));
} else {
$date = $date->add(new \DateInterval('PT1H'));
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
return $this;
}
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
$hours = [];
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = (int) $date->format('H');
$position = $invert ? \count($hours) - 1 : 0;
$countHours = \count($hours);
if ($countHours > 1) {
for ($i = 0; $i < $countHours - 1; ++$i) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$target = (int) $hours[$position];
$originalHour = (int)$date->format('H');
$originalDay = (int)$date->format('d');
$previousOffset = $date->getOffset();
if (! $invert) {
if ($originalHour >= $target) {
$distance = 24 - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay + 1)) && ($actualHour !== 0)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
$originalHour = (int)$date->format('H');
}
$distance = $target - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
} else {
if ($originalHour <= $target) {
$distance = ($originalHour + 1);
$date = $this->timezoneSafeModify($date, "-" . $distance . " hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay - 1)) && ($actualHour !== 23)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
$originalHour = (int)$date->format('H');
}
$distance = $originalHour - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} hours");
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
$actualHour = (int)$date->format('H');
if ($invert && ($actualHour === ($target - 1) || (($actualHour === 23) && ($target === 0)))) {
$date = $this->timezoneSafeModify($date, "+1 hour");
}
return $this;
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
class MinutesField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 59;
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool
{
if ($value === '?') {
return true;
}
return $this->isSatisfied((int)$date->format('i'), $value);
}
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (is_null($parts)) {
$date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute");
return $this;
}
$current_minute = (int) $date->format('i');
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
sort($parts);
$minutes = [];
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$position = $invert ? \count($minutes) - 1 : 0;
if (\count($minutes) > 1) {
for ($i = 0; $i < \count($minutes) - 1; ++$i) {
if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$target = (int) $minutes[$position];
$originalMinute = (int) $date->format("i");
if (! $invert) {
if ($originalMinute >= $target) {
$distance = 60 - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
$distance = $target - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
} else {
if ($originalMinute <= $target) {
$distance = ($originalMinute + 1);
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
$distance = $originalMinute - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
}
return $this;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Cron;
if (!defined('ABSPATH')) exit;
use DateTimeInterface;
class MonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 12;
protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL',
8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC', ];
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ($value === '?') {
return true;
}
$value = $this->convertLiterals($value);
return $this->isSatisfied((int) $date->format('m'), $value);
}
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->modify('first day of next month');
$date = $date->setTime(0, 0);
} else {
$date = $date->modify('last day of previous month');
$date = $date->setTime(23, 59);
}
return $this;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1 @@
<?php
@@ -0,0 +1 @@
<?php
+1
View File
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,7 @@
{
"*.php": [
"../../../mailpoet/tools/vendor/composer.phar code-style-fix",
"../../../mailpoet/tools/vendor/composer.phar phpstan"
],
"*": "../../../mailpoet/tools/vendor/composer.phar code-style"
}
@@ -0,0 +1,14 @@
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
settings:
colors: true
memory_limit: 1024M
log: true
strict_xml: true
extensions:
enabled:
- Codeception\Extension\RunFailed
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,32 @@
<?xml version="1.0"?>
<ruleset name="Custom WordPress Standards">
<!-- Set the base standard to WordPress -->
<rule ref="WordPress"/>
<!-- Define files and folders to scan -->
<file>.</file>
<!-- Exclude test files from FileName rules because we need to match Codeception autoload -->
<rule ref="WordPress.Files.FileName">
<exclude-pattern>tests/*</exclude-pattern>
</rule>
<!-- We should use WP_Filesystem or another safer solution but this change can be more complex -->
<rule ref="WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents">
<exclude-pattern>src</exclude-pattern>
</rule>
<!-- Exclude bootstrap from the SeparateFunctionsFromOO.Mixed rule -->
<rule ref="Universal.Files.SeparateFunctionsFromOO.Mixed">
<exclude-pattern>tests/unit/_bootstrap.php</exclude-pattern>
</rule>
<!-- Exclude test files from the PSR2.Methods.MethodDeclaration.Underscore rule due to methods _after() and _before() -->
<rule ref="PSR2.Methods.MethodDeclaration.Underscore">
<exclude-pattern>tests/integration</exclude-pattern>
<exclude-pattern>tests/unit</exclude-pattern>
</rule>
<!-- Skip the vendor directory -->
<exclude-pattern>vendor/*</exclude-pattern>
</ruleset>
@@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Patterns;
if (!defined('ABSPATH')) exit;
abstract class Abstract_Pattern {
protected $name = '';
protected $namespace = '';
protected $block_types = array();
protected $template_types = array();
protected $inserter = true;
protected $source = 'plugin';
protected $categories = array();
protected $viewport_width = 620;
public function get_name(): string {
return $this->name;
}
public function get_namespace(): string {
return $this->namespace;
}
public function get_properties(): array {
return array(
'title' => $this->get_title(),
'content' => $this->get_content(),
'description' => $this->get_description(),
'categories' => $this->categories,
'inserter' => $this->inserter,
'blockTypes' => $this->block_types,
'templateTypes' => $this->template_types,
'source' => $this->source,
'viewportWidth' => $this->viewport_width,
);
}
abstract protected function get_content(): string;
abstract protected function get_title(): string;
protected function get_description(): string {
return '';
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Patterns;
if (!defined('ABSPATH')) exit;
class Patterns {
public function initialize(): void {
$this->register_block_pattern_categories();
}
private function register_block_pattern_categories(): void {
$categories = array(
array(
'name' => 'email-contents',
'label' => _x( 'Email Contents', 'Block pattern category', 'mailpoet' ),
'description' => __( 'A collection of email content layouts.', 'mailpoet' ),
),
);
foreach ( $categories as $category ) {
register_block_pattern_category(
$category['name'],
array(
'label' => $category['label'],
'description' => $category['description'] ?? '',
)
);
}
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\PersonalizationTags;
if (!defined('ABSPATH')) exit;
use WP_HTML_Tag_Processor;
use WP_HTML_Text_Replacement;
class HTML_Tag_Processor extends WP_HTML_Tag_Processor {
private $deferred_updates = array();
public function replace_token( string $new_content ): void {
$this->set_bookmark( 'here' );
$here = $this->bookmarks['here'];
$this->deferred_updates[] = new WP_HTML_Text_Replacement(
$here->start,
$here->length,
$new_content
);
}
public function flush_updates(): void {
foreach ( $this->deferred_updates as $key => $update ) {
$this->lexical_updates[] = $update;
unset( $this->deferred_updates[ $key ] );
}
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\PersonalizationTags;
if (!defined('ABSPATH')) exit;
class Personalization_Tag {
private string $name;
private string $token;
private string $category;
private $callback;
private array $attributes;
private string $value_to_insert;
public function __construct(
string $name,
string $token,
string $category,
callable $callback,
array $attributes = array(),
?string $value_to_insert = null
) {
$this->name = $name;
// Because Gutenberg does not wrap the token with square brackets, we need to add them here.
$this->token = strpos( $token, '[' ) === 0 ? $token : "[$token]";
$this->category = $category;
$this->callback = $callback;
$this->attributes = $attributes;
// Composing token to insert based on the token and attributes if it is not set.
if ( ! $value_to_insert ) {
if ( $this->attributes ) {
$value_to_insert = substr( $this->token, 0, -1 ) . ' ' .
implode(
' ',
array_map(
function ( $key ) {
return $key . '="' . esc_attr( $this->attributes[ $key ] ) . '"';
},
array_keys( $this->attributes )
)
) . ']';
} else {
$value_to_insert = $this->token;
}
}
$this->value_to_insert = $value_to_insert;
}
public function get_name(): string {
return $this->name;
}
public function get_token(): string {
return $this->token;
}
public function get_category(): string {
return $this->category;
}
public function get_attributes(): array {
return $this->attributes;
}
public function get_value_to_insert(): string {
return $this->value_to_insert;
}
public function execute_callback( $context, $args = array() ): string {
return call_user_func( $this->callback, ...array_merge( array( $context ), array( $args ) ) );
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\PersonalizationTags;
if (!defined('ABSPATH')) exit;
class Personalization_Tags_Registry {
private $tags = array();
public function initialize(): void {
apply_filters( 'mailpoet_email_editor_register_personalization_tags', $this );
}
public function register( Personalization_Tag $tag ): void {
if ( isset( $this->tags[ $tag->get_token() ] ) ) {
return;
}
$this->tags[ $tag->get_token() ] = $tag;
}
public function get_by_token( string $token ): ?Personalization_Tag {
return $this->tags[ $token ] ?? null;
}
public function get_all() {
return $this->tags;
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Layout;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Flex_Layout_Renderer {
public function render_inner_blocks_in_layout( array $parsed_block, Settings_Controller $settings_controller ): string {
$theme_styles = $settings_controller->get_email_styles();
$flex_gap = $theme_styles['spacing']['blockGap'] ?? '0px';
$flex_gap_number = $settings_controller->parse_number_from_string_with_pixels( $flex_gap );
$margin_top = $parsed_block['email_attrs']['margin-top'] ?? '0px';
$justify = $parsed_block['attrs']['layout']['justifyContent'] ?? 'left';
$styles = wp_style_engine_get_styles( $parsed_block['attrs']['style'] ?? array() )['css'] ?? '';
$styles .= 'margin-top: ' . $margin_top . ';';
$styles .= 'text-align: ' . $justify;
// MS Outlook doesn't support style attribute in divs so we conditionally wrap the buttons in a table and repeat styles.
$output_html = sprintf(
'<!--[if mso | IE]><table align="%2$s" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%"><tr><td style="%1$s" ><![endif]-->
<div style="%1$s"><table class="layout-flex-wrapper" style="display:inline-block"><tbody><tr>',
esc_attr( $styles ),
esc_attr( $justify )
);
$inner_blocks = $this->compute_widths_for_flex_layout( $parsed_block, $settings_controller, $flex_gap_number );
foreach ( $inner_blocks as $key => $block ) {
$styles = array();
if ( $block['email_attrs']['layout_width'] ?? null ) {
$styles['width'] = $block['email_attrs']['layout_width'];
}
if ( $key > 0 ) {
$styles['padding-left'] = $flex_gap;
}
$output_html .= '<td class="layout-flex-item" style="' . esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) . '">' . render_block( $block ) . '</td>';
}
$output_html .= '</tr></table></div>
<!--[if mso | IE]></td></tr></table><![endif]-->';
return $output_html;
}
private function compute_widths_for_flex_layout( array $parsed_block, Settings_Controller $settings_controller, float $flex_gap ): array {
// When there is no parent width we can't compute widths so auto width will be used.
if ( ! isset( $parsed_block['email_attrs']['width'] ) ) {
return $parsed_block['innerBlocks'] ?? array();
}
$blocks_count = count( $parsed_block['innerBlocks'] );
$total_used_width = 0; // Total width assuming items without set width would consume proportional width.
$parent_width = $settings_controller->parse_number_from_string_with_pixels( $parsed_block['email_attrs']['width'] );
$inner_blocks = $parsed_block['innerBlocks'] ?? array();
foreach ( $inner_blocks as $key => $block ) {
$block_width_percent = ( $block['attrs']['width'] ?? 0 ) ? intval( $block['attrs']['width'] ) : 0;
$block_width = floor( $parent_width * ( $block_width_percent / 100 ) );
// If width is not set, we assume it's 25% of the parent width.
$total_used_width += $block_width ? $block_width : floor( $parent_width * ( 25 / 100 ) );
if ( ! $block_width ) {
$inner_blocks[ $key ]['email_attrs']['layout_width'] = null; // Will be rendered as auto.
continue;
}
$inner_blocks[ $key ]['email_attrs']['layout_width'] = $this->get_width_without_gap( $block_width, $flex_gap, $block_width_percent ) . 'px';
}
// When there is only one block, or percentage is set reasonably we don't need to adjust and just render as set by user.
if ( $blocks_count <= 1 || ( $total_used_width <= $parent_width ) ) {
return $inner_blocks;
}
foreach ( $inner_blocks as $key => $block ) {
$proportional_space_overflow = $parent_width / $total_used_width;
$block_width = $block['email_attrs']['layout_width'] ? $settings_controller->parse_number_from_string_with_pixels( $block['email_attrs']['layout_width'] ) : 0;
$block_proportional_width = $block_width * $proportional_space_overflow;
$block_proportional_percentage = ( $block_proportional_width / $parent_width ) * 100;
$inner_blocks[ $key ]['email_attrs']['layout_width'] = $block_width ? $this->get_width_without_gap( $block_proportional_width, $flex_gap, $block_proportional_percentage ) . 'px' : null;
}
return $inner_blocks;
}
private function get_width_without_gap( float $block_width, float $flex_gap, float $block_width_percent ): int {
$width_gap_reduction = $flex_gap * ( ( 100 - $block_width_percent ) / 100 );
return intval( floor( $block_width - $width_gap_reduction ) );
}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors;
if (!defined('ABSPATH')) exit;
class Highlighting_Postprocessor implements Postprocessor {
public function postprocess( string $html ): string {
return str_replace(
array( '<mark', '</mark>' ),
array( '<span', '</span>' ),
$html
);
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Theme_Controller;
class Variables_Postprocessor implements Postprocessor {
private Theme_Controller $theme_controller;
public function __construct(
Theme_Controller $theme_controller
) {
$this->theme_controller = $theme_controller;
}
public function postprocess( string $html ): string {
$variables = $this->theme_controller->get_variables_values_map();
$replacements = array();
foreach ( $variables as $name => $value ) {
$var_pattern = '/' . preg_quote( 'var(' . $name . ')', '/' ) . '/i';
$replacements[ $var_pattern ] = $value;
}
// Pattern to match style attributes and their values.
$callback = function ( $matches ) use ( $replacements ) {
// For each match, replace CSS variables with their values.
$style = $matches[1];
$style = preg_replace( array_keys( $replacements ), array_values( $replacements ), $style );
return 'style="' . esc_attr( $style ) . '"';
};
// We want to replace the CSS variables only in the style attributes to avoid replacing the actual content.
$style_pattern = '/style="(.*?)"/i';
$style_pattern_alt = "/style='(.*?)'/i";
$html = (string) preg_replace_callback( $style_pattern, $callback, $html );
$html = (string) preg_replace_callback( $style_pattern_alt, $callback, $html );
return $html;
}
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors;
if (!defined('ABSPATH')) exit;
interface Postprocessor {
public function postprocess( string $html ): string;
}
@@ -0,0 +1,91 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
if (!defined('ABSPATH')) exit;
class Blocks_Width_Preprocessor implements Preprocessor {
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
foreach ( $parsed_blocks as $key => $block ) {
// Layout width is recalculated for each block because full-width blocks don't exclude padding.
$layout_width = $this->parse_number_from_string_with_pixels( $layout['contentSize'] );
$alignment = $block['attrs']['align'] ?? null;
// Subtract padding from the block width if it's not full-width.
if ( 'full' !== $alignment ) {
$layout_width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['left'] ?? '0px' );
$layout_width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['right'] ?? '0px' );
}
$width_input = $block['attrs']['width'] ?? '100%';
// Currently we support only % and px units in case only the number is provided we assume it's %
// because editor saves percent values as a number.
$width_input = is_numeric( $width_input ) ? "$width_input%" : $width_input;
$width = $this->convert_width_to_pixels( $width_input, $layout_width );
if ( 'core/columns' === $block['blockName'] ) {
// Calculate width of the columns based on the layout width and padding.
$columns_width = $layout_width;
$columns_width -= $this->parse_number_from_string_with_pixels( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$columns_width -= $this->parse_number_from_string_with_pixels( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$border_width = $block['attrs']['style']['border']['width'] ?? '0px';
$columns_width -= $this->parse_number_from_string_with_pixels( $block['attrs']['style']['border']['left']['width'] ?? $border_width );
$columns_width -= $this->parse_number_from_string_with_pixels( $block['attrs']['style']['border']['right']['width'] ?? $border_width );
$block['innerBlocks'] = $this->add_missing_column_widths( $block['innerBlocks'], $columns_width );
}
// Copy layout styles and update width and padding.
$modified_layout = $layout;
$modified_layout['contentSize'] = "{$width}px";
$modified_styles = $styles;
$modified_styles['spacing']['padding']['left'] = $block['attrs']['style']['spacing']['padding']['left'] ?? '0px';
$modified_styles['spacing']['padding']['right'] = $block['attrs']['style']['spacing']['padding']['right'] ?? '0px';
$block['email_attrs']['width'] = "{$width}px";
$block['innerBlocks'] = $this->preprocess( $block['innerBlocks'], $modified_layout, $modified_styles );
$parsed_blocks[ $key ] = $block;
}
return $parsed_blocks;
}
// TODO: We could add support for other units like em, rem, etc.
private function convert_width_to_pixels( string $current_width, float $layout_width ): float {
$width = $layout_width;
if ( strpos( $current_width, '%' ) !== false ) {
$width = (float) str_replace( '%', '', $current_width );
$width = round( $width / 100 * $layout_width );
} elseif ( strpos( $current_width, 'px' ) !== false ) {
$width = $this->parse_number_from_string_with_pixels( $current_width );
}
return $width;
}
private function parse_number_from_string_with_pixels( string $value ): float {
return (float) str_replace( 'px', '', $value );
}
private function add_missing_column_widths( array $columns, float $columns_width ): array {
$columns_count_with_defined_width = 0;
$defined_column_width = 0;
$columns_count = count( $columns );
foreach ( $columns as $column ) {
if ( isset( $column['attrs']['width'] ) && ! empty( $column['attrs']['width'] ) ) {
++$columns_count_with_defined_width;
$defined_column_width += $this->convert_width_to_pixels( $column['attrs']['width'], $columns_width );
} else {
// When width is not set we need to add padding to the defined column width for better ratio accuracy.
$defined_column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$defined_column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$border_width = $column['attrs']['style']['border']['width'] ?? '0px';
$defined_column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['border']['left']['width'] ?? $border_width );
$defined_column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['border']['right']['width'] ?? $border_width );
}
}
if ( $columns_count - $columns_count_with_defined_width > 0 ) {
$default_columns_width = round( ( $columns_width - $defined_column_width ) / ( $columns_count - $columns_count_with_defined_width ), 2 );
foreach ( $columns as $key => $column ) {
if ( ! isset( $column['attrs']['width'] ) || empty( $column['attrs']['width'] ) ) {
// Add padding to the specific column width because it's not included in the default width.
$column_width = $default_columns_width;
$column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['spacing']['padding']['left'] ?? '0px' );
$column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['spacing']['padding']['right'] ?? '0px' );
$border_width = $column['attrs']['style']['border']['width'] ?? '0px';
$column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['border']['left']['width'] ?? $border_width );
$column_width += $this->parse_number_from_string_with_pixels( $column['attrs']['style']['border']['right']['width'] ?? $border_width );
$columns[ $key ]['attrs']['width'] = "{$column_width}px";
}
}
}
return $columns;
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
if (!defined('ABSPATH')) exit;
class Cleanup_Preprocessor implements Preprocessor {
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
foreach ( $parsed_blocks as $key => $block ) {
// https://core.trac.wordpress.org/ticket/45312
// \WP_Block_Parser::parse_blocks() sometimes add a block with name null that can cause unexpected spaces in rendered content
// This behavior was reported as an issue, but it was closed as won't fix.
if ( null === $block['blockName'] ) {
unset( $parsed_blocks[ $key ] );
}
}
return array_values( $parsed_blocks );
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
if (!defined('ABSPATH')) exit;
class Spacing_Preprocessor implements Preprocessor {
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
$parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null );
return $parsed_blocks;
}
private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null ): array {
foreach ( $parsed_blocks as $key => $block ) {
$parent_block_name = $parent_block['blockName'] ?? '';
// Ensure that email_attrs are set.
$block['email_attrs'] = $block['email_attrs'] ?? array();
if ( 0 !== $key && $gap && 'core/buttons' !== $parent_block_name ) {
$block['email_attrs']['margin-top'] = $gap;
}
$block['innerBlocks'] = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block );
$parsed_blocks[ $key ] = $block;
}
return $parsed_blocks;
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Typography_Preprocessor implements Preprocessor {
private const TYPOGRAPHY_STYLES = array(
'color',
'font-size',
'text-decoration',
);
private $settings_controller;
public function __construct(
Settings_Controller $settings_controller
) {
$this->settings_controller = $settings_controller;
}
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
foreach ( $parsed_blocks as $key => $block ) {
$block = $this->preprocess_parent( $block );
// Set defaults from theme - this needs to be done on top level blocks only.
$block = $this->set_defaults_from_theme( $block );
$block['innerBlocks'] = $this->copy_typography_from_parent( $block['innerBlocks'], $block );
$parsed_blocks[ $key ] = $block;
}
return $parsed_blocks;
}
private function copy_typography_from_parent( array $children, array $parent_block ): array {
foreach ( $children as $key => $child ) {
$child = $this->preprocess_parent( $child );
$child['email_attrs'] = array_merge( $this->filterStyles( $parent_block['email_attrs'] ), $child['email_attrs'] );
$child['innerBlocks'] = $this->copy_typography_from_parent( $child['innerBlocks'] ?? array(), $child );
$children[ $key ] = $child;
}
return $children;
}
private function preprocess_parent( array $block ): array {
// Build styles that should be copied to children.
$email_attrs = array();
if ( isset( $block['attrs']['style']['color']['text'] ) ) {
$email_attrs['color'] = $block['attrs']['style']['color']['text'];
}
// In case the fontSize is set via a slug (small, medium, large, etc.) we translate it to a number
// The font size slug is set in $block['attrs']['fontSize'] and value in $block['attrs']['style']['typography']['fontSize'].
if ( isset( $block['attrs']['fontSize'] ) ) {
$block['attrs']['style']['typography']['fontSize'] = $this->settings_controller->translate_slug_to_font_size( $block['attrs']['fontSize'] );
}
// Pass font size to email_attrs.
if ( isset( $block['attrs']['style']['typography']['fontSize'] ) ) {
$email_attrs['font-size'] = $block['attrs']['style']['typography']['fontSize'];
}
if ( isset( $block['attrs']['style']['typography']['textDecoration'] ) ) {
$email_attrs['text-decoration'] = $block['attrs']['style']['typography']['textDecoration'];
}
$block['email_attrs'] = array_merge( $email_attrs, $block['email_attrs'] ?? array() );
return $block;
}
private function filterStyles( array $styles ): array {
return array_intersect_key( $styles, array_flip( self::TYPOGRAPHY_STYLES ) );
}
private function set_defaults_from_theme( array $block ): array {
$theme_data = $this->settings_controller->get_theme()->get_data();
if ( ! ( $block['email_attrs']['color'] ?? '' ) ) {
$block['email_attrs']['color'] = $theme_data['styles']['color']['text'] ?? null;
}
if ( ! ( $block['email_attrs']['font-size'] ?? '' ) ) {
$block['email_attrs']['font-size'] = $theme_data['styles']['typography']['fontSize'];
}
return $block;
}
}
@@ -0,0 +1,7 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors;
if (!defined('ABSPATH')) exit;
interface Preprocessor {
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array;
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
interface Block_Renderer {
public function render( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string;
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
if (!defined('ABSPATH')) exit;
use WP_Block_Parser;
class Blocks_Parser extends WP_Block_Parser {
public $output;
public function parse( $document ) {
parent::parse( $document );
return apply_filters( 'mailpoet_blocks_renderer_parsed_blocks', $this->output );
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
if (!defined('ABSPATH')) exit;
class Blocks_Registry {
private $fallback_renderer = null;
private array $block_renderers_map = array();
public function add_block_renderer( string $block_name, Block_Renderer $renderer ): void {
$this->block_renderers_map[ $block_name ] = $renderer;
}
public function add_fallback_renderer( Block_Renderer $renderer ): void {
$this->fallback_renderer = $renderer;
}
public function has_block_renderer( string $block_name ): bool {
return isset( $this->block_renderers_map[ $block_name ] );
}
public function get_block_renderer( string $block_name ): ?Block_Renderer {
return $this->block_renderers_map[ $block_name ] ?? null;
}
public function get_fallback_renderer(): ?Block_Renderer {
return $this->fallback_renderer;
}
public function remove_all_block_renderers(): void {
foreach ( array_keys( $this->block_renderers_map ) as $block_name ) {
$this->remove_block_renderer( $block_name );
}
}
private function remove_block_renderer( string $block_name ): void {
unset( $this->block_renderers_map[ $block_name ] );
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\Css_Inliner;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Engine\Theme_Controller;
use WP_Block_Template;
use WP_Post;
class Content_Renderer {
private Blocks_Registry $blocks_registry;
private Process_Manager $process_manager;
private Settings_Controller $settings_controller;
private Theme_Controller $theme_controller;
const CONTENT_STYLES_FILE = 'content.css';
private Css_Inliner $css_inliner;
public function __construct(
Process_Manager $preprocess_manager,
Blocks_Registry $blocks_registry,
Settings_Controller $settings_controller,
Css_Inliner $css_inliner,
Theme_Controller $theme_controller
) {
$this->process_manager = $preprocess_manager;
$this->blocks_registry = $blocks_registry;
$this->settings_controller = $settings_controller;
$this->theme_controller = $theme_controller;
$this->css_inliner = $css_inliner;
}
private function initialize() {
add_filter( 'render_block', array( $this, 'render_block' ), 10, 2 );
add_filter( 'block_parser_class', array( $this, 'block_parser' ) );
add_filter( 'mailpoet_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) );
do_action( 'mailpoet_blocks_renderer_initialized', $this->blocks_registry );
}
public function render( WP_Post $post, WP_Block_Template $template ): string {
$this->set_template_globals( $post, $template );
$this->initialize();
$rendered_html = get_the_block_template_html();
$this->reset();
return $this->process_manager->postprocess( $this->inline_styles( $rendered_html, $post, $template ) );
}
public function block_parser() {
return 'MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Parser';
}
public function preprocess_parsed_blocks( array $parsed_blocks ): array {
return $this->process_manager->preprocess( $parsed_blocks, $this->theme_controller->get_layout_settings(), $this->theme_controller->get_styles() );
}
public function render_block( string $block_content, array $parsed_block ): string {
$renderer = $this->blocks_registry->get_block_renderer( $parsed_block['blockName'] );
if ( ! $renderer ) {
$renderer = $this->blocks_registry->get_fallback_renderer();
}
return $renderer ? $renderer->render( $block_content, $parsed_block, $this->settings_controller ) : $block_content;
}
private function set_template_globals( WP_Post $post, WP_Block_Template $template ) {
global $_wp_current_template_content, $_wp_current_template_id;
$_wp_current_template_id = $template->id;
$_wp_current_template_content = $template->content;
$GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- I have not found a better way to set the post object for the block renderer.
}
private function reset(): void {
$this->blocks_registry->remove_all_block_renderers();
remove_filter( 'render_block', array( $this, 'render_block' ) );
remove_filter( 'block_parser_class', array( $this, 'block_parser' ) );
remove_filter( 'mailpoet_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) );
}
private function inline_styles( $html, WP_Post $post, $template = null ) {
$styles = (string) file_get_contents( __DIR__ . '/' . self::CONTENT_STYLES_FILE );
$styles .= (string) file_get_contents( __DIR__ . '/../../content-shared.css' );
// Apply default contentWidth to constrained blocks.
$layout = $this->theme_controller->get_layout_settings();
$styles .= sprintf(
'
.is-layout-constrained > *:not(.alignleft):not(.alignright):not(.alignfull) {
max-width: %1$s;
margin-left: auto !important;
margin-right: auto !important;
}
.is-layout-constrained > .alignwide {
max-width: %2$s;
margin-left: auto !important;
margin-right: auto !important;
}
',
$layout['contentSize'],
$layout['wideSize']
);
// Get styles from theme.
$styles .= $this->theme_controller->get_stylesheet_for_rendering( $post, $template );
$block_support_styles = $this->theme_controller->get_stylesheet_from_context( 'block-supports', array() );
// Get styles from block-supports stylesheet. This includes rules such as layout (contentWidth) that some blocks use.
// @see https://github.com/WordPress/WordPress/blob/3c5da9c74344aaf5bf8097f2e2c6a1a781600e03/wp-includes/script-loader.php#L3134
// @internal :where is not supported by emogrifier, so we need to replace it with *.
$block_support_styles = str_replace(
':where(:not(.alignleft):not(.alignright):not(.alignfull))',
'*:not(.alignleft):not(.alignright):not(.alignfull)',
$block_support_styles
);
$block_support_styles = preg_replace(
'/group-is-layout-(\d+) >/',
'group-is-layout-$1 > tbody tr td >',
$block_support_styles
);
$styles .= $block_support_styles;
$styles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'mailpoet_email_content_renderer_styles', $styles, $post ) ) . '</style>';
return $this->css_inliner->from_html( $styles . $html )->inline_css()->render();
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer\ContentRenderer;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors\Highlighting_Postprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors\Postprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Postprocessors\Variables_Postprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Blocks_Width_Preprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Cleanup_Preprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Preprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Spacing_Preprocessor;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Typography_Preprocessor;
class Process_Manager {
private $preprocessors = array();
private $postprocessors = array();
public function __construct(
Cleanup_Preprocessor $cleanup_preprocessor,
Blocks_Width_Preprocessor $blocks_width_preprocessor,
Typography_Preprocessor $typography_preprocessor,
Spacing_Preprocessor $spacing_preprocessor,
Highlighting_Postprocessor $highlighting_postprocessor,
Variables_Postprocessor $variables_postprocessor
) {
$this->register_preprocessor( $cleanup_preprocessor );
$this->register_preprocessor( $blocks_width_preprocessor );
$this->register_preprocessor( $typography_preprocessor );
$this->register_preprocessor( $spacing_preprocessor );
$this->register_postprocessor( $highlighting_postprocessor );
$this->register_postprocessor( $variables_postprocessor );
}
public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
foreach ( $this->preprocessors as $preprocessor ) {
$parsed_blocks = $preprocessor->preprocess( $parsed_blocks, $layout, $styles );
}
return $parsed_blocks;
}
public function postprocess( string $html ): string {
foreach ( $this->postprocessors as $postprocessor ) {
$html = $postprocessor->postprocess( $html );
}
return $html;
}
public function register_preprocessor( Preprocessor $preprocessor ): void {
$this->preprocessors[] = $preprocessor;
}
public function register_postprocessor( Postprocessor $postprocessor ): void {
$this->postprocessors[] = $postprocessor;
}
}
@@ -0,0 +1,55 @@
/**
CSS reset for email clients for elements used in email content
StyleLint is disabled because some rules contain properties that linter marks as unknown (e.g. mso- prefix), but they are valid for email rendering
*/
/* stylelint-disable property-no-unknown */
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
img {
border: 0;
height: auto;
-ms-interpolation-mode: bicubic;
line-height: 100%;
max-width: 100%;
outline: none;
text-decoration: none;
}
p {
display: block;
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 0;
margin-top: 0;
}
/* Ensure border style is set when a block has a border */
.has-border-color {
border-style: solid;
}
/* We want ensure the same design for all email clients */
ul,
ol {
/* When margin attribute is set to zero, Outlook doesn't render the list properly. As a possible workaround, we can reset only margin for top and bottom */
margin-bottom: 0;
margin-top: 0;
padding: 0 0 0 40px;
}
/* Outlook was adding weird spaces around lists in some versions. Resetting vertical margin for list items solved it */
li {
margin-bottom: 0;
margin-top: 0;
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Renderer;
if (!defined('ABSPATH')) exit;
require_once __DIR__ . '/../../../vendor/autoload.php';
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Content_Renderer;
use MailPoet\EmailEditor\Engine\Templates\Templates;
use MailPoet\EmailEditor\Engine\Theme_Controller;
use Soundasleep\Html2Text;
use WP_Style_Engine;
class Renderer {
private Theme_Controller $theme_controller;
private Content_Renderer $content_renderer;
private Templates $templates;
private Css_Inliner $css_inliner;
const TEMPLATE_FILE = 'template-canvas.php';
const TEMPLATE_STYLES_FILE = 'template-canvas.css';
public function __construct(
Content_Renderer $content_renderer,
Templates $templates,
Css_Inliner $css_inliner,
Theme_Controller $theme_controller
) {
$this->content_renderer = $content_renderer;
$this->templates = $templates;
$this->theme_controller = $theme_controller;
$this->css_inliner = $css_inliner;
}
public function render( \WP_Post $post, string $subject, string $pre_header, string $language, $meta_robots = '' ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
$template_slug = get_page_template_slug( $post ) ? get_page_template_slug( $post ) : 'email-general';
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$template = $this->templates->get_block_template( $template_slug );
$email_styles = $this->theme_controller->get_styles();
$template_html = $this->content_renderer->render( $post, $template );
$layout = $this->theme_controller->get_layout_settings();
ob_start();
include self::TEMPLATE_FILE;
$rendered_template = (string) ob_get_clean();
$template_styles =
WP_Style_Engine::compile_css(
array(
'background-color' => $email_styles['color']['background'] ?? 'inherit',
'color' => $email_styles['color']['text'] ?? 'inherit',
'padding-top' => $email_styles['spacing']['padding']['top'] ?? '0px',
'padding-bottom' => $email_styles['spacing']['padding']['bottom'] ?? '0px',
'padding-left' => $email_styles['spacing']['padding']['left'] ?? '0px',
'padding-right' => $email_styles['spacing']['padding']['right'] ?? '0px',
'font-family' => $email_styles['typography']['fontFamily'] ?? 'inherit',
'line-height' => $email_styles['typography']['lineHeight'] ?? '1.5',
'font-size' => $email_styles['typography']['fontSize'] ?? 'inherit',
),
'body, .email_layout_wrapper'
);
$template_styles .= '.email_layout_wrapper { box-sizing: border-box;}';
$template_styles .= file_get_contents( __DIR__ . '/' . self::TEMPLATE_STYLES_FILE );
$template_styles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'mailpoet_email_renderer_styles', $template_styles, $post ) ) . '</style>';
$rendered_template = $this->inline_css_styles( $template_styles . $rendered_template );
// This is a workaround to support link :hover in some clients. Ideally we would remove the ability to set :hover
// however this is not possible using the color panel from Gutenberg.
if ( isset( $email_styles['elements']['link'][':hover']['color']['text'] ) ) {
$rendered_template = str_replace( '<!-- Forced Styles -->', '<style>a:hover { color: ' . esc_attr( $email_styles['elements']['link'][':hover']['color']['text'] ) . ' !important; }</style>', $rendered_template );
}
return array(
'html' => $rendered_template,
'text' => $this->render_text_version( $rendered_template ),
);
}
private function inline_css_styles( $template ) {
return $this->css_inliner->from_html( $template )->inline_css()->render();
}
private function render_text_version( $template ) {
$template = ( mb_detect_encoding( $template, 'UTF-8', true ) ) ? $template : mb_convert_encoding( $template, 'UTF-8', mb_list_encodings() );
$result = Html2Text::convert( $template );
if ( ! $result ) {
return '';
}
return $result;
}
}
@@ -0,0 +1,8 @@
<?php
namespace MailPoet\EmailEditor\Engine\Renderer;
if (!defined('ABSPATH')) exit;
interface Css_Inliner {
public function from_html( string $unprocessed_html ): self;
public function inline_css( string $css = '' ): self;
public function render(): string;
}
@@ -0,0 +1,91 @@
/* Base CSS rules to be applied to all emails */
/* Created based on original MailPoet template for rendering emails */
/* StyleLint is disabled because some rules contain properties that linter marks as unknown (e.g. mso- prefix), but they are valid for email rendering */
/* stylelint-disable property-no-unknown */
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
-ms-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
word-spacing: normal;
}
a {
text-decoration: none;
}
.email_layout_wrapper {
margin: 0 auto;
width: 100%;
}
.email_content_wrapper {
direction: ltr;
font-size: inherit;
text-align: left;
}
.email_footer {
direction: ltr;
text-align: center;
}
/* https://www.emailonacid.com/blog/article/email-development/tips-for-coding-email-preheaders */
.email_preheader,
.email_preheader * {
color: #fff;
display: none;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
mso-hide: all;
opacity: 0;
overflow: hidden;
-webkit-text-size-adjust: none;
visibility: hidden;
}
@media screen and (max-width: 660px) {
.email-block-column-content {
max-width: 100% !important;
}
.block {
display: block;
width: 100% !important;
}
/* Ensure proper width of columns on mobile when we set 100% and a border is set */
.email-block-column {
box-sizing: border-box;
}
/* We set width to some tables e.g. for wrappers of horizontally aligned images and we force width 100% on mobile */
.email-table-with-width {
width: 100% !important;
}
/* Flex Layout */
.layout-flex-wrapper,
.layout-flex-wrapper tbody,
.layout-flex-wrapper tr {
display: block !important;
width: 100% !important;
}
.layout-flex-item {
display: block !important;
padding-bottom: 8px !important; /* Half of the flex gap between blocks */
padding-left: 0 !important;
width: 100% !important;
}
.layout-flex-item table,
.layout-flex-item td {
box-sizing: border-box !important;
display: block !important;
width: 100% !important;
}
/* Flex Layout End */
}
/* stylelint-enable property-no-unknown */
@@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
if (!defined('ABSPATH')) exit;
// phpcs:disable Generic.Files.InlineHTML.Found
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<title><?php echo esc_html( $subject ); // @phpstan-ignore-line ?></title>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="format-detection" content="telephone=no" />
<?php echo $meta_robots; // @phpstan-ignore-line HTML defined by MailPoet--do not escape. ?>
<!-- Forced Styles -->
</head>
<body>
<!--[if mso | IE]><table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" width="<?php echo esc_attr( $layout['contentSize'] ); // @phpstan-ignore-line ?>" style="width:<?php echo esc_attr( $layout['contentSize'] ); // @phpstan-ignore-line ?>"><tr><td><![endif]-->
<div class="email_layout_wrapper" style="max-width: <?php echo esc_attr( $layout['contentSize'] ); // @phpstan-ignore-line ?>">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td class="email_preheader" height="1">
<?php echo esc_html( wp_strip_all_tags( $pre_header ) ); // @phpstan-ignore-line ?>
</td>
</tr>
<tr>
<td class="email_content_wrapper">
<?php echo $template_html; // @phpstan-ignore-line ?>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</body>
</html>
@@ -0,0 +1,87 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine\Templates;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Validator\Builder;
use WP_Block_Template;
class Templates {
private string $template_prefix = 'mailpoet';
private array $post_types = array();
private string $template_directory = __DIR__ . DIRECTORY_SEPARATOR;
public function initialize( array $post_types ): void {
$this->post_types = $post_types;
add_filter( 'theme_templates', array( $this, 'add_theme_templates' ), 10, 4 ); // Workaround needed when saving post template association.
$this->register_templates();
$this->register_post_types_to_api();
}
public function get_block_template( $template_slug ) {
// Template id is always prefixed by active theme and get_stylesheet returns the active theme slug.
$template_id = get_stylesheet() . '//' . $template_slug;
return get_block_template( $template_id );
}
private function register_templates(): void {
// The function was added in WordPress 6.7. We can remove this check after we drop support for WordPress 6.6.
if ( ! function_exists( 'register_block_template' ) ) {
return;
}
// Register basic blank template.
$general_email = array(
'title' => __( 'General Email', 'mailpoet' ),
'description' => __( 'A general template for emails.', 'mailpoet' ),
'slug' => 'email-general',
);
$template_filename = $general_email['slug'] . '.html';
$template_name = $this->template_prefix . '//' . $general_email['slug'];
if ( ! \WP_Block_Templates_Registry::get_instance()->is_registered( $template_name ) ) {
// skip registration if the template was already registered.
register_block_template(
$template_name,
array(
'title' => $general_email['title'],
'description' => $general_email['description'],
'content' => (string) file_get_contents( $this->template_directory . $template_filename ),
'post_types' => $this->post_types,
)
);
}
do_action( 'mailpoet_email_editor_register_templates' );
}
public function register_post_types_to_api(): void {
$controller = new \WP_REST_Templates_Controller( 'wp_template' );
$schema = $controller->get_item_schema();
// Future compatibility check if the post_types property is already registered.
if ( isset( $schema['properties']['post_types'] ) ) {
return;
}
register_rest_field(
'wp_template',
'post_types',
array(
'get_callback' => array( $this, 'get_post_types' ),
'update_callback' => null,
'schema' => Builder::string()->to_array(),
)
);
}
public function get_post_types( $response_object ): array {
if ( isset( $response_object['plugin'] ) && $response_object['plugin'] === $this->template_prefix ) {
return $this->post_types;
}
return $response_object['post_types'] ?? array();
}
public function add_theme_templates( $templates, $theme, $post, $post_type ) {
if ( $post_type && ! in_array( $post_type, $this->post_types, true ) ) {
return $templates;
}
$block_templates = get_block_templates();
foreach ( $block_templates as $block_template ) {
// Ideally we could check for supported post_types but there seems to be a bug and once a template has some edits and is stored in DB
// the core returns null for post_types.
if ( $block_template->plugin !== $this->template_prefix ) {
continue;
}
$templates[ $block_template->slug ] = $block_template;
}
return $templates;
}
}
@@ -0,0 +1,2 @@
<!-- wp:core/post-content {"lock":{"move":true,"remove":true},"layout":{"type":"default"}} /-->
<!-- wp:mailpoet/powered-by-mailpoet {"lock":{"move":true,"remove":true}} /-->
@@ -0,0 +1,6 @@
<?php
if (!defined('ABSPATH')) exit;
// get the rendered post HTML content.
$template_html = apply_filters( 'mailpoet_email_editor_preview_post_template_html', get_post() );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $template_html;
@@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
class Dependency_Check {
public const MIN_WP_VERSION = '6.7';
public function are_dependencies_met(): bool {
if ( ! $this->is_wp_version_compatible() ) {
return false;
}
return true;
}
private function is_wp_version_compatible(): bool {
return version_compare( get_bloginfo( 'version' ), self::MIN_WP_VERSION, '>=' );
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\PersonalizationTags\Personalization_Tag;
use MailPoet\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
use MailPoet\EmailEditor\Validator\Builder;
use WP_Post;
use WP_REST_Request;
use WP_REST_Response;
class Email_Api_Controller {
private Personalization_Tags_Registry $personalization_tags_registry;
public function __construct( Personalization_Tags_Registry $personalization_tags_registry ) {
$this->personalization_tags_registry = $personalization_tags_registry;
}
public function get_email_data(): array {
// Here comes code getting Email specific data that will be passed on 'email_data' attribute.
return array();
}
public function save_email_data( array $data, WP_Post $email_post ): void {
// Here comes code saving of Email specific data that will be passed on 'email_data' attribute.
}
public function send_preview_email_data( WP_REST_Request $request ): WP_REST_Response {
$data = $request->get_params();
try {
$result = apply_filters( 'mailpoet_email_editor_send_preview_email', $data );
return new WP_REST_Response(
array(
'success' => (bool) $result,
'result' => $result,
),
$result ? 200 : 400
);
} catch ( \Exception $exception ) {
return new WP_REST_Response( array( 'error' => $exception->getMessage() ), 400 );
}
}
public function get_personalization_tags(): WP_REST_Response {
$tags = $this->personalization_tags_registry->get_all();
return new WP_REST_Response(
array(
'success' => true,
'result' => array_values(
array_map(
function ( Personalization_Tag $tag ) {
return array(
'name' => $tag->get_name(),
'token' => $tag->get_token(),
'category' => $tag->get_category(),
'attributes' => $tag->get_attributes(),
'valueToInsert' => $tag->get_value_to_insert(),
);
},
$tags
),
),
),
200
);
}
public function get_email_data_schema(): array {
return Builder::object()->to_array();
}
}
@@ -0,0 +1,167 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Patterns\Patterns;
use MailPoet\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
use MailPoet\EmailEditor\Engine\Templates\Templates;
use WP_Post;
use WP_Theme_JSON;
class Email_Editor {
public const MAILPOET_EMAIL_META_THEME_TYPE = 'mailpoet_email_theme';
private Email_Api_Controller $email_api_controller;
private Templates $templates;
private Patterns $patterns;
private Send_Preview_Email $send_preview_email;
private Personalization_Tags_Registry $personalization_tags_registry;
public function __construct(
Email_Api_Controller $email_api_controller,
Templates $templates,
Patterns $patterns,
Send_Preview_Email $send_preview_email,
Personalization_Tags_Registry $personalization_tags_controller
) {
$this->email_api_controller = $email_api_controller;
$this->templates = $templates;
$this->patterns = $patterns;
$this->send_preview_email = $send_preview_email;
$this->personalization_tags_registry = $personalization_tags_controller;
}
public function initialize(): void {
do_action( 'mailpoet_email_editor_initialized' );
add_filter( 'mailpoet_email_editor_rendering_theme_styles', array( $this, 'extend_email_theme_styles' ), 10, 2 );
$this->register_block_patterns();
$this->register_email_post_types();
$this->register_block_templates();
$this->register_email_post_send_status();
$this->register_personalization_tags();
$is_editor_page = apply_filters( 'mailpoet_is_email_editor_page', false );
if ( $is_editor_page ) {
$this->extend_email_post_api();
}
add_action( 'rest_api_init', array( $this, 'register_email_editor_api_routes' ) );
add_filter( 'mailpoet_email_editor_send_preview_email', array( $this->send_preview_email, 'send_preview_email' ), 11, 1 ); // allow for other filter methods to take precedent.
add_filter( 'single_template', array( $this, 'load_email_preview_template' ) );
}
private function register_block_templates(): void {
// Since we cannot currently disable blocks in the editor for specific templates, disable templates when viewing site editor. @see https://github.com/WordPress/gutenberg/issues/41062.
if ( strstr( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ), 'site-editor.php' ) === false ) {
$post_types = array_column( $this->get_post_types(), 'name' );
$this->templates->initialize( $post_types );
}
}
private function register_block_patterns(): void {
$this->patterns->initialize();
}
private function register_email_post_types(): void {
foreach ( $this->get_post_types() as $post_type ) {
register_post_type(
$post_type['name'],
array_merge( $this->get_default_email_post_args(), $post_type['args'] )
);
}
}
private function register_personalization_tags(): void {
$this->personalization_tags_registry->initialize();
}
private function get_post_types(): array {
$post_types = array();
return apply_filters( 'mailpoet_email_editor_post_types', $post_types );
}
private function get_default_email_post_args(): array {
return array(
'public' => false,
'hierarchical' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'supports' => array( 'editor', 'title', 'custom-fields' ), // 'custom-fields' is required for loading meta fields via API.
'has_archive' => true,
'show_in_rest' => true, // Important to enable Gutenberg editor.
'default_rendering_mode' => 'template-locked',
'publicly_queryable' => true, // required by the preview in new tab feature.
);
}
private function register_email_post_send_status(): void {
register_post_status(
'sent',
array(
'public' => false,
'exclude_from_search' => true,
'internal' => true, // for now, we hide it, if we use the status in the listings we may flip this and following values.
'show_in_admin_all_list' => false,
'show_in_admin_status_list' => false,
)
);
}
public function extend_email_post_api() {
$email_post_types = array_column( $this->get_post_types(), 'name' );
register_rest_field(
$email_post_types,
'email_data',
array(
'get_callback' => array( $this->email_api_controller, 'get_email_data' ),
'update_callback' => array( $this->email_api_controller, 'save_email_data' ),
'schema' => $this->email_api_controller->get_email_data_schema(),
)
);
}
public function register_email_editor_api_routes() {
register_rest_route(
'mailpoet-email-editor/v1',
'/send_preview_email',
array(
'methods' => 'POST',
'callback' => array( $this->email_api_controller, 'send_preview_email_data' ),
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
)
);
register_rest_route(
'mailpoet-email-editor/v1',
'/get_personalization_tags',
array(
'methods' => 'GET',
'callback' => array( $this->email_api_controller, 'get_personalization_tags' ),
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
)
);
}
public function extend_email_theme_styles( WP_Theme_JSON $theme, WP_Post $post ): WP_Theme_JSON {
$email_theme = get_post_meta( $post->ID, self::MAILPOET_EMAIL_META_THEME_TYPE, true );
if ( $email_theme && is_array( $email_theme ) ) {
$theme->merge( new WP_Theme_JSON( $email_theme ) );
}
return $theme;
}
public function get_current_post() {
if ( isset( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_post = get_post( intval( $_GET['post'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- data valid
} else {
$current_post = $GLOBALS['post'];
}
return $current_post;
}
public function load_email_preview_template( string $template ): string {
$post = $this->get_current_post();
if ( ! $post instanceof \WP_Post ) {
return $template;
}
$current_post_type = $post->post_type;
$email_post_types = array_column( $this->get_post_types(), 'name' );
if ( ! in_array( $current_post_type, $email_post_types, true ) ) {
return $template;
}
add_filter(
'mailpoet_email_editor_preview_post_template_html',
function () use ( $post ) {
// Generate HTML content for email editor post.
return $this->send_preview_email->render_html( $post );
}
);
return __DIR__ . '/Templates/single-email-post-template.php';
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Validator\Builder;
class Email_Styles_Schema {
public function get_schema(): array {
$typography_props = Builder::object(
array(
'fontFamily' => Builder::string()->nullable(),
'fontSize' => Builder::string()->nullable(),
'fontStyle' => Builder::string()->nullable(),
'fontWeight' => Builder::string()->nullable(),
'letterSpacing' => Builder::string()->nullable(),
'lineHeight' => Builder::string()->nullable(),
'textTransform' => Builder::string()->nullable(),
'textDecoration' => Builder::string()->nullable(),
)
)->nullable();
return Builder::object(
array(
'version' => Builder::integer(),
'styles' => Builder::object(
array(
'spacing' => Builder::object(
array(
'padding' => Builder::object(
array(
'top' => Builder::string(),
'right' => Builder::string(),
'bottom' => Builder::string(),
'left' => Builder::string(),
)
)->nullable(),
'blockGap' => Builder::string()->nullable(),
)
)->nullable(),
'color' => Builder::object(
array(
'background' => Builder::string()->nullable(),
'text' => Builder::string()->nullable(),
)
)->nullable(),
'typography' => $typography_props,
'elements' => Builder::object(
array(
'heading' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'button' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'link' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h1' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h2' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h3' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h4' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h5' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
'h6' => Builder::object(
array(
'typography' => $typography_props,
)
)->nullable(),
)
)->nullable(),
)
)->nullable(),
)
)->to_array();
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\PersonalizationTags\HTML_Tag_Processor;
use MailPoet\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
class Personalizer {
private Personalization_Tags_Registry $tags_registry;
private array $context;
public function __construct( Personalization_Tags_Registry $tags_registry ) {
$this->tags_registry = $tags_registry;
$this->context = array();
}
public function set_context( array $context ) {
$this->context = $context;
}
public function personalize_content( string $content ): string {
$content_processor = new HTML_Tag_Processor( $content );
while ( $content_processor->next_token() ) {
if ( $content_processor->get_token_type() === '#comment' ) {
$token = $this->parse_token( $content_processor->get_modifiable_text() );
$tag = $this->tags_registry->get_by_token( $token['token'] );
if ( ! $tag ) {
continue;
}
$value = $tag->execute_callback( $this->context, $token['arguments'] );
$content_processor->replace_token( $value );
} elseif ( $content_processor->get_token_type() === '#tag' && $content_processor->get_tag() === 'TITLE' ) {
// The title tag contains the subject of the email which should be personalized. HTML_Tag_Processor does parse the header tags.
$title = $this->personalize_content( $content_processor->get_modifiable_text() );
$content_processor->set_modifiable_text( $title );
} elseif ( $content_processor->get_token_type() === '#tag' && $content_processor->get_tag() === 'A' && $content_processor->get_attribute( 'data-link-href' ) ) {
// The anchor tag contains the data-link-href attribute which should be personalized.
$href = $content_processor->get_attribute( 'data-link-href' );
$token = $this->parse_token( $href );
$tag = $this->tags_registry->get_by_token( $token['token'] );
if ( ! $tag ) {
continue;
}
$value = $tag->execute_callback( $this->context, $token['arguments'] );
$value = $this->replace_link_href( $href, $tag->get_token(), $value );
if ( $value ) {
$content_processor->set_attribute( 'href', $value );
$content_processor->remove_attribute( 'data-link-href' );
$content_processor->remove_attribute( 'contenteditable' );
}
}
}
$content_processor->flush_updates();
return $content_processor->get_updated_html();
}
private function parse_token( string $token ): array {
$result = array(
'token' => '',
'arguments' => array(),
);
// Step 1: Separate the tag and attributes.
if ( preg_match( '/^\[([a-zA-Z0-9\-\/]+)\s*(.*?)\]$/', trim( $token ), $matches ) ) {
$result['token'] = "[{$matches[1]}]"; // The tag part (e.g., "[mailpoet/subscriber-firstname]").
$attributes_string = $matches[2]; // The attributes part (e.g., 'default="subscriber"').
// Step 2: Extract attributes from the attribute string.
if ( preg_match_all( '/(\w+)=["\']([^"\']+)["\']/', $attributes_string, $attribute_matches, PREG_SET_ORDER ) ) {
foreach ( $attribute_matches as $attribute ) {
$result['arguments'][ $attribute[1] ] = $attribute[2];
}
}
}
return $result;
}
private function replace_link_href( string $content, string $token, string $replacement ) {
// Escape the shortcode name for safe regex usage and strip the brackets.
$escaped_shortcode = preg_quote( substr( $token, 1, strlen( $token ) - 2 ), '/' );
// Create a regex pattern dynamically.
$pattern = '/\[' . $escaped_shortcode . '(?:\s+[^\]]+)?\]/';
return trim( (string) preg_replace( $pattern, $replacement, $content ) );
}
}
@@ -0,0 +1,76 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\Renderer;
class Send_Preview_Email {
private Renderer $renderer;
private Personalizer $personalizer;
public function __construct(
Renderer $renderer,
Personalizer $personalizer
) {
$this->renderer = $renderer;
$this->personalizer = $personalizer;
}
public function send_preview_email( $data ): bool {
if ( is_bool( $data ) ) {
// preview mail already sent. Do not process again.
return $data;
}
$this->validate_data( $data );
$email = $data['email'];
$post_id = $data['postId'];
$post = $this->fetch_post( $post_id );
$subject = $post->post_title;
$email_html_content = $this->render_html( $post );
return $this->send_email( $email, $subject, $email_html_content );
}
public function render_html( $post ): string {
$subject = $post->post_title;
$language = get_bloginfo( 'language' );
$rendered_data = $this->renderer->render(
$post,
$subject,
__( 'Preview', 'mailpoet' ),
$language
);
return $this->set_personalize_content( $rendered_data['html'] );
}
public function set_personalize_content( string $content ): string {
$current_user = wp_get_current_user();
$subscriber = ! empty( $current_user->ID ) ? $current_user : null;
$this->personalizer->set_context(
array(
'recipient_email' => $subscriber ? $subscriber->user_email : null,
'is_user_preview' => true,
)
);
return $this->personalizer->personalize_content( $content );
}
public function send_email( string $to, string $subject, string $body ): bool {
add_filter( 'wp_mail_content_type', array( $this, 'set_mail_content_type' ) );
$result = wp_mail( $to, $subject, $body );
// Reset content-type to avoid conflicts.
remove_filter( 'wp_mail_content_type', array( $this, 'set_mail_content_type' ) );
return $result;
}
public function set_mail_content_type( string $content_type ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
return 'text/html';
}
private function validate_data( array $data ) {
if ( empty( $data['email'] ) || empty( $data['postId'] ) ) {
throw new \InvalidArgumentException( esc_html__( 'Missing required data', 'mailpoet' ) );
}
if ( ! is_email( $data['email'] ) ) {
throw new \InvalidArgumentException( esc_html__( 'Invalid email', 'mailpoet' ) );
}
}
private function fetch_post( $post_id ): \WP_Post {
$post = get_post( intval( $post_id ) );
if ( ! $post instanceof \WP_Post ) {
throw new \Exception( esc_html__( 'Invalid post', 'mailpoet' ) );
}
return $post;
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
class Settings_Controller {
const ALLOWED_BLOCK_TYPES = array(
'core/button',
'core/buttons',
'core/paragraph',
'core/heading',
'core/column',
'core/columns',
'core/image',
'core/list',
'core/list-item',
'core/group',
'core/spacer',
);
const DEFAULT_SETTINGS = array(
'enableCustomUnits' => array( 'px', '%' ),
);
private Theme_Controller $theme_controller;
private array $iframe_assets = array();
public function __construct(
Theme_Controller $theme_controller
) {
$this->theme_controller = $theme_controller;
}
public function get_settings(): array {
$this->init_iframe_assets();
$core_default_settings = \get_default_block_editor_settings();
$theme_settings = $this->theme_controller->get_settings();
$settings = array_merge( $core_default_settings, self::DEFAULT_SETTINGS );
$settings['allowedBlockTypes'] = self::ALLOWED_BLOCK_TYPES;
// Assets for iframe editor (component styles, scripts, etc.).
$settings['__unstableResolvedAssets'] = $this->iframe_assets;
$editor_content_styles = file_get_contents( __DIR__ . '/content-editor.css' );
$shares_content_styles = file_get_contents( __DIR__ . '/content-shared.css' );
$settings['styles'] = array(
array( 'css' => $editor_content_styles ),
array( 'css' => $shares_content_styles ),
);
$settings['__experimentalFeatures'] = $theme_settings;
// Controls which alignment options are available for blocks.
$settings['supportsLayout'] = true; // Allow using default layouts.
$settings['__unstableIsBlockBasedTheme'] = true; // For default setting this to true disables wide and full alignments.
return $settings;
}
public function get_layout(): array {
$layout_settings = $this->theme_controller->get_layout_settings();
return array(
'contentSize' => $layout_settings['contentSize'],
'wideSize' => $layout_settings['wideSize'],
);
}
public function get_email_styles(): array {
$theme = $this->get_theme();
return $theme->get_data()['styles'];
}
public function get_layout_width_without_padding(): string {
$styles = $this->get_email_styles();
$layout = $this->get_layout();
$width = $this->parse_number_from_string_with_pixels( $layout['contentSize'] );
$width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['left'] );
$width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['right'] );
return "{$width}px";
}
public function parse_styles_to_array( string $styles ): array {
$styles = explode( ';', $styles );
$parsed_styles = array();
foreach ( $styles as $style ) {
$style = explode( ':', $style );
if ( count( $style ) === 2 ) {
$parsed_styles[ trim( $style[0] ) ] = trim( $style[1] );
}
}
return $parsed_styles;
}
public function parse_number_from_string_with_pixels( string $value ): float {
return (float) str_replace( 'px', '', $value );
}
public function get_theme(): \WP_Theme_JSON {
return $this->theme_controller->get_theme();
}
public function translate_slug_to_font_size( string $font_size ): string {
return $this->theme_controller->translate_slug_to_font_size( $font_size );
}
public function translate_slug_to_color( string $color_slug ): string {
return $this->theme_controller->translate_slug_to_color( $color_slug );
}
private function init_iframe_assets(): void {
if ( ! empty( $this->iframe_assets ) ) {
return;
}
$this->iframe_assets = _wp_get_iframed_editor_assets();
// Remove layout styles and block library for classic themes. They are added only when a classic theme is active
// and they add unwanted margins and paddings in the editor content.
$cleaned_styles = array();
foreach ( explode( "\n", (string) $this->iframe_assets['styles'] ) as $asset ) {
if ( strpos( $asset, 'wp-editor-classic-layout-styles-css' ) !== false ) {
continue;
}
if ( strpos( $asset, 'wp-block-library-theme-css' ) !== false ) {
continue;
}
$cleaned_styles[] = $asset;
}
$this->iframe_assets['styles'] = implode( "\n", $cleaned_styles );
}
}
@@ -0,0 +1,181 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use WP_Block_Template;
use WP_Post;
use WP_Theme_JSON;
use WP_Theme_JSON_Resolver;
class Theme_Controller {
private WP_Theme_JSON $core_theme;
private WP_Theme_JSON $base_theme;
private User_Theme $user_theme;
public function __construct() {
$this->core_theme = WP_Theme_JSON_Resolver::get_core_data();
$this->base_theme = new WP_Theme_JSON( (array) json_decode( (string) file_get_contents( __DIR__ . '/theme.json' ), true ), 'default' );
$this->user_theme = new User_Theme();
}
public function get_theme(): WP_Theme_JSON {
$theme = $this->get_base_theme();
$theme->merge( $this->user_theme->get_theme() );
return $theme;
}
public function get_base_theme(): WP_Theme_JSON {
$theme = new WP_Theme_JSON();
$theme->merge( $this->core_theme );
$theme->merge( $this->base_theme );
return apply_filters( 'mailpoet_email_editor_theme_json', $theme );
}
private function recursive_replace_presets( $values, $presets ) {
foreach ( $values as $key => $value ) {
if ( is_array( $value ) ) {
$values[ $key ] = $this->recursive_replace_presets( $value, $presets );
} elseif ( is_string( $value ) ) {
$values[ $key ] = preg_replace( array_keys( $presets ), array_values( $presets ), $value );
} else {
$values[ $key ] = $value;
}
}
return $values;
}
private function recursive_extract_preset_variables( $styles ) {
foreach ( $styles as $key => $style_value ) {
if ( is_array( $style_value ) ) {
$styles[ $key ] = $this->recursive_extract_preset_variables( $style_value );
} elseif ( strpos( $style_value, 'var:preset|' ) === 0 ) {
// phpcs:ignore Generic.Commenting.DocComment.MissingShort
$styles[ $key ] = 'var(--wp--' . str_replace( '|', '--', str_replace( 'var:', '', $style_value ) ) . ')';
} else {
$styles[ $key ] = $style_value;
}
}
return $styles;
}
public function get_styles(): array {
$theme_styles = $this->get_theme()->get_data()['styles'];
// Extract preset variables.
$theme_styles = $this->recursive_extract_preset_variables( $theme_styles );
// Replace preset values.
$variables = $this->get_variables_values_map();
$presets = array();
foreach ( $variables as $name => $value ) {
$pattern = '/var\(' . preg_quote( $name, '/' ) . '\)/i';
$presets[ $pattern ] = $value;
}
return $this->recursive_replace_presets( $theme_styles, $presets );
}
public function get_settings(): array {
$email_editor_theme_settings = $this->get_theme()->get_settings();
$site_theme_settings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings();
$email_editor_theme_settings['color']['palette']['theme'] = array();
if ( isset( $site_theme_settings['color']['palette']['theme'] ) ) {
$email_editor_theme_settings['color']['palette']['theme'] = $site_theme_settings['color']['palette']['theme'];
}
return $email_editor_theme_settings;
}
public function get_layout_settings(): array {
return $this->get_theme()->get_settings()['layout'];
}
public function get_stylesheet_from_context( $context, $options = array() ): string {
return function_exists( 'gutenberg_style_engine_get_stylesheet_from_context' ) ? gutenberg_style_engine_get_stylesheet_from_context( $context, $options ) : wp_style_engine_get_stylesheet_from_context( $context, $options );
}
public function get_stylesheet_for_rendering( ?WP_Post $post = null, $template = null ): string {
$email_theme_settings = $this->get_settings();
$css_presets = '';
// Font family classes.
foreach ( $email_theme_settings['typography']['fontFamilies']['default'] as $font_family ) {
$css_presets .= ".has-{$font_family['slug']}-font-family { font-family: {$font_family['fontFamily']}; } \n";
}
// Font size classes.
foreach ( $email_theme_settings['typography']['fontSizes']['default'] as $font_size ) {
$css_presets .= ".has-{$font_size['slug']}-font-size { font-size: {$font_size['size']}; } \n";
}
// Color palette classes.
$color_definitions = array_merge( $email_theme_settings['color']['palette']['theme'], $email_theme_settings['color']['palette']['default'] );
foreach ( $color_definitions as $color ) {
$css_presets .= ".has-{$color['slug']}-color { color: {$color['color']}; } \n";
$css_presets .= ".has-{$color['slug']}-background-color { background-color: {$color['color']}; } \n";
$css_presets .= ".has-{$color['slug']}-border-color { border-color: {$color['color']}; } \n";
}
// Block specific styles.
$css_blocks = '';
$blocks = $this->get_theme()->get_styles_block_nodes();
foreach ( $blocks as $block_metadata ) {
$css_blocks .= $this->get_theme()->get_styles_for_block( $block_metadata );
}
// Element specific styles.
$elements_styles = $this->get_theme()->get_raw_data()['styles']['elements'] ?? array();
// Because the section styles is not a part of the output the `get_styles_block_nodes` method, we need to get it separately.
if ( $template && $template->wp_id ) {
$template_theme = (array) get_post_meta( $template->wp_id, 'mailpoet_email_theme', true );
$template_styles = (array) ( $template_theme['styles'] ?? array() );
$template_elements = $template_styles['elements'] ?? array();
$elements_styles = array_replace_recursive( (array) $elements_styles, (array) $template_elements );
}
if ( $post ) {
$post_theme = (array) get_post_meta( $post->ID, 'mailpoet_email_theme', true );
$post_styles = (array) ( $post_theme['styles'] ?? array() );
$post_elements = $post_styles['elements'] ?? array();
$elements_styles = array_replace_recursive( (array) $elements_styles, (array) $post_elements );
}
$css_elements = '';
foreach ( $elements_styles as $key => $elements_style ) {
$selector = $key;
if ( 'button' === $key ) {
$selector = '.wp-block-button';
$css_elements .= wp_style_engine_get_styles( $elements_style, array( 'selector' => '.wp-block-button' ) )['css'];
// Add color to link element.
$css_elements .= wp_style_engine_get_styles( array( 'color' => array( 'text' => $elements_style['color']['text'] ?? '' ) ), array( 'selector' => '.wp-block-button a' ) )['css'];
continue;
}
switch ( $key ) {
case 'heading':
$selector = 'h1, h2, h3, h4, h5, h6';
break;
case 'link':
$selector = 'a:not(.button-link)';
break;
}
$css_elements .= wp_style_engine_get_styles( $elements_style, array( 'selector' => $selector ) )['css'];
}
$result = $css_presets . $css_blocks . $css_elements;
// Because font-size can by defined by the clamp() function that is not supported in the e-mail clients, we need to replace it to the value.
// Regular expression to match clamp() function and capture its max value.
$pattern = '/clamp\([^,]+,\s*[^,]+,\s*([^)]+)\)/';
// Replace clamp() with its maximum value.
$result = (string) preg_replace( $pattern, '$1', $result );
return $result;
}
public function translate_slug_to_font_size( string $font_size ): string {
$settings = $this->get_settings();
foreach ( $settings['typography']['fontSizes']['default'] as $font_size_definition ) {
if ( $font_size_definition['slug'] === $font_size ) {
return $font_size_definition['size'];
}
}
return $font_size;
}
public function translate_slug_to_color( string $color_slug ): string {
$settings = $this->get_settings();
$color_definitions = array_merge( $settings['color']['palette']['theme'], $settings['color']['palette']['default'] );
foreach ( $color_definitions as $color_definition ) {
if ( $color_definition['slug'] === $color_slug ) {
return strtolower( $color_definition['color'] );
}
}
return $color_slug;
}
public function get_variables_values_map(): array {
$variables_css = $this->get_theme()->get_stylesheet( array( 'variables' ) );
$map = array();
// Regular expression to match CSS variable definitions.
$pattern = '/--(.*?):\s*(.*?);/';
if ( preg_match_all( $pattern, $variables_css, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $match ) {
// '--' . $match[1] is the variable name, $match[2] is the variable value.
$map[ '--' . $match[1] ] = $match[2];
}
}
return $map;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace MailPoet\EmailEditor\Engine;
if (!defined('ABSPATH')) exit;
use WP_Post;
use WP_Theme_JSON;
class User_Theme {
private const USER_THEME_POST_NAME = 'wp-global-styles-mailpoet-email';
private const INITIAL_THEME_DATA = array(
'version' => 3,
'isGlobalStylesUserThemeJSON' => true,
);
private ?WP_Post $user_theme_post = null;
public function get_theme(): WP_Theme_JSON {
$post = $this->get_user_theme_post();
$theme_data = json_decode( $post->post_content, true );
if ( ! is_array( $theme_data ) ) {
$theme_data = self::INITIAL_THEME_DATA;
}
return new WP_Theme_JSON( $theme_data, 'custom' );
}
public function get_user_theme_post(): WP_Post {
$this->ensure_theme_post();
if ( ! $this->user_theme_post instanceof WP_Post ) {
throw new \Exception( 'Error creating user theme post' );
}
return $this->user_theme_post;
}
private function ensure_theme_post(): void {
if ( $this->user_theme_post ) {
return;
}
$this->user_theme_post = get_page_by_path( self::USER_THEME_POST_NAME, OBJECT, 'wp_global_styles' );
if ( $this->user_theme_post instanceof WP_Post ) {
return;
}
$post_data = array(
'post_title' => __( 'Custom Email Styles', 'mailpoet' ),
'post_name' => self::USER_THEME_POST_NAME,
'post_content' => (string) wp_json_encode( self::INITIAL_THEME_DATA, JSON_FORCE_OBJECT ),
'post_status' => 'publish',
'post_type' => 'wp_global_styles',
);
$post_id = wp_insert_post( $post_data );
if ( is_wp_error( $post_id ) ) {
throw new \Exception( 'Error creating user theme post: ' . esc_html( $post_id->get_error_message() ) );
}
$this->user_theme_post = get_post( $post_id );
}
}
@@ -0,0 +1,116 @@
/*
* Styles for the email editor.
*/
/*
* Flex layout used for buttons block for email editor.
*/
.is-layout-email-flex {
flex-wrap: nowrap;
}
:where(body .is-layout-flex) {
gap: var(--wp--style--block-gap, 16px);
}
.is-mobile-preview .is-layout-email-flex {
display: block;
}
.is-mobile-preview .is-layout-email-flex .block-editor-block-list__block {
padding: 5px 0;
width: 100%;
}
.is-mobile-preview .is-layout-email-flex .wp-block-button__link {
width: 100%;
}
/*
* Email Editor specific styles for vertical gap between blocks in column and group.
* This is needed because we disable layout for core/group, core/column and core/columns blocks, and .is-layout-flex is not applied.
*/
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column
> .wp-block:first-child,
.wp-block-group > .wp-block:first-child {
margin-top: 0;
}
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column
> .wp-block,
.wp-block-group > .wp-block {
margin-bottom: var(--wp--style--block-gap, 16px);
margin-top: var(--wp--style--block-gap, 16px);
}
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column
> .wp-block:not([aria-hidden="true"]):last-of-type,
.wp-block-group > .wp-block:not([aria-hidden="true"]):last-of-type {
margin-bottom: 0;
}
/*
* Use box sizing border box for columns that have defined a width (they have flex-basis set).
*/
.wp-block-columns:not(.is-not-stacked-on-mobile)
> .wp-block-column[style*='flex-basis'] {
box-sizing: border-box;
}
/*
* For the WYSIWYG experience we don't want to display any margins between blocks in the editor
*/
.wp-block {
clear: both; // for ensuring that floated elements (images) are cleared
}
/*
* Image block enhancements
*/
.wp-block-image figcaption {
/* Resetting the margin for images in the editor to avoid unexpected spacing */
margin: 0;
}
.wp-block-image.alignleft,
.wp-block-image.alignright {
margin-inline: 0 0;
text-align: center;
}
.wp-block-image.aligncenter {
margin-left: auto;
margin-right: auto;
}
.wp-block-image.alignright {
margin-left: auto;
}
/*
* Set default padding-left to have consistent default look in editor and in email
* This also overrides the default values in browsers for padding-inline-start
*/
ul,
ol,
ul.has-background,
ol.has-background {
padding-left: 40px;
}
/*
* Override default button border radius which is set in core to 9999px
*/
.wp-block-button__link {
border-radius: 0;
}
/*
* Mobile preview fixes
*/
.is-mobile-preview figure > div {
max-width: 100% !important;
height: auto !important;
}
@@ -0,0 +1,15 @@
/*
* Styles for both the email editor and renderer.
*/
/* Automatic padding for blocks with background color */
.email-text-block.has-background,
p.has-background,
h1.has-background,
h2.has-background,
h3.has-background,
h4.has-background,
h5.has-background,
h6.has-background {
padding: 16px 24px;
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,283 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"customGradient": false,
"defaultGradients": false,
"gradients": [],
"background": true,
"text": true,
"link": true
},
"layout": {
"contentSize": "660px",
"wideSize": "",
"allowEditing": false,
"allowCustomContentAndWideSize": false
},
"background": {
"backgroundImage": true
},
"spacing": {
"units": ["px"],
"blockGap": false,
"padding": true,
"margin": false,
"spacingSizes": [
{
"name": "1",
"size": "10px",
"slug": "10"
},
{
"name": "2",
"size": "20px",
"slug": "20"
},
{
"name": "3",
"size": "30px",
"slug": "30"
},
{
"name": "4",
"size": "40px",
"slug": "40"
},
{
"name": "5",
"size": "50px",
"slug": "50"
},
{
"name": "6",
"size": "60px",
"slug": "60"
}
]
},
"border": {
"radius": true,
"color": true,
"style": true,
"width": true
},
"typography": {
"dropCap": false,
"fontWeight": true,
"lineHeight": true,
"defaultFontSizes": true,
"fontFamilies": [
{
"name": "Arial",
"slug": "arial",
"fontFamily": "Arial, 'Helvetica Neue', Helvetica, sans-serif"
},
{
"name": "Comic Sans MS",
"slug": "comic-sans-ms",
"fontFamily": "'Comic Sans MS', 'Marker Felt-Thin', Arial, sans-serif"
},
{
"name": "Courier New",
"slug": "courier-new",
"fontFamily": "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace"
},
{
"name": "Georgia",
"slug": "georgia",
"fontFamily": "Georgia, Times, 'Times New Roman', serif"
},
{
"name": "Lucida",
"slug": "lucida",
"fontFamily": "'Lucida Sans Unicode', 'Lucida Grande', sans-serif"
},
{
"name": "Tahoma",
"slug": "tahoma",
"fontFamily": "'Tahoma, Verdana, Segoe, sans-serif'"
},
{
"name": "Times New Roman",
"slug": "times-new-roman",
"fontFamily": "'Times New Roman', Times, Baskerville, Georgia, serif"
},
{
"name": "Trebuchet MS",
"slug": "trebuchet-ms",
"fontFamily": "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif"
},
{
"name": "Verdana",
"slug": "verdana",
"fontFamily": "'Verdana, Geneva, sans-serif'"
},
{
"name": "Arvo",
"slug": "arvo",
"fontFamily": "'arvo, courier, georgia, serif'"
},
{
"name": "Lato",
"slug": "lato",
"fontFamily": "lato, 'helvetica neue', helvetica, arial, sans-serif"
},
{
"name": "Lora",
"slug": "lora",
"fontFamily": "lora, georgia, 'times new roman', serif"
},
{
"name": "Merriweather",
"slug": "merriweather",
"fontFamily": "merriweather, georgia, 'times new roman', serif"
},
{
"name": "Merriweather Sans",
"slug": "merriweather-sans",
"fontFamily": "'merriweather sans', 'helvetica neue', helvetica, arial, sans-serif"
},
{
"name": "Noticia Text",
"slug": "noticia-text",
"fontFamily": "'noticia text', georgia, 'times new roman', serif"
},
{
"name": "Open Sans",
"slug": "open-sans",
"fontFamily": "'open sans', 'helvetica neue', helvetica, arial, sans-serif"
},
{
"name": "Playfair Display",
"slug": "playfair-display",
"fontFamily": "'playfair display', georgia, 'times new roman', serif"
},
{
"name": "Roboto",
"slug": "roboto",
"fontFamily": "roboto, 'helvetica neue', helvetica, arial, sans-serif"
},
{
"name": "Source Sans Pro",
"slug": "source-sans-pro",
"fontFamily": "'source sans pro', 'helvetica neue', helvetica, arial, sans-serif"
},
{
"name": "Oswald",
"slug": "oswald",
"fontFamily": "Oswald, 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif"
},
{
"name": "Raleway",
"slug": "raleway",
"fontFamily": "Raleway, 'Century Gothic', CenturyGothic, AppleGothic, sans-serif"
},
{
"name": "Permanent Marker",
"slug": "permanent-marker",
"fontFamily": "'Permanent Marker', Tahoma, Verdana, Segoe, sans-serif"
},
{
"name": "Pacifico",
"slug": "pacifico",
"fontFamily": "Pacifico, 'Arial Narrow', Arial, sans-serif"
}
],
"fontSizes": [
{
"name": "small",
"size": "13px",
"slug": "small"
},
{
"name": "medium",
"size": "16px",
"slug": "medium"
},
{
"name": "large",
"size": "28px",
"slug": "large"
},
{
"name": "extra-large",
"size": "42px",
"slug": "x-large"
}
]
},
"useRootPaddingAwareAlignments": true
},
"styles": {
"spacing": {
"blockGap": "16px",
"padding": {
"bottom": "20px",
"left": "20px",
"right": "20px",
"top": "20px"
}
},
"color": {
"background": "#ffffff",
"text": "#1e1e1e"
},
"typography": {
"fontFamily": "Arial, 'Helvetica Neue', Helvetica, sans-serif",
"fontSize": "16px",
"fontWeight": "400",
"fontStyle": "normal",
"letterSpacing": "0",
"lineHeight": "1.5",
"textDecoration": "none",
"textTransform": "none"
},
"elements": {
"heading": {
"typography": {
"fontFamily": "Arial, 'Helvetica Neue', Helvetica, sans-serif",
"fontWeight": "400",
"fontStyle": "normal",
"lineHeight": "1.5"
},
"color": {
"text": "#000000"
}
},
"h1": {
"typography": {
"fontSize": "42px",
"fontWeight": "700",
"fontStyle": "normal"
}
},
"h2": {
"typography": {
"fontSize": "42px"
}
},
"h3": {
"typography": {
"fontSize": "28px"
}
},
"h4": {
"typography": {
"fontSize": "16px"
}
},
"h5": {
"typography": {
"fontSize": "13px"
}
},
"h6": {
"typography": {
"fontSize": "13px"
}
}
}
}
}
@@ -0,0 +1,45 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Block_Renderer;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use WP_Style_Engine;
abstract class Abstract_Block_Renderer implements Block_Renderer {
protected function get_styles_from_block( array $block_styles, $skip_convert_vars = false ) {
$styles = wp_style_engine_get_styles( $block_styles, array( 'convert_vars_to_classnames' => $skip_convert_vars ) );
return wp_parse_args(
$styles,
array(
'css' => '',
'declarations' => array(),
'classnames' => '',
)
);
}
protected function compile_css( ...$styles ): string {
return WP_Style_Engine::compile_css( array_merge( ...$styles ), '' );
}
protected function add_spacer( $content, $email_attrs ): string {
$gap_style = WP_Style_Engine::compile_css( array_intersect_key( $email_attrs, array_flip( array( 'margin-top' ) ) ), '' );
$padding_style = WP_Style_Engine::compile_css( array_intersect_key( $email_attrs, array_flip( array( 'padding-left', 'padding-right' ) ) ), '' );
if ( ! $gap_style && ! $padding_style ) {
return $content;
}
return sprintf(
'<!--[if mso | IE]><table align="left" role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%%" style="%2$s"><tr><td style="%3$s"><![endif]-->
<div class="email-block-layout" style="%2$s %3$s">%1$s</div>
<!--[if mso | IE]></td></tr></table><![endif]-->',
$content,
esc_attr( $gap_style ),
esc_attr( $padding_style )
);
}
public function render( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
return $this->add_spacer(
$this->render_content( $block_content, $parsed_block, $settings_controller ),
$parsed_block['email_attrs'] ?? array()
);
}
abstract protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string;
}
@@ -0,0 +1,95 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
class Button extends Abstract_Block_Renderer {
private function get_wrapper_styles( array $block_styles ) {
$properties = array( 'border', 'color', 'typography', 'spacing' );
$styles = $this->get_styles_from_block( array_intersect_key( $block_styles, array_flip( $properties ) ) );
return (object) array(
'css' => $this->compile_css(
$styles['declarations'],
array(
'word-break' => 'break-word',
'display' => 'block',
)
),
'classname' => $styles['classnames'],
);
}
private function get_link_styles( array $block_styles ) {
$styles = $this->get_styles_from_block(
array(
'color' => array(
'text' => $block_styles['color']['text'] ?? '',
),
'typography' => $block_styles['typography'] ?? array(),
)
);
return (object) array(
'css' => $this->compile_css( $styles['declarations'], array( 'display' => 'block' ) ),
'classname' => $styles['classnames'],
);
}
public function render( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
return $this->render_content( $block_content, $parsed_block, $settings_controller );
}
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
if ( empty( $parsed_block['innerHTML'] ) ) {
return '';
}
$dom_helper = new Dom_Document_Helper( $parsed_block['innerHTML'] );
$block_classname = $dom_helper->get_attribute_value_by_tag_name( 'div', 'class' ) ?? '';
$button_link = $dom_helper->find_element( 'a' );
if ( ! $button_link ) {
return '';
}
$button_text = $dom_helper->get_element_inner_html( $button_link ) ? $dom_helper->get_element_inner_html( $button_link ) : '';
$button_url = $button_link->getAttribute( 'href' ) ? $button_link->getAttribute( 'href' ) : '#';
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'width' => '',
'style' => array(),
'textAlign' => 'center',
'backgroundColor' => '',
'textColor' => '',
)
);
$block_styles = array_replace_recursive(
array(
'color' => array_filter(
array(
'background' => $block_attributes['backgroundColor'] ? $settings_controller->translate_slug_to_color( $block_attributes['backgroundColor'] ) : null,
'text' => $block_attributes['textColor'] ? $settings_controller->translate_slug_to_color( $block_attributes['textColor'] ) : null,
)
),
),
$block_attributes['style'] ?? array()
);
if ( ! empty( $block_styles['border'] ) && empty( $block_styles['border']['style'] ) ) {
$block_styles['border']['style'] = 'solid';
}
$wrapper_styles = $this->get_wrapper_styles( $block_styles );
$link_styles = $this->get_link_styles( $block_styles );
return sprintf(
'<table border="0" cellspacing="0" cellpadding="0" role="presentation" style="width:%1$s;">
<tr>
<td align="%2$s" valign="middle" role="presentation" class="%3$s" style="%4$s">
<a class="button-link %5$s" style="%6$s" href="%7$s" target="_blank">%8$s</a>
</td>
</tr>
</table>',
esc_attr( $block_attributes['width'] ? '100%' : 'auto' ),
esc_attr( $block_attributes['textAlign'] ),
esc_attr( $wrapper_styles->classname . ' ' . $block_classname ),
esc_attr( $wrapper_styles->css ),
esc_attr( $link_styles->classname ),
esc_attr( $link_styles->css ),
esc_url( $button_url ),
$button_text,
);
}
}
@@ -0,0 +1,23 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Renderer;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Buttons extends Abstract_Block_Renderer {
private $flex_layout_renderer;
public function __construct(
Flex_Layout_Renderer $flex_layout_renderer
) {
$this->flex_layout_renderer = $flex_layout_renderer;
}
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
// Ignore font size set on the buttons block.
// We rely on TypographyPreprocessor to set the font size on the buttons.
// Rendering font size on the wrapper causes unwanted whitespace below the buttons.
if ( isset( $parsed_block['attrs']['style']['typography']['fontSize'] ) ) {
unset( $parsed_block['attrs']['style']['typography']['fontSize'] );
}
return $this->flex_layout_renderer->render_inner_blocks_in_layout( $parsed_block, $settings_controller );
}
}
@@ -0,0 +1,80 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Column extends Abstract_Block_Renderer {
protected function add_spacer( $content, $email_attrs ): string {
return $content;
}
protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$content = '';
foreach ( $parsed_block['innerBlocks'] ?? array() as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{column_content}',
$content,
$this->get_block_wrapper( $block_content, $parsed_block, $settings_controller )
);
}
private function get_block_wrapper( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? '';
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'verticalAlignment' => 'stretch',
'width' => $settings_controller->get_layout_width_without_padding(),
'style' => array(),
)
);
// The default column alignment is `stretch to fill` which means that we need to set the background color to the main cell
// to create a feeling of a stretched column. This also needs to apply to CSS classnames which can also apply styles.
$is_stretched = empty( $block_attributes['verticalAlignment'] ) || 'stretch' === $block_attributes['verticalAlignment'];
$padding_css = $this->get_styles_from_block( array( 'spacing' => array( 'padding' => $block_attributes['style']['spacing']['padding'] ?? array() ) ) )['css'];
$cell_styles = $this->get_styles_from_block(
array(
'color' => $block_attributes['style']['color'] ?? array(),
'background' => $block_attributes['style']['background'] ?? array(),
)
)['declarations'];
$border_styles = $this->get_styles_from_block( array( 'border' => $block_attributes['style']['border'] ?? array() ) )['declarations'];
if ( ! empty( $border_styles ) ) {
$cell_styles = array_merge( $cell_styles, array( 'border-style' => 'solid' ), $border_styles );
}
if ( ! empty( $cell_styles['background-image'] ) && empty( $cell_styles['background-size'] ) ) {
$cell_styles['background-size'] = 'cover';
}
$wrapper_classname = 'block wp-block-column email-block-column';
$content_classname = 'email-block-column-content';
$wrapper_css = WP_Style_Engine::compile_css(
array(
'vertical-align' => $is_stretched ? 'top' : $block_attributes['verticalAlignment'],
),
''
);
$content_css = 'vertical-align: top;';
if ( $is_stretched ) {
$wrapper_classname .= ' ' . $original_wrapper_classname;
$wrapper_css .= ' ' . WP_Style_Engine::compile_css( $cell_styles, '' );
} else {
$content_classname .= ' ' . $original_wrapper_classname;
$content_css .= ' ' . WP_Style_Engine::compile_css( $cell_styles, '' );
}
return '
<td class="' . esc_attr( $wrapper_classname ) . '" style="' . esc_attr( $wrapper_css ) . '" width="' . esc_attr( $block_attributes['width'] ) . '">
<table class="' . esc_attr( $content_classname ) . '" style="' . esc_attr( $content_css ) . '" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td align="left" style="text-align:left;' . esc_attr( $padding_css ) . '">
{column_content}
</td>
</tr>
</tbody>
</table>
</td>
';
}
}
@@ -0,0 +1,67 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Columns extends Abstract_Block_Renderer {
protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$content = '';
foreach ( $parsed_block['innerBlocks'] ?? array() as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{columns_content}',
$content,
$this->getBlockWrapper( $block_content, $parsed_block, $settings_controller )
);
}
private function getBlockWrapper( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? '';
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'align' => null,
'width' => $settings_controller->get_layout_width_without_padding(),
'style' => array(),
)
);
$columns_styles = $this->get_styles_from_block(
array(
'spacing' => array( 'padding' => $block_attributes['style']['spacing']['padding'] ?? array() ),
'color' => $block_attributes['style']['color'] ?? array(),
'background' => $block_attributes['style']['background'] ?? array(),
)
)['declarations'];
$border_styles = $this->get_styles_from_block( array( 'border' => $block_attributes['style']['border'] ?? array() ) )['declarations'];
if ( ! empty( $border_styles ) ) {
$columns_styles = array_merge( $columns_styles, array( 'border-style' => 'solid' ), $border_styles );
}
if ( empty( $columns_styles['background-size'] ) ) {
$columns_styles['background-size'] = 'cover';
}
$rendered_columns = '<table class="' . esc_attr( 'email-block-columns ' . $original_wrapper_classname ) . '" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr( WP_Style_Engine::compile_css( $columns_styles, '' ) ) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>{columns_content}</tr>
</tbody>
</table>';
// Margins are not supported well in outlook for tables, so wrap in another table.
$margins = $block_attributes['style']['spacing']['margin'] ?? array();
if ( ! empty( $margins ) ) {
$margin_to_padding_styles = $this->get_styles_from_block(
array(
'spacing' => array( 'margin' => $margins ),
)
)['css'];
$rendered_columns = '<table class="email-block-columns-wrapper" style="width:100%;border-collapse:separate;text-align:left;' . esc_attr( $margin_to_padding_styles ) . '" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>' . $rendered_columns . '</td>
</tr>
</tbody>
</table>';
}
return $rendered_columns;
}
}
@@ -0,0 +1,10 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Fallback extends Abstract_Block_Renderer {
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
return $block_content;
}
}
@@ -0,0 +1,73 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
use WP_Style_Engine;
class Group extends Abstract_Block_Renderer {
protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$content = '';
$inner_blocks = $parsed_block['innerBlocks'] ?? array();
foreach ( $inner_blocks as $block ) {
$content .= render_block( $block );
}
return str_replace(
'{group_content}',
$content,
$this->get_block_wrapper( $block_content, $parsed_block, $settings_controller )
);
}
private function get_block_wrapper( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$original_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? '';
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'style' => array(),
'backgroundColor' => '',
'textColor' => '',
'borderColor' => '',
'layout' => array(),
)
);
// Layout, background, borders need to be on the outer table element.
$table_styles = $this->get_styles_from_block(
array(
'color' => array_filter(
array(
'background' => $block_attributes['backgroundColor'] ? $settings_controller->translate_slug_to_color( $block_attributes['backgroundColor'] ) : null,
'text' => $block_attributes['textColor'] ? $settings_controller->translate_slug_to_color( $block_attributes['textColor'] ) : null,
'border' => $block_attributes['borderColor'] ? $settings_controller->translate_slug_to_color( $block_attributes['borderColor'] ) : null,
)
),
'background' => $block_attributes['style']['background'] ?? array(),
'border' => $block_attributes['style']['border'] ?? array(),
'spacing' => array( 'padding' => $block_attributes['style']['spacing']['margin'] ?? array() ),
)
)['declarations'];
$table_styles['border-collapse'] = 'separate'; // Needed for the border radius to work.
// Padding properties need to be added to the table cell.
$cell_styles = $this->get_styles_from_block(
array(
'spacing' => array( 'padding' => $block_attributes['style']['spacing']['padding'] ?? array() ),
)
)['declarations'];
$table_styles['background-size'] = empty( $table_styles['background-size'] ) ? 'cover' : $table_styles['background-size'];
$width = $parsed_block['email_attrs']['width'] ?? '100%';
return sprintf(
'<table class="email-block-group %3$s" style="%1$s" width="100%%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td class="email-block-group-content" style="%2$s" width="%4$s">
{group_content}
</td>
</tr>
</tbody>
</table>',
esc_attr( WP_Style_Engine::compile_css( $table_styles, '' ) ),
esc_attr( WP_Style_Engine::compile_css( $cell_styles, '' ) ),
esc_attr( $original_classname ),
esc_attr( $width ),
);
}
}
@@ -0,0 +1,251 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Integrations\Utils\Dom_Document_Helper;
class Image extends Abstract_Block_Renderer {
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$parsed_html = $this->parse_block_content( $block_content );
if ( ! $parsed_html ) {
return '';
}
$image_url = $parsed_html['imageUrl'];
$image = $parsed_html['image'];
$caption = $parsed_html['caption'];
$class = $parsed_html['class'];
$parsed_block = $this->add_image_size_when_missing( $parsed_block, $image_url, $settings_controller );
$image = $this->addImageDimensions( $image, $parsed_block, $settings_controller );
$image = $this->apply_image_border_style( $image, $parsed_block, $caption );
$image = $this->apply_rounded_style( $image, $parsed_block );
$image_with_wrapper = str_replace(
array( '{image_content}', '{caption_content}' ),
array( $image, $caption ),
$this->get_block_wrapper( $parsed_block, $settings_controller, $caption )
);
$image_with_wrapper = $this->apply_rounded_style( $image_with_wrapper, $parsed_block );
$image_with_wrapper = $this->apply_image_border_style( $image_with_wrapper, $parsed_block, $class );
return $image_with_wrapper;
}
private function apply_rounded_style( string $block_content, array $parsed_block ): string {
// Because the isn't an attribute for definition of rounded style, we have to check the class name.
if ( isset( $parsed_block['attrs']['className'] ) && strpos( $parsed_block['attrs']['className'], 'is-style-rounded' ) !== false ) {
// If the image should be in a circle, we need to set the border-radius to 9999px to make it the same as is in the editor
// This style is applied to both wrapper and the image.
$block_content = $this->remove_style_attribute_from_element(
$block_content,
array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
),
'border-radius'
);
$block_content = $this->add_style_to_element(
$block_content,
array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
),
'border-radius: 9999px;'
);
$block_content = $this->remove_style_attribute_from_element( $block_content, array( 'tag_name' => 'img' ), 'border-radius' );
$block_content = $this->add_style_to_element( $block_content, array( 'tag_name' => 'img' ), 'border-radius: 9999px;' );
}
return $block_content;
}
private function add_image_size_when_missing( array $parsed_block, string $image_url, Settings_Controller $settings_controller ): array {
if ( isset( $parsed_block['attrs']['width'] ) ) {
return $parsed_block;
}
// Can't determine any width let's go with 100%.
if ( ! isset( $parsed_block['email_attrs']['width'] ) ) {
$parsed_block['attrs']['width'] = '100%';
}
$max_width = $settings_controller->parse_number_from_string_with_pixels( $parsed_block['email_attrs']['width'] );
$image_size = wp_getimagesize( $image_url );
$image_size = $image_size ? $image_size[0] : $max_width;
$width = min( $image_size, $max_width );
$parsed_block['attrs']['width'] = "{$width}px";
return $parsed_block;
}
private function apply_image_border_style( string $block_content, array $parsed_block, string $class_name ): string {
// Getting individual border properties.
$border_styles = wp_style_engine_get_styles( array( 'border' => $parsed_block['attrs']['style']['border'] ?? array() ) );
$border_styles = $border_styles['declarations'] ?? array();
if ( ! empty( $border_styles ) ) {
$border_styles['border-style'] = 'solid';
$border_styles['box-sizing'] = 'border-box';
}
$border_element_tag = array(
'tag_name' => 'td',
'class_name' => 'email-image-cell',
);
$content_with_border_styles = $this->add_style_to_element( $block_content, $border_element_tag, \WP_Style_Engine::compile_css( $border_styles, '' ) );
// Add Border related classes to proper element. This is required for inlined border-color styles when defined via class.
$border_classes = array_filter(
explode( ' ', $class_name ),
function ( $class_name ) {
return strpos( $class_name, 'border' ) !== false;
}
);
$html = new \WP_HTML_Tag_Processor( $content_with_border_styles );
if ( $html->next_tag( $border_element_tag ) ) {
$class_name = $html->get_attribute( 'class' ) ?? '';
$border_classes[] = $class_name;
$html->set_attribute( 'class', implode( ' ', $border_classes ) );
}
return $html->get_updated_html();
}
private function addImageDimensions( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) {
// Getting height from styles and if it's set, we set the height attribute.
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$styles = $html->get_attribute( 'style' ) ?? '';
$styles = $settings_controller->parse_styles_to_array( $styles );
$height = $styles['height'] ?? null;
if ( $height && 'auto' !== $height && is_numeric( $settings_controller->parse_number_from_string_with_pixels( $height ) ) ) {
$height = $settings_controller->parse_number_from_string_with_pixels( $height );
$html->set_attribute( 'height', esc_attr( $height ) );
}
if ( isset( $parsed_block['attrs']['width'] ) ) {
$width = $settings_controller->parse_number_from_string_with_pixels( $parsed_block['attrs']['width'] );
$html->set_attribute( 'width', esc_attr( $width ) );
}
$block_content = $html->get_updated_html();
}
return $block_content;
}
private function get_caption_styles( Settings_Controller $settings_controller, array $parsed_block ): string {
$theme_data = $settings_controller->get_theme()->get_data();
$styles = array(
'text-align' => isset( $parsed_block['attrs']['align'] ) ? 'center' : 'left',
);
$styles['font-size'] = $parsed_block['email_attrs']['font-size'] ?? $theme_data['styles']['typography']['fontSize'];
return \WP_Style_Engine::compile_css( $styles, '' );
}
private function get_block_wrapper( array $parsed_block, Settings_Controller $settings_controller, ?string $caption ): string {
$styles = array(
'border-collapse' => 'collapse',
'border-spacing' => '0px',
'font-size' => '0px',
'vertical-align' => 'top',
'width' => '100%',
);
$width = $parsed_block['attrs']['width'] ?? '100%';
$wrapper_width = ( $width && '100%' !== $width ) ? $width : 'auto';
$wrapper_styles = $styles;
$wrapper_styles['width'] = $wrapper_width;
$wrapper_styles['border-collapse'] = 'separate'; // Needed because of border radius.
$caption_html = '';
if ( $caption ) {
// When the image is not aligned, the wrapper is set to 100% width due to caption that can be longer than the image.
$caption_width = isset( $parsed_block['attrs']['align'] ) ? ( $parsed_block['attrs']['width'] ?? '100%' ) : '100%';
$caption_wrapper_styles = $styles;
$caption_wrapper_styles['width'] = $caption_width;
$caption_styles = $this->get_caption_styles( $settings_controller, $parsed_block );
$caption_html = '
<table
role="presentation"
class="email-table-with-width"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr( \WP_Style_Engine::compile_css( $caption_wrapper_styles, '' ) ) . '"
width="' . esc_attr( $caption_width ) . '"
>
<tr>
<td style="' . esc_attr( $caption_styles ) . '">{caption_content}</td>
</tr>
</table>';
}
$styles['width'] = '100%';
$align = $parsed_block['attrs']['align'] ?? 'left';
return '
<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) . '"
width="100%"
>
<tr>
<td align="' . esc_attr( $align ) . '">
<table
role="presentation"
class="email-table-with-width"
border="0"
cellpadding="0"
cellspacing="0"
style="' . esc_attr( \WP_Style_Engine::compile_css( $wrapper_styles, '' ) ) . '"
width="' . esc_attr( $wrapper_width ) . '"
>
<tr>
<td class="email-image-cell">{image_content}</td>
</tr>
</table>' . $caption_html . '
</td>
</tr>
</table>
';
}
private function add_style_to_element( $block_content, array $tag, string $style ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
if ( $html->next_tag( $tag ) ) {
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$element_style = $html->get_attribute( 'style' ) ?? '';
$element_style = ! empty( $element_style ) ? ( rtrim( $element_style, ';' ) . ';' ) : ''; // Adding semicolon if it's missing.
$element_style .= $style;
$html->set_attribute( 'style', esc_attr( $element_style ) );
$block_content = $html->get_updated_html();
}
return $block_content;
}
private function remove_style_attribute_from_element( $block_content, array $tag, string $style_name ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
if ( $html->next_tag( $tag ) ) {
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$element_style = $html->get_attribute( 'style' ) ?? '';
$element_style = preg_replace( '/' . $style_name . ':(.?[0-9]+px)+;?/', '', $element_style );
$html->set_attribute( 'style', esc_attr( strval( $element_style ) ) );
$block_content = $html->get_updated_html();
}
return $block_content;
}
private function parse_block_content( string $block_content ): ?array {
// If block's image is not set, we don't need to parse the content.
if ( empty( $block_content ) ) {
return null;
}
$dom_helper = new Dom_Document_Helper( $block_content );
$figure_tag = $dom_helper->find_element( 'figure' );
if ( ! $figure_tag ) {
return null;
}
$img_tag = $dom_helper->find_element( 'img' );
if ( ! $img_tag ) {
return null;
}
$image_src = $dom_helper->get_attribute_value( $img_tag, 'src' );
$image_class = $dom_helper->get_attribute_value( $img_tag, 'class' );
$image_html = $dom_helper->get_outer_html( $img_tag );
$figcaption = $dom_helper->find_element( 'figcaption' );
$figcaption_html = $figcaption ? $dom_helper->get_outer_html( $figcaption ) : '';
$figcaption_html = str_replace( array( '<figcaption', '</figcaption>' ), array( '<span', '</span>' ), $figcaption_html );
return array(
'imageUrl' => $image_src ? $image_src : '',
'image' => $this->cleanup_image_html( $image_html ),
'caption' => $figcaption_html ? $figcaption_html : '',
'class' => $image_class ? $image_class : '',
);
}
private function cleanup_image_html( string $content_html ): string {
$html = new \WP_HTML_Tag_Processor( $content_html );
if ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) {
$html->remove_attribute( 'srcset' );
$html->remove_attribute( 'class' );
}
return $html->get_updated_html();
}
}
@@ -0,0 +1,39 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class List_Block extends Abstract_Block_Renderer {
protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
$tag_name = ( $parsed_block['attrs']['ordered'] ?? false ) ? 'ol' : 'ul';
if ( $html->next_tag( array( 'tag_name' => $tag_name ) ) ) {
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$styles = $html->get_attribute( 'style' ) ?? '';
$styles = $settings_controller->parse_styles_to_array( $styles );
// Font size.
if ( isset( $parsed_block['email_attrs']['font-size'] ) ) {
$styles['font-size'] = $parsed_block['email_attrs']['font-size'];
} else {
// Use font-size from email theme when those properties are not set.
$theme_data = $settings_controller->get_theme()->get_data();
$styles['font-size'] = $theme_data['styles']['typography']['fontSize'];
}
$html->set_attribute( 'style', esc_attr( \WP_Style_Engine::compile_css( $styles, '' ) ) );
$block_content = $html->get_updated_html();
}
$wrapper_style = \WP_Style_Engine::compile_css(
array(
'margin-top' => $parsed_block['email_attrs']['margin-top'] ?? '0px',
),
''
);
// \WP_HTML_Tag_Processor escapes the content, so we have to replace it back
$block_content = str_replace( '&#039;', "'", $block_content );
return sprintf(
'<div style="%1$s">%2$s</div>',
esc_attr( $wrapper_style ),
$block_content
);
}
}
@@ -0,0 +1,13 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class List_Item extends Abstract_Block_Renderer {
protected function add_spacer( $content, $email_attrs ): string {
return $content;
}
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
return $block_content;
}
}
@@ -0,0 +1,92 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core\Renderer\Blocks;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Settings_Controller;
class Text extends Abstract_Block_Renderer {
protected function render_content( string $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
// Do not render empty blocks.
if ( empty( trim( wp_strip_all_tags( $block_content ) ) ) ) {
return '';
}
$block_content = $this->adjustStyleAttribute( $block_content );
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'textAlign' => 'left',
'style' => array(),
)
);
$html = new \WP_HTML_Tag_Processor( $block_content );
$classes = 'email-text-block';
if ( $html->next_tag() ) {
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$block_classes = $html->get_attribute( 'class' ) ?? '';
$classes .= ' ' . $block_classes;
// remove has-background to prevent double padding applied for wrapper and inner element.
$block_classes = str_replace( 'has-background', '', $block_classes );
// remove border related classes because we handle border on wrapping table cell.
$block_classes = preg_replace( '/[a-z-]+-border-[a-z-]+/', '', $block_classes );
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$html->set_attribute( 'class', trim( $block_classes ) );
$block_content = $html->get_updated_html();
}
$block_styles = $this->get_styles_from_block(
array(
'color' => $block_attributes['style']['color'] ?? array(),
'spacing' => $block_attributes['style']['spacing'] ?? array(),
'typography' => $block_attributes['style']['typography'] ?? array(),
'border' => $block_attributes['style']['border'] ?? array(),
)
);
$styles = array(
'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices.
);
$styles['text-align'] = 'left';
if ( ! empty( $parsed_block['attrs']['textAlign'] ) ) { // in this case, textAlign needs to be one of 'left', 'center', 'right'.
$styles['text-align'] = $parsed_block['attrs']['textAlign'];
} elseif ( in_array( $parsed_block['attrs']['align'] ?? null, array( 'left', 'center', 'right' ), true ) ) {
$styles['text-align'] = $parsed_block['attrs']['align'];
}
$compiled_styles = $this->compile_css( $block_styles['declarations'], $styles );
$table_styles = 'border-collapse: separate;'; // Needed because of border radius.
return sprintf(
'<table
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
width="100%%"
style="%1$s"
>
<tr>
<td class="%2$s" style="%3$s" align="%4$s">%5$s</td>
</tr>
</table>',
esc_attr( $table_styles ),
esc_attr( $classes ),
esc_attr( $compiled_styles ),
esc_attr( $styles['text-align'] ),
$block_content
);
}
private function adjustStyleAttribute( string $block_content ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
if ( $html->next_tag() ) {
$element_style_value = $html->get_attribute( 'style' );
$element_style = isset( $element_style_value ) ? strval( $element_style_value ) : '';
// Padding may contain value like 10px or variable like var(--spacing-10).
$element_style = preg_replace( '/padding[^:]*:.?[0-9a-z-()]+;?/', '', $element_style );
// Remove border styles. We apply border styles on the wrapping table cell.
$element_style = preg_replace( '/border[^:]*:.?[0-9a-z-()#]+;?/', '', strval( $element_style ) );
// We define the font-size on the wrapper element, but we need to keep font-size definition here
// to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element.
// The value provided by WP uses clamp() function which is not supported in many email clients.
$element_style = preg_replace( '/font-size:[^;]+;?/', 'font-size: inherit;', strval( $element_style ) );
// phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$html->set_attribute( 'style', esc_attr( $element_style ) );
$block_content = $html->get_updated_html();
}
return $block_content;
}
}
@@ -0,0 +1,44 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Core;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Registry;
use MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Layout\Flex_Layout_Renderer;
class Initializer {
public function initialize(): void {
add_action( 'mailpoet_blocks_renderer_initialized', array( $this, 'register_core_blocks_renderers' ), 10, 1 );
add_filter( 'mailpoet_email_editor_theme_json', array( $this, 'adjust_theme_json' ), 10, 1 );
add_filter( 'safe_style_css', array( $this, 'allow_styles' ) );
}
public function register_core_blocks_renderers( Blocks_Registry $blocks_registry ): void {
$blocks_registry->add_block_renderer( 'core/paragraph', new Renderer\Blocks\Text() );
$blocks_registry->add_block_renderer( 'core/heading', new Renderer\Blocks\Text() );
$blocks_registry->add_block_renderer( 'core/column', new Renderer\Blocks\Column() );
$blocks_registry->add_block_renderer( 'core/columns', new Renderer\Blocks\Columns() );
$blocks_registry->add_block_renderer( 'core/list', new Renderer\Blocks\List_Block() );
$blocks_registry->add_block_renderer( 'core/list-item', new Renderer\Blocks\List_Item() );
$blocks_registry->add_block_renderer( 'core/image', new Renderer\Blocks\Image() );
$blocks_registry->add_block_renderer( 'core/buttons', new Renderer\Blocks\Buttons( new Flex_Layout_Renderer() ) );
$blocks_registry->add_block_renderer( 'core/button', new Renderer\Blocks\Button() );
$blocks_registry->add_block_renderer( 'core/group', new Renderer\Blocks\Group() );
// Render used for all other blocks.
$blocks_registry->add_fallback_renderer( new Renderer\Blocks\Fallback() );
}
public function adjust_theme_json( \WP_Theme_JSON $editor_theme_json ): \WP_Theme_JSON {
$theme_json = (string) file_get_contents( __DIR__ . '/theme.json' );
$theme_json = json_decode( $theme_json, true );
$editor_theme_json->merge( new \WP_Theme_JSON( $theme_json, 'default' ) );
return $editor_theme_json;
}
public function allow_styles( ?array $allowed_styles ): array {
// The styles can be null in some cases.
if ( ! is_array( $allowed_styles ) ) {
$allowed_styles = array();
}
$allowed_styles[] = 'display';
$allowed_styles[] = 'mso-padding-alt';
$allowed_styles[] = 'mso-font-width';
$allowed_styles[] = 'mso-text-raise';
return $allowed_styles;
}
}
@@ -0,0 +1,27 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"styles": {
"blocks": {
"core/button": {
"variations": {}
}
},
"elements": {
"button": {
"color": {
"background": "#32373c",
"text": "#ffffff"
},
"spacing": {
"padding": {
"bottom": "0.7em",
"left": "1.4em",
"right": "1.4em",
"top": "0.7em"
}
}
}
}
}
}
@@ -0,0 +1,47 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Integrations\Utils;
if (!defined('ABSPATH')) exit;
class Dom_Document_Helper {
private \DOMDocument $dom;
public function __construct( string $html_content ) {
$this->load_html( $html_content );
}
private function load_html( string $html_content ): void {
libxml_use_internal_errors( true );
$this->dom = new \DOMDocument();
if ( ! empty( $html_content ) ) {
// prefixing the content with the XML declaration to force the input encoding to UTF-8.
$this->dom->loadHTML( '<?xml encoding="UTF-8">' . $html_content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
}
libxml_clear_errors();
}
public function find_element( string $tag_name ): ?\DOMElement {
$elements = $this->dom->getElementsByTagName( $tag_name );
return $elements->item( 0 ) ? $elements->item( 0 ) : null;
}
public function get_attribute_value( \DOMElement $element, string $attribute ): string {
return $element->hasAttribute( $attribute ) ? $element->getAttribute( $attribute ) : '';
}
public function get_attribute_value_by_tag_name( string $tag_name, string $attribute ): ?string {
$element = $this->find_element( $tag_name );
if ( ! $element ) {
return null;
}
return $this->get_attribute_value( $element, $attribute );
}
public function get_outer_html( \DOMElement $element ): string {
return (string) $this->dom->saveHTML( $element );
}
public function get_element_inner_html( \DOMElement $element ): string {
$inner_html = '';
$children = $element->childNodes; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
foreach ( $children as $child ) {
if ( ! $child instanceof \DOMNode ) {
continue;
}
$inner_html .= $this->dom->saveHTML( $child );
}
return $inner_html;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,34 @@
<?php
declare( strict_types = 1 );
namespace MailPoet\EmailEditor\Validator\Schema;
if (!defined('ABSPATH')) exit;
use MailPoet\EmailEditor\Validator\Schema;
class Any_Of_Schema extends Schema {
protected $schema = array(
'anyOf' => array(),
);
public function __construct(
array $schemas
) {
foreach ( $schemas as $schema ) {
$this->schema['anyOf'][] = $schema->to_array();
}
}
public function nullable(): self {
$null = array( 'type' => 'null' );
$any_of = $this->schema['anyOf'];
$value = in_array( $null, $any_of, true ) ? $any_of : array_merge( $any_of, array( $null ) );
return $this->update_schema_property( 'anyOf', $value );
}
public function non_nullable(): self {
$null = array( 'type' => 'null' );
$any_of = $this->schema['any_of'];
$value = array_filter(
$any_of,
function ( $item ) use ( $null ) {
return $item !== $null;
}
);
return $this->update_schema_property( 'any_of', $value );
}
}

Some files were not shown because too many files have changed in this diff Show More