Commit 882b14c4 authored by Mathieu Lutfy's avatar Mathieu Lutfy Committed by Aegir user

Update mosaico to 2.0-beta4

parent 45656722
# APIv3
CiviCRM's APIv3 framework provides a way for consumers to manage data and call services. APIv3 can be called in many ways
(such as PHP, REST, and CLI). For a general introduction, see [APIv3 Intro](https://docs.civicrm.org/dev/en/latest/api/).
This extension defines a few new APIs:
* `Job.mosaico_migrate`: If you need to perform an automated migration from v1.x to v2.x, use this API to copy all
v1.x templates to v2.x.
* `Job.mosaico_purge`: If you need to perform an automated migration from v1.x to v2.x, use this API to clear out the
old v1.x templates.
* `MosaicoTemplate.*`: This API provides access to the user-configurable templates. It supports all standard CRUD
actions (`get`, `create`, `delete`etc). Its data-structure closely adheres to Mosaico's canonical storage format.
* `MosaicoBaseTemplate.get`: This API provides access to the *base templates*. A base template (such as `versafix-1`)
defines the HTML blocks that are available for drag/drop in the Mosaico palette. Note: This API is *read-only*.
To define custom templates, see the section on "Base templates".
# Base templates
A base template defines the HTML blocks that are available for drag/drop in the Mosaico palette. The standard distribution
of Mosaico includes a few base templates, such as `versafix-1`.
The upstream Mosaico project provides a tutorial on how to develop a custom base template:
https://github.com/voidlabs/mosaico/blob/master/templates/tutorial/mosaico-tutorial.md
The new template is essentially a folder with a couple standard files. Once you've developed these files, you'll need
a way to *deploy* the folder, such as:
* __Drop-in folder__: On any site, create a folder dedicated to custom base templates. By default, this is
`[civicrm.files]/mosaico_tpl`. (`[civicrm.files]` is a variable that resolves somewhere under the CMS's data
folder.) For example, if you deployed a template `foobar` on a typical Drupal 7 site, the full path to the template HTML
might be `/var/www/sites/default/files/civicrm/mosaico_tpl/foobar/template-foobar.html`. (The folder name can be
customized in "Administer => CiviMail => Mosaico Settings".)
* __Extension__: Create a CiviCRM extension and put the template in it. Use the [hook system](https://docs.civicrm.org/dev/en/latest/hooks/) to register the template via `hook_civicrm_mosaicoBaseTemplates`. For example, this snippet shows how an extension named `mymodule` can register a base-template named `foobar`:
```php
<?php
use CRM_Mymodule_ExtensionUtil as E;
function mymodule_civicrm_mosaicoBaseTemplates(&$templates) {
$templates['foobar'] = array(
'name' => 'foobar',
'title' => 'Foo Bar',
'path' => E::url('foobar/template-foobar.html'),
'thumbnail' => E::url('foobar/edres/_full.png'),
);
}
```
# Delivery
After designing a mailing, email messages are composed and delivered through FlexMailer. To programmaticaly tap into the
composition and delivery process, see the [FlexMailer developer docs](https://docs.civicrm.org/flexmailer/en/latest/).
......@@ -38,20 +38,41 @@ class CRM_Mosaico_BAO_MosaicoTemplate extends CRM_Mosaico_DAO_MosaicoTemplate {
$templatesUrl = CRM_Mosaico_Utils::getTemplatesUrl('absolute');
$templatesLocation[] = array('dir' => $templatesDir, 'url' => $templatesUrl);
$customTemplatesDir = \Civi::paths()->getPath(CRM_Core_BAO_Setting::getItem('Mosaico Preferences', 'mosaico_custom_templates_dir'));
$customTemplatesUrl = \Civi::paths()->getUrl(CRM_Core_BAO_Setting::getItem('Mosaico Preferences', 'mosaico_custom_templates_url'));
if (!is_null($customTemplatesDir) && !is_null($customTemplatesUrl)) {
if (is_dir($customTemplatesDir)) {
$templatesLocation[] = array('dir' => $customTemplatesDir, 'url' => $customTemplatesUrl);
}
}
$records = array();
foreach (glob("{$templatesDir}/*", GLOB_ONLYDIR) as $dir) {
foreach ($templatesLocation as $templateLocation) {
foreach (glob("{$templateLocation['dir']}/*", GLOB_ONLYDIR) as $dir) {
$template = basename($dir);
$templateHTML = "{$templatesUrl}/{$template}/template-{$template}.html";
$templateThumbnail = "{$templatesUrl}/{$template}/edres/_full.png";
$templateHTML = "{$templateLocation['url']}/{$template}/template-{$template}.html";
$templateThumbnail = "{$templateLocation['url']}/{$template}/edres/_full.png";
$records[] = array(
$records[$template] = array(
'name' => $template,
'title' => $template,
'thumbnail' => $templateThumbnail,
'path' => $templateHTML,
);
}
}
if (class_exists('\Civi\Core\Event\GenericHookEvent')) {
\Civi::dispatcher()->dispatch('hook_civicrm_mosaicoBaseTemplates',
\Civi\Core\Event\GenericHookEvent::create(array(
'templates' => &$records,
))
);
}
Civi::$statics[__CLASS__]['bases'] = $records;
}
......
......@@ -115,6 +115,12 @@ class CRM_Mosaico_DAO_MosaicoTemplate extends CRM_Core_DAO {
* @var longtext
*/
public $content;
/**
* FK to civicrm_msg_template.
*
* @var int unsigned
*/
public $msg_tpl_id;
/**
* class constructor
*
......@@ -124,6 +130,19 @@ class CRM_Mosaico_DAO_MosaicoTemplate extends CRM_Core_DAO {
$this->__table = 'civicrm_mosaico_template';
parent::__construct();
}
/**
* Returns foreign keys and entity references
*
* @return array
* [CRM_Core_Reference_Interface]
*/
static function getReferenceColumns() {
if (!self::$_links) {
self::$_links = static ::createReferenceColumns(__CLASS__);
self::$_links[] = new CRM_Core_Reference_Basic(self::getTableName() , 'msg_tpl_id', 'civicrm_msg_template', 'id');
}
return self::$_links;
}
/**
* Returns all the column names of this table
*
......@@ -144,7 +163,7 @@ class CRM_Mosaico_DAO_MosaicoTemplate extends CRM_Core_DAO {
'title' => ts('Title') ,
'description' => 'Title',
'maxlength' => 255,
'size' => CRM_Utils_Type::BIG,
'size' => CRM_Utils_Type::HUGE,
) ,
'base' => array(
'name' => 'base',
......@@ -172,6 +191,22 @@ class CRM_Mosaico_DAO_MosaicoTemplate extends CRM_Core_DAO {
'title' => ts('Content') ,
'description' => 'Mosaico content (JSON)',
) ,
'msg_tpl_id' => array(
'name' => 'msg_tpl_id',
'type' => CRM_Utils_Type::T_INT,
'title' => ts('message template ID') ,
'description' => 'FK to civicrm_msg_template.',
'required' => false,
'FKClassName' => 'CRM_Core_DAO_MessageTemplate',
'html' => array(
'type' => 'Select',
) ,
'pseudoconstant' => array(
'table' => 'civicrm_msg_template',
'keyColumn' => 'id',
'labelColumn' => 'msg_title',
)
) ,
);
}
return self::$_fields;
......@@ -191,6 +226,7 @@ class CRM_Mosaico_DAO_MosaicoTemplate extends CRM_Core_DAO {
'html' => 'html',
'metadata' => 'metadata',
'content' => 'content',
'msg_tpl_id' => 'msg_tpl_id',
);
}
return self::$_fieldKeys;
......
<?php
use CRM_Mosaico_ExtensionUtil as E;
/**
* Form controller class
*
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/QuickForm+Reference
*/
class CRM_Mosaico_Form_Migrate extends CRM_Core_Form {
public function buildQuickForm() {
$this->loadTemplateData();
$migrateComment = E::ts('Are you sure want to copy data from Mosaico 1.x to 2.x?') . '\n' . E::ts('This action cannot be easily undone.');
$purgeComment = E::ts('Are you sure want to purge Mosaico 1.x data?') . '\n' . E::ts('This action cannot be undone.');
$buttons = array();
$buttons[] = array(
'type' => 'submit',
'name' => ts('Copy'),
'subName' => 'migrate',
'isDefault' => TRUE,
'icon' => 'fa-copy',
'js' => array('onclick' => 'return confirm(\'' . $migrateComment . '\');'),
);
$buttons[] = array(
'type' => 'submit',
'name' => ts('Purge'),
'subName' => 'purge',
'icon' => 'fa-trash',
'isDefault' => FALSE,
'js' => array('onclick' => 'return confirm(\'' . $purgeComment . '\');'),
);
$this->addButtons($buttons);
// export form elements
$this->assign('elementNames', $this->getRenderableElementNames());
parent::buildQuickForm();
}
public function postProcess() {
$values = $this->exportValues();
if (!empty($values['_qf_Migrate_submit_migrate'])) {
$apiResult = civicrm_api3('Job', 'mosaico_migrate', array(
'check_permissions' => 1,
));
CRM_Core_Session::setStatus(E::ts('Copied %1 templates from Mosaico 1.x to 2.x.', array(
1 => $apiResult['count'],
)), '', 'success', array(
'expires' => 0,
));
}
elseif (!empty($values['_qf_Migrate_submit_purge'])) {
civicrm_api3('Job', 'mosaico_purge', array(
'check_permissions' => 1,
));
CRM_Core_Session::setStatus(E::ts('Purged invisible Mosaico 1.x data.', array()), '', 'success', array(
'expires' => 0,
));
}
else {
CRM_Core_Session::setStatus(E::ts('Unrecognized action'));
}
$this->loadTemplateData();
parent::postProcess();
}
/**
* Get the fields/elements defined in this form.
*
* @return array (string)
*/
public function getRenderableElementNames() {
// The _elements list includes some items which should not be
// auto-rendered in the loop -- such as "qfKey" and "buttons". These
// items don't have labels. We'll identify renderable by filtering on
// the 'label'.
$elementNames = array();
foreach ($this->_elements as $element) {
/** @var HTML_QuickForm_Element $element */
$label = $element->getLabel();
if (!empty($label)) {
$elementNames[] = $element->getName();
}
}
return $elementNames;
}
protected function loadTemplateData() {
$oldTemplates = CRM_Core_DAO::executeQuery('SELECT id, name, msg_tpl_id FROM civicrm_mosaico_msg_template')
->fetchAll();
$newTemplates = CRM_Core_DAO::executeQuery('SELECT id, title, msg_tpl_id FROM civicrm_mosaico_template')
->fetchAll();
$this->assign('oldTemplates', $oldTemplates);
$this->assign('newTemplates', $newTemplates);
$msgTplIds = array_filter(CRM_Utils_Array::collect('msg_tpl_id', $newTemplates), 'is_numeric');
sort($msgTplIds);
$uniqueIds = array_unique($msgTplIds);
$this->assign('msgTplWarning', count($msgTplIds) > count($uniqueIds));
}
}
......@@ -11,6 +11,8 @@ class CRM_Mosaico_Form_MosaicoAdmin extends CRM_Admin_Form_Setting {
protected $_settings = array(
'mosaico_layout' => 'Mosaico Preferences',
'mosaico_custom_templates_dir' => 'Mosaico Custom Templates Directory',
'mosaico_custom_templates_url' => 'Mosaico Custom Templates URL'
);
/**
......
......@@ -63,7 +63,7 @@ class CRM_Mosaico_Page_Editor extends CRM_Core_Page {
return array(
'imgProcessorBackend' => $this->getUrl('civicrm/mosaico/img', NULL, TRUE),
'emailProcessorBackend' => $this->getUrl('civicrm/mosaico/dl', NULL, FALSE),
'emailProcessorBackend' => 'unused-emailProcessorBackend',
'titleToken' => 'MOSAICO Responsive Email Designer',
'fileuploadConfig' => array(
'url' => $this->getUrl('civicrm/mosaico/upload', NULL, FALSE),
......
......@@ -103,6 +103,27 @@ class CRM_Mosaico_Upgrader extends CRM_Mosaico_Upgrader_Base {
return TRUE;
}
/**
* Add menu for traditional mailing.
*/
public function upgrade_4704() {
$this->ctx->log->info('Applying update 4704');
CRM_Core_DAO::executeQuery('
ALTER TABLE civicrm_mosaico_template
ADD COLUMN `msg_tpl_id` int unsigned NULL COMMENT \'FK to civicrm_msg_template.\'
');
CRM_Core_DAO::executeQuery('
ALTER TABLE civicrm_mosaico_template
ADD CONSTRAINT FK_civicrm_mosaico_template_msg_tpl_id
FOREIGN KEY (`msg_tpl_id`) REFERENCES `civicrm_msg_template`(`id`)
ON DELETE SET NULL
');
return TRUE;
}
/**
* Example: Run an external SQL script.
*
......
<?php
// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
use CRM_Mosaico_ExtensionUtil as E;
/**
* Base class which provides helpers to execute upgrade logic
......@@ -32,6 +33,12 @@ class CRM_Mosaico_Upgrader_Base {
*/
private $revisions;
/**
* @var boolean
* Flag to clean up extension revision data in civicrm_setting
*/
private $revisionStorageIsDeprecated = FALSE;
/**
* Obtain a reference to the active upgrade handler.
*/
......@@ -91,7 +98,6 @@ class CRM_Mosaico_Upgrader_Base {
* @return bool
*/
protected static function executeCustomDataFileByAbsPath($xml_file) {
require_once 'CRM/Utils/Migrate/Import.php';
$import = new CRM_Utils_Migrate_Import();
$import->run($xml_file);
return TRUE;
......@@ -107,7 +113,26 @@ class CRM_Mosaico_Upgrader_Base {
public function executeSqlFile($relativePath) {
CRM_Utils_File::sourceSQLFile(
CIVICRM_DSN,
$this->extensionDir . '/' . $relativePath
$this->extensionDir . DIRECTORY_SEPARATOR . $relativePath
);
return TRUE;
}
/**
* @param string $tplFile
* The SQL file path (relative to this extension's dir).
* Ex: "sql/mydata.mysql.tpl".
* @return bool
*/
public function executeSqlTemplate($tplFile) {
// Assign multilingual variable to Smarty.
$upgrade = new CRM_Upgrade_Form();
$tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile;
$smarty = CRM_Core_Smarty::singleton();
$smarty->assign('domainID', CRM_Core_Config::domainID());
CRM_Utils_File::sourceSQLFile(
CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE
);
return TRUE;
}
......@@ -121,7 +146,7 @@ class CRM_Mosaico_Upgrader_Base {
*/
public function executeSql($query, $params = array()) {
// FIXME verify that we raise an exception on error
CRM_Core_DAO::executeSql($query, $params);
CRM_Core_DAO::executeQuery($query, $params);
return TRUE;
}
......@@ -222,22 +247,37 @@ class CRM_Mosaico_Upgrader_Base {
}
public function getCurrentRevision() {
// return CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName);
$revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName);
if (!$revision) {
$revision = $this->getCurrentRevisionDeprecated();
}
return $revision;
}
private function getCurrentRevisionDeprecated() {
$key = $this->extensionName . ':version';
return CRM_Core_BAO_Setting::getItem('Extension', $key);
if ($revision = CRM_Core_BAO_Setting::getItem('Extension', $key)) {
$this->revisionStorageIsDeprecated = TRUE;
}
return $revision;
}
public function setCurrentRevision($revision) {
// We call this during hook_civicrm_install, but the underlying SQL
// UPDATE fails because the extension record hasn't been INSERTed yet.
// Instead, track revisions in our own namespace.
// CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision);
$key = $this->extensionName . ':version';
CRM_Core_BAO_Setting::setItem($revision, 'Extension', $key);
CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision);
// clean up legacy schema version store (CRM-19252)
$this->deleteDeprecatedRevision();
return TRUE;
}
private function deleteDeprecatedRevision() {
if ($this->revisionStorageIsDeprecated) {
$setting = new CRM_Core_BAO_Setting();
$setting->name = $this->extensionName . ':version';
$setting->delete();
CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n");
}
}
// ******** Hook delegates ********
/**
......@@ -250,6 +290,12 @@ class CRM_Mosaico_Upgrader_Base {
CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
}
}
$files = glob($this->extensionDir . '/sql/*_install.mysql.tpl');
if (is_array($files)) {
foreach ($files as $file) {
$this->executeSqlTemplate($file);
}
}
$files = glob($this->extensionDir . '/xml/*_install.xml');
if (is_array($files)) {
foreach ($files as $file) {
......@@ -259,13 +305,31 @@ class CRM_Mosaico_Upgrader_Base {
if (is_callable(array($this, 'install'))) {
$this->install();
}
}
/**
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
*/
public function onPostInstall() {
$revisions = $this->getRevisions();
if (!empty($revisions)) {
$this->setCurrentRevision(max($revisions));
}
if (is_callable(array($this, 'postInstall'))) {
$this->postInstall();
}
}
/**
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
*/
public function onUninstall() {
$files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl');
if (is_array($files)) {
foreach ($files as $file) {
$this->executeSqlTemplate($file);
}
}
if (is_callable(array($this, 'uninstall'))) {
$this->uninstall();
}
......@@ -275,9 +339,11 @@ class CRM_Mosaico_Upgrader_Base {
CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
}
}
$this->setCurrentRevision(NULL);
}
/**
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
*/
public function onEnable() {
// stub for possible future use
if (is_callable(array($this, 'enable'))) {
......@@ -285,6 +351,9 @@ class CRM_Mosaico_Upgrader_Base {
}
}
/**
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
*/
public function onDisable() {
// stub for possible future use
if (is_callable(array($this, 'disable'))) {
......
......@@ -21,14 +21,14 @@ class CRM_Mosaico_UrlFilter extends \Civi\FlexMailer\Listener\BaseListener {
return;
}
SimpleFilter::byColumn($e, 'html', array($this, 'filterHtml'));
SimpleFilter::byValue($e, 'html', array($this, 'filterHtml'));
}
/**
* Find any image URLs and ensure they're absolute (not relative).
*
* @param array<string> $htmls
* @return array<string>
* @param string|array<string> $htmls
* @return string|array<string>
* Filtered HTML, with relative IMG url's changed to absolute URLs.
*/
public function filterHtml($htmls) {
......
......@@ -10,6 +10,10 @@ use CRM_Mosaico_ExtensionUtil as E;
*/
class CRM_Mosaico_Utils {
public static function isBootstrap() {
return strpos(CRM_Mosaico_Utils::getLayoutPath(), '/crmstar-') === FALSE;
}
/**
* Get a list of layout options.
*
......@@ -19,6 +23,7 @@ class CRM_Mosaico_Utils {
public static function getLayoutOptions() {
return array(
'auto' => E::ts('Automatically select a layout'),
'crmstar-single' => E::ts('Single Page (crm-*)'),
'bootstrap-single' => E::ts('Single Page (Bootstrap CSS)'),
'bootstrap-wizard' => E::ts('Wizard (Bootstrap CSS)'),
);
......@@ -35,17 +40,22 @@ class CRM_Mosaico_Utils {
$layout = CRM_Core_BAO_Setting::getItem('Mosaico Preferences', 'mosaico_layout');
$prefix = '~/crmMosaico/EditMailingCtrl';
switch ($layout) {
case '':
case 'auto':
case 'bootstrap-single':
return "$prefix/mosaico.html";
case 'bootstrap-wizard':
return "$prefix/mosaico-wizard.html";
$paths = array(
'crmstar-single' => "$prefix/crmstar-single.html",
'bootstrap-single' => "$prefix/bootstrap-single.html",
'bootstrap-wizard' => "$prefix/bootstrap-wizard.html",
);
default:
if (empty($layout) || $layout === 'auto') {
return CRM_Extension_System::singleton()->getMapper()->isActiveModule('shoreditch')
? $paths['bootstrap-wizard'] : $paths['crmstar-single'];
}
elseif (isset($paths[$layout])) {
return $paths[$layout];
}
else {
throw new \RuntimeException("Failed to determine path for Mosaico layout ($layout)");
}
}
......@@ -337,171 +347,6 @@ class CRM_Mosaico_Utils {
CRM_Utils_System::civiExit();
}
/**
* handler for dl requests
*/
public static function processDl() {
$config = self::getConfig();
global $http_return_code;
/* run this puppy through premailer */
// DS: not sure why we need premailer as it always sends out mobile (inline) layout.
// Lets disable it till we figure out why we need it.
//$premailer = Premailer::html( $_POST[ "html" ], true, "hpricot", $config['BASE_URL'] );
//$html = $premailer[ "html" ];
$html = $_POST["html"];
/* create static versions of resized images */
$matches = array();
$num_full_pattern_matches = preg_match_all('#<img.*?src="([^"]*?\/[^/]*\.[^"]+)#i',
$html, $matches);
for ($i = 0; $i < $num_full_pattern_matches; $i++) {
if (preg_match('#/img/(\?|&amp;)src=#i', $matches[1][$i])) {
$src_matches = array();
if (preg_match('#/img/(\?|&amp;)src=(.*)&amp;method=(.*)&amp;params=(.*)#i',
$matches[1][$i], $src_matches) !== FALSE
) {
$file_name = urldecode($src_matches[2]);
$file_name = substr($file_name,
strlen($config['BASE_URL'] . $config['UPLOADS_DIR']));
$method = urldecode($src_matches[3]);
$params = urldecode($src_matches[4]);
$params = explode(",", $params);
$width = (int) $params[0];
$height = (int) $params[1];
$static_file_name = $method . "_" . $width . "x" . $height . "_" . $file_name;
$html = str_ireplace($matches[1][$i],
$config['BASE_URL'] . $config['STATIC_URL'] . rawurlencode($static_file_name),
$html);//Changed to rawurlencode because space gets into + in the image file name if it has space
// resize and save static version of image
self::resizeImage($file_name, $method, $width, $height);
}
}
}
if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY == 1) {
// keep head section in literal to avoid smarty errors. Specially when CIVICRM_MAIL_SMARTY is turned on.
$html = str_ireplace(array('<head>', '</head>'),
array('{literal}<head>', '</head>{/literal}'), $html);
}
else {
if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY == 0) {
// get rid of any injected literal tags to avoid them appearing in emails
$html = str_ireplace(array('{literal}<head>', '</head>{/literal}'),
array('<head>', '</head>'), $html);
}
}
/* perform the requested action */
switch (CRM_Utils_Type::escape($_POST['action'], 'String')) {
case "download":
// download
header("Content-Type: application/force-download");
header("Content-Disposition: attachment; filename=\"" . $_POST["filename"] . "\"");
header("Content-Length: " . strlen($html));
echo $html;
break;
case "save":
$result = array();
$msgTplId = NULL;
$hashKey = CRM_Utils_Type::escape($_POST['key'], 'String');
if (!$hashKey) {
CRM_Core_Session::setStatus(ts('Mosaico hask key not found...'));
return FALSE;
}
$mosTpl = new CRM_Mosaico_DAO_MessageTemplate();
$mosTpl->hash_key = $hashKey;
if ($mosTpl->find(TRUE)) {
$msgTplId = $mosTpl->msg_tpl_id;
}
$name = "Mosaico Template " . date('d-m-Y H:i:s');
if (CRM_Utils_Type::escape($_POST['name'], 'String')) {
$name = $_POST['name'];
}
// save to message templates
$messageTemplate = array