Compare commits

...

31 Commits
0.0.5 ... 0.0.6

Author SHA1 Message Date
daff3d5016 Merge pull request #249 from mailpoet/sticky_kit_fix
Add back sticky-kit dependency
2015-11-27 19:14:03 +01:00
f13a4dd4f3 Add back sticky-kit dependency 2015-11-27 16:44:04 +02:00
04238f3339 Phoenix Vagrant VM has been replaced with a new VM with test data. 2015-11-27 15:16:06 +01:00
bc62474f3c Merge pull request #240 from mailpoet/queue
Queue
2015-11-27 13:49:18 +01:00
98005a2a6f - Rebases master 2015-11-27 07:46:26 -05:00
b7fe8dc6d6 - Adds Daemon status (under Mailpoet->Queue) 2015-11-27 07:40:23 -05:00
436faea591 - Refactors and renames code
- Adds Queue menu option and displays status
2015-11-27 07:40:22 -05:00
4208b148b4 - Implements queue worker class 2015-11-27 07:35:16 -05:00
5ce5f0bf8a - Refactors code 2015-11-27 07:35:16 -05:00
18518de397 - Refactors some code 2015-11-27 07:35:15 -05:00
68f747211a - Adds a helper method to convert cameCase to under_score
- Removes unused method from Queue daemon
2015-11-27 07:35:15 -05:00
ebd26ddd98 - Silences fsockopen errors 2015-11-27 07:35:14 -05:00
924aa0439f - Minor regex changes
- Adds method to Env class that returns plugin activation status
- Prevents Supervisor from running if the plugin isn't activated
2015-11-27 07:35:13 -05:00
3124d6a61b - Fixes issue with ucwords() function on older PHP versions
- Updates Supervisor/Daemon to strip port from site_url() when it's
  running on localhost inside a virtual machine :)
2015-11-27 07:35:13 -05:00
149d031b52 - Adds session support
- Renames Queue to Daemon
- Updates router methods
2015-11-27 07:35:12 -05:00
fa96c4697d - Adds Queue router
- Updates logic for Queue and Supervisor
- #227
2015-11-27 07:35:12 -05:00
d46c9d5412 - Fixes issue with Supervisor when database tables do not exist 2015-11-27 07:35:11 -05:00
9425ac1593 Merge pull request #244 from mailpoet/wp_users_segment
WP Users segment
2015-11-27 11:39:08 +01:00
4e32479609 Merge pull request #246 from mailpoet/import-fixes
Fixes import issues and updates code
2015-11-27 09:30:19 +01:00
7d95b38dc4 - Renames/refactors Import and Export classes/views/JS
- Updates Import and Export to ignore trashed subscribers
- Updates tests
Closes #245
2015-11-26 20:48:19 -05:00
17c83c5bd4 Merge pull request #242 from mailpoet/newsletter_templates
Newsletter template thumbnails
2015-11-26 21:26:23 +01:00
23bb2f111f Add a sample translatable template 2015-11-26 17:21:10 +02:00
4655b2c64c Switch template thumbnails to be generated in jpeg, not png 2015-11-26 17:20:34 +02:00
84fede11b8 removed dynamic lists from import 2015-11-26 16:02:53 +01:00
f37488fc0b Adds a Preview icon for previewing newsletter templates 2015-11-26 13:09:08 +02:00
20e2e03982 Hide non default segments on some pages
- added getPublic() method on Segment model
- filter out dynamic lists from add/move/remove segment in subscribers
2015-11-26 11:32:10 +01:00
a5d96f1534 WP Users list
- refactored and fixed listing issues (related to Segments)
- removed bulk actions from segments
- added synchronize methods for WP users
- Update action in segments only for WP Users list
- added "type" column to segments (default, wp_users, dynamic...)
- added "status" column to subscriber_segment table (useful soon)
2015-11-25 18:31:57 +01:00
a516eb1a95 Add overlay and newsletter thumbnail preview 2015-11-25 16:44:18 +02:00
6dd8270bec WP Users list
- migration for filters & segment_filter tables
- models for new tables
- update of Listing JSX to allow for conditional display of item actions
2015-11-24 17:12:14 +01:00
981cbd579f Fix clickable labels for editor settings views 2015-11-24 16:51:27 +02:00
52a0aae10f Add template thumbnail generation and display 2015-11-24 16:50:57 +02:00
62 changed files with 1829 additions and 332 deletions

67
Vagrantfile vendored
View File

@ -1,67 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.provider "virtualbox" do |v|
v.name = "phoenix"
v.memory = "2048"
end
config.vm.define :web do |web|
web.vm.box = "ubuntu/trusty64"
web.vm.hostname = "phoenix"
web.vm.network "forwarded_port", guest: 80, host: 8080
web.vm.synced_folder(
".",
"/var/www/html/wp-content/plugins/wysija-newsletters",
create: true,
owner: "vagrant",
group: "www-data"
)
web.vm.provision "shell", inline: <<-SHELL
sudo apt-get update
sudo apt-get install -y apache2 curl zip sendmail git build-essential
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password root'
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password root'
sudo apt-get install -y mysql-server-5.5
sudo apt-get install -y php5 libapache2-mod-php5 php5-curl php5-gd php5-mcrypt php5-readline mysql-server-5.5 php5-mysql php-apc
sudo sed -i "s/error_reporting = .*/error_reporting = E_ALL/" /etc/php5/apache2/php.ini
sudo sed -i "s/display_errors = .*/display_errors = On/" /etc/php5/apache2/php.ini
cd /var/www/html
sudo wget https://github.com/calvinlough/sqlbuddy/raw/gh-pages/sqlbuddy.zip -O /var/www/html/sqlbuddy.zip
sudo rm index.html
unzip sqlbuddy.zip
sudo curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
sudo chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
sudo wp core download --allow-root
mysql -uroot -proot -e "create database wordpress"
sudo wp core config --allow-root --dbname=wordpress --dbuser=root --dbpass=root
sudo wp core install --allow-root --url="http://localhost:8080" --title=WordPress --admin_user=admin --admin_password=password --admin_email=test@mailpoet-container.com
sudo sed -i "s/upload_max_filesize = .*/upload_max_filesize = 32M/" /etc/php5/apache2/php.ini
sudo sed -i "s/post_max_size = .*/post_max_size = 32M/" /etc/php5/apache2/php.ini
sudo chown -hR vagrant:www-data /var/www/html/
sudo a2enmod rewrite > /dev/null 2>&1
cd /var/www/html/wp-content/plugins/wysija-newsletters
curl -sS https://getcomposer.org/installer | php
sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install -y nodejs
sudo sed -i "s/export APACHE_RUN_USER.*/export APACHE_RUN_USER=vagrant/" /etc/apache2/envvars
sudo chown -R vagrant:www-data /var/lock/apache2
sudo service apache2 restart
SHELL
end
end

View File

@ -18,6 +18,38 @@
height: 150px
margin-right: 15px
float: left
overflow: hidden
position: relative
img
min-width: 150px
min-height: 150px
height: auto
width: 110%
position: relative
top: 0
left: 50%
transform: translate(-50%, 0%)
.mailpoet_overlay
position: absolute
top: 0
left: 0
right: 0
bottom: 0
background-color: rgba(255, 255, 255, 0.0)
&:hover
background-color: rgba(255, 255, 255, 0.7)
&::after
content: " "
position: absolute
top: 0
left: 0
bottom: 0
right: 0
background: url(../img/preview_magnifying_glass.svg) no-repeat center center
.mailpoet_boxes .mailpoet_description
float:left

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="47.002px" height="38.003px" viewBox="0 0 47.002 38.003" enable-background="new 0 0 47.002 38.003" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#565656" d="M46.328,36.365c-1.188,1.725-3.553,2.158-5.273,0.962L25.479,26.52
c-1.104-0.763-1.674-2.007-1.631-3.257c-2.006,2.157-4.642,3.606-7.594,4.145c-3.626,0.663-7.288-0.13-10.311-2.227
c-3.024-2.098-5.054-5.253-5.714-8.887c-0.661-3.636,0.127-7.31,2.221-10.344c4.325-6.264,12.927-7.834,19.177-3.5
c5.672,3.938,7.486,11.412,4.537,17.443c1.152-0.486,2.519-0.392,3.627,0.377l15.58,10.808C47.09,32.274,47.52,34.641,46.328,36.365
z M23.235,12.09c-0.459-2.534-1.874-4.734-3.982-6.196C14.897,2.87,8.896,3.963,5.878,8.331c-3.014,4.373-1.922,10.388,2.435,13.408
c4.356,3.025,10.356,1.932,13.374-2.438C23.146,17.187,23.696,14.625,23.235,12.09z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -105,6 +105,9 @@ const item_actions = [
refresh();
});
}
},
{
name: 'trash'
}
];

View File

@ -79,27 +79,48 @@ define(
if(custom_actions.length > 0) {
item_actions = custom_actions.map(function(action, index) {
if(action.refresh) {
if(action.display !== undefined) {
if(action.display(this.props.item) === false) {
return;
}
}
if(action.name === 'trash') {
return (
<span key={ 'action-'+index } className="trash">
{(index > 0) ? ' | ' : ''}
<a
href="javascript:;"
onClick={ this.handleTrashItem.bind(
null,
this.props.item.id
) }>
Trash
</a>
</span>
);
} else if(action.refresh) {
return (
<span
onClick={ this.props.onRefreshItems }
key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''}
{ action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
} else if(action.link) {
return (
<span
key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''}
{ action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
} else {
return (
<span
key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''}
<a href="javascript:;" onClick={
(action.onClick !== undefined)
? action.onClick.bind(null,
@ -108,7 +129,6 @@ define(
)
: false
}>{ action.label }</a>
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
}
@ -158,17 +178,6 @@ define(
<div>
<div className="row-actions">
{ item_actions }
{ ' | ' }
<span className="trash">
<a
href="javascript:;"
onClick={ this.handleTrashItem.bind(
null,
this.props.item.id
) }>
Trash
</a>
</span>
</div>
<button
onClick={ this.handleToggleItem.bind(null, this.props.item.id) }
@ -637,7 +646,7 @@ define(
// bulk actions
var bulk_actions = this.props.bulk_actions || [];
if(this.state.group === 'trash') {
if(this.state.group === 'trash' && bulk_actions.length > 0) {
bulk_actions = [
{
name: 'restore',

View File

@ -6,8 +6,9 @@ define([
'backbone.marionette',
'jquery',
'blob',
'filesaver'
], function(App, MailPoet, Notice, Backbone, Marionette, jQuery, Blob, FileSaver) {
'filesaver',
'html2canvas'
], function(App, MailPoet, Notice, Backbone, Marionette, jQuery, Blob, FileSaver, html2canvas) {
"use strict";
@ -56,16 +57,26 @@ define([
});
};
Module.exportTemplate = function(options) {
var data = _.extend(options || {}, {
body: App.getBody(),
});
var blob = new Blob(
[JSON.stringify(data)],
{ type: 'application/json;charset=utf-8' }
);
Module.getThumbnail = function(element, options) {
return html2canvas(element, options || {});
};
FileSaver.saveAs(blob, 'template.json');
Module.exportTemplate = function(options) {
var that = this;
return Module.getThumbnail(
jQuery('#mailpoet_editor_content > .mailpoet_block').get(0)
).then(function(thumbnail) {
var data = _.extend(options || {}, {
thumbnail: thumbnail.toDataURL('image/jpeg'),
body: App.getBody(),
});
var blob = new Blob(
[JSON.stringify(data)],
{ type: 'application/json;charset=utf-8' }
);
FileSaver.saveAs(blob, 'template.json');
});
};
Module.SaveView = Marionette.LayoutView.extend({

View File

@ -56,7 +56,7 @@ define([
};
Module.getTransformedPosts = function(options) {
return Module._query({
return Module._cachedQuery({
action: 'getTransformedPosts',
options: options,
});

View File

@ -112,6 +112,9 @@ define(
</a>
);
}
},
{
name: 'trash'
}
];

View File

@ -150,6 +150,13 @@ define(
this.setState({ loading: false });
}
},
handleShowTemplate: function(template) {
MailPoet.Modal.popup({
title: template.name,
template: '<img src="{{ thumbnail }}" />',
data: template,
});
},
handleTemplateImport: function() {
this.getTemplates();
},
@ -164,11 +171,22 @@ define(
Delete
</a>
</div>
);
), thumbnail = '';
if (typeof template.thumbnail === 'string'
&& template.thumbnail.length > 0) {
thumbnail = (
<a href="javascript:;" onClick={this.handleShowTemplate.bind(null, template)}>
<img src={ template.thumbnail } />
<div className="mailpoet_overlay"></div>
</a>
);
}
return (
<li key={ 'template-'+index }>
<div className="mailpoet_thumbnail">
{ thumbnail }
</div>
<div className="mailpoet_description">

75
assets/js/src/queue.jsx Normal file
View File

@ -0,0 +1,75 @@
define(
[
'react',
'react-dom',
'mailpoet',
'classnames'
],
function (
React,
ReactDOM,
MailPoet,
classNames
) {
var QueueDaemonControl = React.createClass({
getInitialState: function () {
return (queueDaemon) ? {
status: queueDaemon.status,
timeSinceStart: queueDaemon.time_since_start,
timeSinceUpdate: queueDaemon.time_since_update,
counter: queueDaemon.counter
} : null;
},
getDaemonData: function () {
MailPoet.Ajax.post({
endpoint: 'queue',
action: 'getQueueStatus'
}).done(function (response) {
this.setState({
status: response.status,
timeSinceStart: response.time_since_start,
timeSinceUpdate: response.time_since_update,
counter: response.counter,
});
}.bind(this));
},
componentDidMount: function () {
this.getDaemonData;
setInterval(this.getDaemonData, 5000);
},
render: function () {
if (!this.state) {
return (
<div className="QueueControl">
Woops, daemon is not running ;\
</div>
)
}
return (
<div>
<div>
Queue is currently <b>{this.state.status}</b>.
<br/>
<br/>
It was started
<b> {this.state.timeSinceStart} </b> and was last executed
<b> {this.state.timeSinceUpdate} </b> for a total of
<b> {this.state.counter} </b> times (once every 30 seconds, unless it was interrupted and restarted).
<br />
</div>
<div>
</div>
</div>
);
}
});
let container = document.getElementById('queue_container');
if (container) {
ReactDOM.render(
<QueueDaemonControl />,
container
)
}
}
);

View File

@ -121,6 +121,30 @@ const item_actions = [
);
refresh();
});
},
display: function(segment) {
return (segment.type !== 'wp_users');
}
},
{
name: 'synchronize_segment',
label: 'Update',
className: 'update',
onClick: function(item, refresh) {
return MailPoet.Ajax.post({
endpoint: 'segments',
action: 'synchronize'
}).done(function(response) {
if(response === true) {
MailPoet.Notice.success(
('List "%$1s" has been synchronized.').replace('%$1s', item.name)
);
refresh();
}
});
},
display: function(segment) {
return (segment.type === 'wp_users');
}
},
{
@ -130,15 +154,16 @@ const item_actions = [
<a href={ item.subscribers_url }>View subscribers</a>
);
}
},
{
name: 'trash',
display: function(segment) {
return (segment.type !== 'wp_users');
}
}
];
const bulk_actions = [
{
name: 'trash',
label: 'Trash',
onSuccess: messages.onTrash
}
];
const SegmentList = React.createClass({
@ -148,7 +173,6 @@ const SegmentList = React.createClass({
'column-primary',
'has-row-actions'
);
return (
<div>
<td className={ rowClasses }>

View File

@ -20,6 +20,7 @@ define(
return;
}
jQuery(document).ready(function () {
jQuery('input[name="select_method"]').attr('checked', false);
// configure router
router = new (Backbone.Router.extend({
routes: {
@ -299,11 +300,11 @@ define(
var test,
cleanEmail =
email
// left/right trim spaces, punctuation (e.g., " 'email@email.com'; ")
// right trim non-printable characters (e.g., "email@email.com<6F>")
// left/right trim spaces, punctuation (e.g., " 'email@email.com'; ")
// right trim non-printable characters (e.g., "email@email.com<6F>")
.replace(/^["';.,\s]+|[^\x20-\x7E]+$|["';.,_\s]+$/g, '')
// remove spaces (e.g., "email @ email . com")
// remove urlencoded characters
// remove spaces (e.g., "email @ email . com")
// remove urlencoded characters
.replace(/\s+|%\d+|,+/g, '')
.toLowerCase();
@ -1000,7 +1001,6 @@ define(
}
function toggleNextStepButton(condition) {
console.log(condition);
var disabled = 'button-disabled';
if (condition === 'on') {
nextStepButton.removeClass(disabled);

View File

@ -103,7 +103,9 @@ const bulk_actions = [
id: 'move_to_segment',
endpoint: 'segments',
filter: function(segment) {
return !!(!segment.deleted_at);
return !!(
!segment.deleted_at && segment.type === 'default'
);
}
};
@ -132,7 +134,9 @@ const bulk_actions = [
id: 'add_to_segment',
endpoint: 'segments',
filter: function(segment) {
return !!(!segment.deleted_at);
return !!(
!segment.deleted_at && segment.type === 'default'
);
}
};
@ -159,7 +163,12 @@ const bulk_actions = [
onSelect: function() {
let field = {
id: 'remove_from_segment',
endpoint: 'segments'
endpoint: 'segments',
filter: function(segment) {
return !!(
segment.type === 'default'
);
}
};
return (
@ -206,6 +215,21 @@ const bulk_actions = [
}
];
const item_actions = [
{
name: 'edit',
label: 'Edit',
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>Edit</Link>
);
}
},
{
name: 'trash'
}
];
const SubscriberList = React.createClass({
renderItem: function(subscriber, actions) {
let row_classes = classNames(
@ -295,6 +319,7 @@ const SubscriberList = React.createClass({
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ item_actions }
messages={ messages }
onGetItems={ this.onGetItems }
/>

View File

@ -8,7 +8,9 @@
"tburry/pquery": "*",
"j4mie/paris": "1.5.4",
"swiftmailer/swiftmailer": "^5.4",
"phpseclib/phpseclib": "*"
"phpseclib/phpseclib": "*",
"mtdowling/cron-expression": "^1.0",
"nesbot/carbon": "^1.21"
},
"require-dev": {
"codeception/codeception": "*",

View File

@ -4,36 +4,37 @@ namespace MailPoet\Config;
if(!defined('ABSPATH')) exit;
class Env {
public static $version;
public static $plugin_name;
public static $plugin_url;
public static $file;
public static $path;
public static $views_path;
public static $assets_path;
public static $assets_url;
public static $temp_name;
public static $temp_path;
public static $languages_path;
public static $lib_path;
public static $plugin_prefix;
public static $db_prefix;
public static $db_source_name;
public static $db_host;
public static $db_socket;
public static $db_port;
public static $db_name;
public static $db_username;
public static $db_password;
public static $db_charset;
static $version;
static $plugin_name;
static $plugin_url;
static $plugin_path;
static $file;
static $path;
static $views_path;
static $assets_path;
static $assets_url;
static $temp_name;
static $temp_path;
static $languages_path;
static $lib_path;
static $plugin_prefix;
static $db_prefix;
static $db_source_name;
static $db_host;
static $db_socket;
static $db_port;
static $db_name;
static $db_username;
static $db_password;
static $db_charset;
public static function init($file, $version) {
static function init($file, $version) {
global $wpdb;
self::$version = $version;
self::$file = $file;
self::$path = dirname(self::$file);
self::$plugin_name = 'mailpoet';
self::$plugin_url = plugins_url() . '/' . basename(Env::$path);
self::$plugin_url = plugin_dir_url(__FILE__);
self::$views_path = self::$path . '/views';
self::$assets_path = self::$path . '/assets';
self::$assets_url = plugins_url('/assets', $file);
@ -46,11 +47,12 @@ class Env {
self::$db_host = DB_HOST;
self::$db_port = 3306;
self::$db_socket = false;
if (preg_match('/(?=:\d+$)/', DB_HOST)) {
if(preg_match('/(?=:\d+$)/', DB_HOST)) {
list(self::$db_host, self::$db_port) = explode(':', DB_HOST);
}
else if (preg_match('/:/', DB_HOST)) {
self::$db_socket = true;
} else {
if(preg_match('/:/', DB_HOST)) {
self::$db_socket = true;
}
}
self::$db_name = DB_NAME;
self::$db_username = DB_USER;
@ -59,7 +61,7 @@ class Env {
self::$db_source_name = self::dbSourceName(self::$db_host, self::$db_socket, self::$db_port);
}
private static function dbSourceName($host, $socket,$port) {
private static function dbSourceName($host, $socket, $port) {
$source_name = array(
(!$socket) ? 'mysql:host=' : 'mysql:unix_socket=',
$host,
@ -72,4 +74,14 @@ class Env {
);
return implode('', $source_name);
}
static function isPluginActivated() {
$activatesPlugins = get_option('active_plugins');
$isActivated =
in_array(
sprintf('%s/%s.php', basename(self::$path), self::$plugin_name),
$activatesPlugins
);
return ($isActivated) ? true : false;
}
}

42
lib/Config/Hooks.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace MailPoet\Config;
class Hooks {
function __construct() {
}
function init() {
// WP Users synchronization
add_action(
'user_register',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
add_action(
'added_existing_user',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
add_action(
'profile_update',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
add_action(
'delete_user',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
// multisite
add_action(
'deleted_user',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
add_action(
'remove_user_from_blog',
'\MailPoet\Segments\WP::synchronizeUser',
1
);
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Config;
use MailPoet\Models;
use MailPoet\Queue\Supervisor;
use MailPoet\Router;
if(!defined('ABSPATH')) exit;
@ -25,6 +26,9 @@ class Initializer {
$this->setupAnalytics();
$this->setupPermissions();
$this->setupChangelog();
$this->setupPublicAPI();
$this->runQueueSupervisor();
$this->setupHooks();
}
function setupDB() {
@ -33,7 +37,8 @@ class Initializer {
\ORM::configure('password', Env::$db_password);
\ORM::configure('logging', WP_DEBUG);
\ORM::configure('driver_options', array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET TIME_ZONE = "+00:00"'
));
$subscribers = Env::$db_prefix . 'subscribers';
@ -41,6 +46,8 @@ class Initializer {
$newsletters = Env::$db_prefix . 'newsletters';
$newsletter_templates = Env::$db_prefix . 'newsletter_templates';
$segments = Env::$db_prefix . 'segments';
$filters = Env::$db_prefix . 'filters';
$segment_filter = Env::$db_prefix . 'segment_filter';
$forms = Env::$db_prefix . 'forms';
$subscriber_segment = Env::$db_prefix . 'subscriber_segment';
$newsletter_segment = Env::$db_prefix . 'newsletter_segment';
@ -48,11 +55,15 @@ class Initializer {
$subscriber_custom_field = Env::$db_prefix . 'subscriber_custom_field';
$newsletter_option_fields = Env::$db_prefix . 'newsletter_option_fields';
$newsletter_option = Env::$db_prefix . 'newsletter_option';
$queues = Env::$db_prefix . 'queues';
$newsletter_statistics = Env::$db_prefix . 'newsletter_statistics';
define('MP_SUBSCRIBERS_TABLE', $subscribers);
define('MP_SETTINGS_TABLE', $settings);
define('MP_NEWSLETTERS_TABLE', $newsletters);
define('MP_SEGMENTS_TABLE', $segments);
define('MP_FILTERS_TABLE', $filters);
define('MP_SEGMENT_FILTER_TABLE', $segment_filter);
define('MP_FORMS_TABLE', $forms);
define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment);
define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates);
@ -61,6 +72,8 @@ class Initializer {
define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', $subscriber_custom_field);
define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', $newsletter_option_fields);
define('MP_NEWSLETTER_OPTION_TABLE', $newsletter_option);
define('MP_QUEUES_TABLE', $queues);
define('MP_NEWSLETTER_STATISTICS_TABLE', $newsletter_statistics);
}
function setupActivator() {
@ -110,4 +123,21 @@ class Initializer {
$changelog = new Changelog();
$changelog->init();
}
}
function setupHooks() {
$hooks = new Hooks();
$hooks->init();
}
function setupPublicAPI() {
$publicAPI = new PublicAPI();
$publicAPI->init();
}
function runQueueSupervisor() {
try {
$supervisor = new Supervisor();
$supervisor->checkDaemon();
} catch (\Exception $e) {}
}
}

View File

@ -1,17 +1,17 @@
<?php
namespace MailPoet\Config;
use MailPoet\Form\Block;
use MailPoet\Form\Renderer as FormRenderer;
use MailPoet\Models\Form;
use MailPoet\Models\Segment;
use MailPoet\Models\Setting;
use MailPoet\Settings\Charsets;
use MailPoet\Settings\Hosts;
use MailPoet\Settings\Pages;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use \MailPoet\Models\Segment;
use \MailPoet\Models\Setting;
use \MailPoet\Models\Form;
use \MailPoet\Form\Block;
use \MailPoet\Form\Renderer as FormRenderer;
use \MailPoet\Settings\Hosts;
use \MailPoet\Settings\Pages;
use \MailPoet\Settings\Charsets;
use \MailPoet\Util\Permissions;
use \MailPoet\Util\DKIM;
use MailPoet\Util\DKIM;
use MailPoet\Util\Permissions;
if(!defined('ABSPATH')) exit;
@ -24,7 +24,10 @@ class Menu {
function init() {
add_action(
'admin_menu',
array($this, 'setup')
array(
$this,
'setup'
)
);
}
@ -34,7 +37,10 @@ class Menu {
'MailPoet',
'manage_options',
'mailpoet',
array($this, 'home'),
array(
$this,
'home'
),
$this->assets_url . '/img/menu_icon.png',
30
);
@ -44,7 +50,10 @@ class Menu {
__('Newsletters'),
'manage_options',
'mailpoet-newsletters',
array($this, 'newsletters')
array(
$this,
'newsletters'
)
);
add_submenu_page(
'mailpoet',
@ -52,7 +61,10 @@ class Menu {
__('Forms'),
'manage_options',
'mailpoet-forms',
array($this, 'forms')
array(
$this,
'forms'
)
);
add_submenu_page(
'mailpoet',
@ -60,7 +72,10 @@ class Menu {
__('Subscribers'),
'manage_options',
'mailpoet-subscribers',
array($this, 'subscribers')
array(
$this,
'subscribers'
)
);
add_submenu_page(
'mailpoet',
@ -68,7 +83,10 @@ class Menu {
__('Segments'),
'manage_options',
'mailpoet-segments',
array($this, 'segments')
array(
$this,
'segments'
)
);
add_submenu_page(
'mailpoet',
@ -76,7 +94,10 @@ class Menu {
__('Settings'),
'manage_options',
'mailpoet-settings',
array($this, 'settings')
array(
$this,
'settings'
)
);
add_submenu_page(
null,
@ -84,7 +105,10 @@ class Menu {
__('Import'),
'manage_options',
'mailpoet-import',
array($this, 'import')
array(
$this,
'import'
)
);
add_submenu_page(
null,
@ -92,7 +116,10 @@ class Menu {
__('Export'),
'manage_options',
'mailpoet-export',
array($this, 'export')
array(
$this,
'export'
)
);
add_submenu_page(
@ -101,7 +128,10 @@ class Menu {
__('Welcome'),
'manage_options',
'mailpoet-welcome',
array($this, 'welcome')
array(
$this,
'welcome'
)
);
add_submenu_page(
@ -110,7 +140,10 @@ class Menu {
__('Update'),
'manage_options',
'mailpoet-update',
array($this, 'update')
array(
$this,
'update'
)
);
add_submenu_page(
@ -119,7 +152,10 @@ class Menu {
__('Form editor'),
'manage_options',
'mailpoet-form-editor',
array($this, 'formEditor')
array(
$this,
'formEditor'
)
);
add_submenu_page(
@ -128,7 +164,22 @@ class Menu {
__('Newsletter editor'),
'manage_options',
'mailpoet-newsletter-editor',
array($this, 'newletterEditor')
array(
$this,
'newletterEditor'
)
);
add_submenu_page(
'mailpoet',
__('Queue'),
__('Queue'),
'manage_options',
'mailpoet-queue',
array(
$this,
'queue'
)
);
}
@ -142,8 +193,8 @@ class Menu {
$current_url = home_url(add_query_arg($wp->query_string, $wp->request));
$redirect_url =
(!empty($_GET['mailpoet_redirect']))
? urldecode($_GET['mailpoet_redirect'])
: wp_get_referer();
? urldecode($_GET['mailpoet_redirect'])
: wp_get_referer();
if(
$redirect_url === $current_url
@ -166,8 +217,8 @@ class Menu {
$current_url = home_url(add_query_arg($wp->query_string, $wp->request));
$redirect_url =
(!empty($_GET['mailpoet_redirect']))
? urldecode($_GET['mailpoet_redirect'])
: wp_get_referer();
? urldecode($_GET['mailpoet_redirect'])
: wp_get_referer();
if(
$redirect_url === $current_url
@ -206,7 +257,8 @@ class Menu {
$data = array(
'settings' => $settings,
'segments' => Segment::getPublished()->findArray(),
'segments' => Segment::getPublished()
->findArray(),
'pages' => Pages::getAll(),
'flags' => $this->_getFlags(),
'charsets' => Charsets::getAll(),
@ -234,11 +286,14 @@ class Menu {
// check if users can register
$flags['registration_enabled'] =
!(in_array($registration, array('none', 'blog')));
!(in_array($registration, array(
'none',
'blog'
)));
} else {
// check if users can register
$flags['registration_enabled'] =
(bool)get_option('users_can_register', false);
(bool) get_option('users_can_register', false);
}
return $flags;
@ -249,7 +304,7 @@ class Menu {
$data['segments'] = Segment::findArray();
echo $this->renderer->render('subscribers.html', $data);
echo $this->renderer->render('subscribers/subscribers.html', $data);
}
function segments() {
@ -272,7 +327,7 @@ class Menu {
$data['segments'] = Segment::findArray();
$settings = Setting::findArray();
$data['settings'] = array();
foreach($settings as $setting) {
foreach ($settings as $setting) {
$data['settings'][$setting['name']] = $setting['value'];
}
$data['roles'] = $wp_roles->get_names();
@ -290,17 +345,17 @@ class Menu {
function import() {
$import = new BootStrapMenu('import');
$data = $import->bootstrap();
echo $this->renderer->render('import.html', $data);
echo $this->renderer->render('subscribers/importExport/import.html', $data);
}
function export() {
$export = new BootStrapMenu('export');
$data = $export->bootstrap();
echo $this->renderer->render('export.html', $data);
echo $this->renderer->render('subscribers/importExport/export.html', $data);
}
function formEditor() {
$id = (isset($_GET['id']) ? (int)$_GET['id'] : 0);
$id = (isset($_GET['id']) ? (int) $_GET['id'] : 0);
$form = Form::findOne($id);
if($form !== false) {
$form = $form->asArray();
@ -309,7 +364,8 @@ class Menu {
$data = array(
'form' => $form,
'pages' => Pages::getAll(),
'segments' => Segment::getPublished()->findArray(),
'segments' => Segment::getPublished()
->findArray(),
'styles' => FormRenderer::getStyles($form),
'date_types' => Block\Date::getDateTypes(),
'date_formats' => Block\Date::getDateFormats()
@ -317,4 +373,10 @@ class Menu {
echo $this->renderer->render('form/editor.html', $data);
}
function queue() {
$daemon = new \MailPoet\Queue\BootStrapMenu();
$data['daemon'] = json_encode($daemon->bootstrap());
echo $this->renderer->render('queue.html', $data);
}
}

View File

@ -21,6 +21,8 @@ class Migrator {
'subscriber_custom_field',
'newsletter_option_fields',
'newsletter_option',
'queues',
'newsletter_statistics',
'forms'
);
}
@ -50,6 +52,7 @@ class Migrator {
function subscribers() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'wp_user_id bigint(20) NULL,',
'first_name tinytext NOT NULL,',
'last_name tinytext NOT NULL,',
'email varchar(150) NOT NULL,',
@ -97,6 +100,7 @@ class Migrator {
'name varchar(250) NOT NULL,',
'description varchar(250) NOT NULL,',
'body longtext,',
'thumbnail longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
@ -106,14 +110,15 @@ class Migrator {
function segments() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'description varchar(250) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'type varchar(90) NOT NULL DEFAULT "default",',
'description varchar(250) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
@ -123,6 +128,7 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'subscriber_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "subscribed",',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
@ -199,6 +205,41 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function queues() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscribers longtext,',
'status varchar(12) NOT NULL,',
'priority mediumint(9) NOT NULL DEFAULT 0,',
'count_total mediumint(9) NOT NULL DEFAULT 0,',
'count_processed mediumint(9) NOT NULL DEFAULT 0,',
'count_to_process mediumint(9) NOT NULL DEFAULT 0,',
'count_failed mediumint(9) NOT NULL DEFAULT 0,',
'processed_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_statistics() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'sent_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function forms() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
@ -224,4 +265,4 @@ class Migrator {
return implode("\n", $sql);
}
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Config;
use MailPoet\Config\PopulatorData\Templates\SampleTemplate;
if (!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@ -10,6 +12,7 @@ class Populator {
$this->prefix = Env::$db_prefix;
$this->models = array(
'newsletter_option_fields',
'newsletter_templates',
);
}
@ -84,6 +87,12 @@ class Populator {
);
}
private function newsletter_templates() {
return array(
(new SampleTemplate(Env::$assets_url))->get(),
);
}
private function populate($model) {
$rows = $this->$model();
$table = $this->prefix . $model;

File diff suppressed because one or more lines are too long

45
lib/Config/PublicAPI.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace MailPoet\Config;
use MailPoet\Queue\Daemon;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class PublicAPI {
function __construct() {
# http://example.com/?mailpoet-api&section=&action=&payload=
$this->api = isset($_GET['mailpoet-api']) ? true : false;
$this->section = isset($_GET['section']) ? $_GET['section'] : false;
$this->action = isset($_GET['action']) ?
Helpers::underscoreToCamelCase($_GET['action']) :
false;
$this->payload = isset($_GET['payload']) ?
json_decode(urldecode($_GET['payload']), true) :
false;
}
function init() {
if(!$this->api && !$this->section) return;
$this->_checkAndCallMethod($this, $this->section, $terminate = true);
}
function queue() {
$queue = new Daemon($this->payload);
$this->_checkAndCallMethod($queue, $this->action);
}
private function _checkAndCallMethod($class, $method, $terminate = false) {
if(!method_exists($class, $method)) {
if(!$terminate) return;
header('HTTP/1.0 404 Not Found');
exit;
}
call_user_func(
array(
$class,
$method
)
);
}
}

50
lib/Models/Filter.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Filter extends Model {
static $_table = MP_FILTERS_TABLE;
function __construct() {
parent::__construct();
$this->addValidations('name', array(
'required' => __('You need to specify a name.')
));
}
function delete() {
// delete all relations to subscribers
SegmentFilter::where('filter_id', $this->id)->deleteMany();
return parent::delete();
}
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\SegmentFilter',
'filter_id',
'segment_id'
);
}
static function createOrUpdate($data = array()) {
$filter = false;
if(isset($data['id']) && (int)$data['id'] > 0) {
$filter = self::findOne((int)$data['id']);
}
if($filter === false) {
$filter = self::create();
$filter->hydrate($data);
} else {
unset($data['id']);
$filter->set($data);
}
$filter->save();
return $filter;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class NewsletterStatistics extends Model {
public static $_table = MP_NEWSLETTER_STATISTICS_TABLE;
function __construct() {
parent::__construct();
}
}

12
lib/Models/Queue.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Queue extends Model {
public static $_table = MP_QUEUES_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -38,6 +38,15 @@ class Segment extends Model {
);
}
function segmentFilters() {
return $this->has_many_through(
__NAMESPACE__.'\Filter',
__NAMESPACE__.'\SegmentFilter',
'segment_id',
'filter_id'
);
}
function duplicate($data = array()) {
$duplicate = parent::duplicate($data);
@ -67,6 +76,23 @@ class Segment extends Model {
->delete();
}
static function getWPUsers() {
$segment = self::where('type', 'wp_users')->findOne();
if($segment === false) {
// create the wp users list
$segment = self::create();
$segment->hydrate(array(
'name' => __('WordPress Users'),
'type' => 'wp_users'
));
$segment->save();
return self::findOne($segment->id());
}
return $segment;
}
static function search($orm, $search = '') {
return $orm->whereLike('name', '%'.$search.'%');
}
@ -102,8 +128,13 @@ class Segment extends Model {
->left_outer_join(
MP_SUBSCRIBER_SEGMENT_TABLE,
array(self::$_table.'.id', '=', MP_SUBSCRIBER_SEGMENT_TABLE.'.segment_id'))
->left_outer_join(
MP_SUBSCRIBERS_TABLE,
array(MP_SUBSCRIBER_SEGMENT_TABLE.'.subscriber_id', '=', MP_SUBSCRIBERS_TABLE.'.id'))
->whereNull(MP_SUBSCRIBERS_TABLE.'.deleted_at')
->group_by(self::$_table.'.id')
->group_by(self::$_table.'.name')
->where(self::$_table.'.type', 'default')
->findArray();
}
@ -114,16 +145,18 @@ class Segment extends Model {
'LEFT JOIN ' . self::$_table . ' segments ON segments.id = relation.segment_id ' .
'LEFT JOIN ' . MP_SUBSCRIBERS_TABLE . ' subscribers ON subscribers.id = relation.subscriber_id ' .
(($withConfirmedSubscribers) ?
'WHERE subscribers.status = 1 ' :
'WHERE subscribers.status = "subscribed" ' :
'WHERE relation.segment_id IS NOT NULL ') .
'AND subscribers.deleted_at IS NULL ' .
'GROUP BY segments.id) ' .
'UNION ALL ' .
'(SELECT 0 as id, "' . __('Not In List') . '" as name, COUNT(*) as subscribers ' .
'FROM ' . MP_SUBSCRIBERS_TABLE . ' subscribers ' .
'LEFT JOIN ' . MP_SUBSCRIBER_SEGMENT_TABLE . ' relation on relation.subscriber_id = subscribers.id ' .
(($withConfirmedSubscribers) ?
'WHERE relation.subscriber_id is NULL AND subscribers.status = 1 ' :
'WHERE relation.subscriber_id is NULL AND subscribers.status = "subscribed" ' :
'WHERE relation.subscriber_id is NULL ') .
'AND subscribers.deleted_at IS NULL ' .
'HAVING subscribers) ' .
'ORDER BY name'
)->findArray();
@ -147,4 +180,8 @@ class Segment extends Model {
$segment->save();
return $segment;
}
static function getPublic() {
return self::getPublished()->where('type', 'default');
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class SegmentFilter extends Model {
public static $_table = MP_SEGMENT_FILTER_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -324,7 +324,7 @@ class Subscriber extends Model {
->whereNull('deleted_at')
->where('status', 'unconfirmed');
}
static function createMultiple($columns, $values) {
return self::rawExecute(
'INSERT INTO `' . self::$_table . '` ' .

View File

@ -0,0 +1,36 @@
<?php
namespace MailPoet\Queue;
use Carbon\Carbon;
use MailPoet\Models\Queue;
use MailPoet\Models\Setting;
class BootStrapMenu {
function __construct() {
$this->daemon = Setting::where('name', 'daemon')
->findOne();
}
function bootStrap() {
$queues = Queue::findMany();
return ($this->daemon) ?
array_merge(
array(
'time_since_start' =>
Carbon::createFromFormat(
'Y-m-d H:i:s',
$this->daemon->created_at,
'UTC'
)->diffForHumans(),
'time_since_update' =>
Carbon::createFromFormat(
'Y-m-d H:i:s',
$this->daemon->updated_at,
'UTC'
)->diffForHumans()
),
json_decode($this->daemon->value, true)
) :
"false";
}
}

128
lib/Queue/Daemon.php Normal file
View File

@ -0,0 +1,128 @@
<?php
namespace MailPoet\Queue;
use MailPoet\Models\Setting;
use MailPoet\Util\Security;
require_once(ABSPATH . 'wp-includes/pluggable.php');
if(!defined('ABSPATH')) exit;
class Daemon {
function __construct($payload = array()) {
set_time_limit(0);
ignore_user_abort();
list ($this->daemon, $this->daemonData) = $this->getDaemon();
$this->refreshedToken = $this->refreshToken();
$this->payload = $payload;
$this->timer = microtime(true);
}
function start() {
if(!isset($this->payload['session'])) {
$this->abortWithError('missing session ID');
}
$this->manageSession('start');
$daemon = $this->daemon;
$daemonData = $this->daemonData;
if(!$daemon) {
$daemon = Setting::create();
$daemon->name = 'daemon';
$daemon->value = json_encode(array('status' => 'stopped'));
$daemon->save();
}
if($daemonData['status'] !== 'started') {
$_SESSION['daemon'] = 'started';
$daemonData = array(
'status' => 'started',
'token' => $this->refreshedToken,
'counter' => ($daemonData['status'] === 'paused') ?
$daemonData['counter'] :
0
);
$_SESSION['daemon'] = array('result' => true);
$this->manageSession('end');
$daemon->value = json_encode($daemonData);
$daemon->save();
$this->callSelf();
} else {
$_SESSION['daemon'] = array(
'result' => false,
'error' => 'already started'
);
}
$this->manageSession('end');
}
function run() {
if(!$this->daemon || $this->daemonData['status'] !== 'started') {
$this->abortWithError('not running');
}
if(!isset($this->payload['token']) ||
$this->payload['token'] !== $this->daemonData['token']
) {
$this->abortWithError('invalid token');
}
$worker = new Worker();
$worker->process();
$elapsedTime = microtime(true) - $this->timer;
if ($elapsedTime < 30) {
sleep(30 - $elapsedTime);
}
// after each execution, read daemon in case it's status was modified
list($daemon, $daemonData) = $this->getDaemon();
$daemonData['counter']++;
$daemonData['token'] = $this->refreshedToken;
$daemon->value = json_encode($daemonData);
$daemon->save();
$this->callSelf();
}
function getDaemon() {
$daemon = Setting::where('name', 'daemon')
->findOne();
return array(
($daemon) ? $daemon : null,
($daemon) ? json_decode($daemon->value, true) : null
);
}
function refreshToken() {
return Security::generateRandomString(5);
}
function manageSession($action) {
switch ($action) {
case 'start':
if(session_id()) {
session_write_close();
}
session_id($this->payload['session']);
session_start();
break;
case 'end':
session_write_close();
break;
}
}
function callSelf() {
$payload = json_encode(array('token' => $this->refreshedToken));
Supervisor::getRemoteUrl(
'/?mailpoet-api&section=queue&action=run&payload=' . urlencode($payload)
);
exit;
}
function abortWithError($error) {
wp_send_json(
array(
'result' => false,
'error' => $error
));
exit;
}
}

89
lib/Queue/Supervisor.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace MailPoet\Queue;
use Carbon\Carbon;
use MailPoet\Config\Env;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit;
class Supervisor {
function __construct($forceStart = false) {
$this->forceStart = $forceStart;
if(!Env::isPluginActivated()) {
throw new \Exception('Database has not been configured.');
}
list ($this->daemon, $this->daemonData) = $this->getDaemon();
}
function checkDaemon() {
if(!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])) return;
if(!$this->daemon) {
return $this->startDaemon();
} else {
if(!$this->forceStart && ($this->daemonData['status'] === 'paused' ||
$this->daemonData['status'] === 'stopped'
)
) {
return;
}
$currentTime = Carbon::now('UTC');
$lastUpdateTime = Carbon::createFromFormat(
'Y-m-d H:i:s',
$this->daemon->updated_at, 'UTC'
);
$timeSinceLastStart = $currentTime->diffInSeconds($lastUpdateTime);
if($timeSinceLastStart < 50) return;
$this->daemonData['status'] = 'paused';
$this->daemon->value = json_encode($this->daemonData);
$this->daemon->save();
return $this->startDaemon();
}
}
function startDaemon() {
if(!session_id()) session_start();
$sessionId = session_id();
session_write_close();
$_SESSION['daemon'] = null;
$payload = json_encode(array('session' => $sessionId));
self::getRemoteUrl(
'/?mailpoet-api&section=queue&action=start&payload=' . urlencode($payload)
);
session_start();
$daemonStatus = $_SESSION['daemon'];
unset($_SESSION['daemon']);
session_write_close();
return $daemonStatus;
}
function getDaemon() {
$daemon = Setting::where('name', 'daemon')
->findOne();
$daemonData = ($daemon) ? json_decode($daemon->value, true) : false;
return array(
$daemon,
$daemonData
);
}
static function getRemoteUrl($url) {
$args = array(
'timeout' => 1,
'user-agent' => 'MailPoet (www.mailpoet.com)'
);
wp_remote_get(
self::getSiteUrl() . $url,
$args
);
}
static function getSiteUrl() {
if(preg_match('!:\d+/!', site_url())) return site_url();
preg_match('!http://(?P<host>.*?):(?P<port>\d+)!', site_url(), $server);
$fp = @fsockopen($server['host'], $server['port'], $errno, $errstr, 1);
return ($fp) ?
site_url() :
preg_replace('/(?=:\d+):\d+/', '$1', site_url());
}
}

62
lib/Queue/Worker.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace MailPoet\Queue;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterStatistics;
use MailPoet\Models\Queue;
if(!defined('ABSPATH')) exit;
class Worker {
function __construct($timer = false) {
$this->timer = $timer;
$this->timer = microtime(true);
}
function process() {
$queues =
Queue::orderByDesc('priority')
->whereNotIn('status', array(
'paused',
'completed'
))
->findResultSet();
foreach ($queues as $queue) {
$newsletter = Newsletter::findOne($queue->newsletter_id)
->asArray();
$subscribers = json_decode($queue->subscribers, true);
if(!isset($subscribers['failed'])) $subscribers['failed'] = array();
if(!isset($subscribers['processed'])) $subscribers['processed'] = array();
$subscribersToProcess = $subscribers['to_process'];
foreach ($subscribersToProcess as $subscriber) {
$elapsedTime = microtime(true) - $this->timer;
if($elapsedTime >= 28) break;
// TODO: hook up to mailer
sleep(1);
$newsletterStatistics = NewsletterStatistics::create();
$newsletterStatistics->subscriber_id = $subscriber;
$newsletterStatistics->newsletter_id = $newsletter['id'];
$newsletterStatistics->queue_id = $queue->id;
$newsletterStatistics->save();
$subscribers['processed'][] = $subscriber;
$subscribers['to_process'] = array_values(
array_diff(
$subscribers['to_process'],
$subscribers['processed']
)
);
$queue->count_processed = count($subscribers['processed']);
$queue->count_to_process = count($subscribers['to_process']);
$queue->count_failed = count($subscribers['failed']);
$queue->count_total =
$queue->count_processed + $queue->count_to_process + $queue->count_failed;
if(!$queue->count_to_process) {
$queue->processed_at = date('Y-m-d H:i:s');
$queue->status = 'completed';
}
$queue->subscribers = json_encode($subscribers);
$queue->save();
}
}
}
}

51
lib/Router/Queue.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace MailPoet\Router;
use MailPoet\Models\Setting;
use MailPoet\Queue\Daemon;
use MailPoet\Queue\Supervisor;
if(!defined('ABSPATH')) exit;
class Queue {
function start() {
$supervisor = new Supervisor();
wp_send_json(
array(
'result' => ($supervisor->checkDaemon($forceStart = true)) ?
true :
false
)
);
}
function update($data) {
switch ($data['action']) {
case 'stop':
$status = 'stopped';
break;
default:
$status = 'paused';
break;
}
$daemon = new Daemon();
if(!$daemon->daemon || $daemon->daemonData['status'] !== 'started') {
$result = false;
} else {
$daemon->daemonData['status'] = $status;
$daemon->daemon->value = json_encode($daemon->daemonData);
$result = $daemon->daemon->save();
}
wp_send_json(
array(
'result' => $result
)
);
}
function getQueueStatus() {
$daemon = new \MailPoet\Queue\BootStrapMenu();
wp_send_json($daemon->bootStrap());
}
}

View File

@ -2,7 +2,9 @@
namespace MailPoet\Router;
use \MailPoet\Models\Segment;
use \MailPoet\Models\SubscriberSegment;
use \MailPoet\Models\SegmentFilter;
use \MailPoet\Listing;
use \MailPoet\Segments\WP;
if(!defined('ABSPATH')) exit;
@ -42,15 +44,15 @@ class Segments {
'subscribers'
)
->select_expr(
'SUM(CASE status WHEN "subscribed" THEN 1 ELSE 0 END)',
'SUM(CASE subscribers.status WHEN "subscribed" THEN 1 ELSE 0 END)',
'subscribed'
)
->select_expr(
'SUM(CASE status WHEN "unsubscribed" THEN 1 ELSE 0 END)',
'SUM(CASE subscribers.status WHEN "unsubscribed" THEN 1 ELSE 0 END)',
'unsubscribed'
)
->select_expr(
'SUM(CASE status WHEN "unconfirmed" THEN 1 ELSE 0 END)',
'SUM(CASE subscribers.status WHEN "unconfirmed" THEN 1 ELSE 0 END)',
'unconfirmed'
)
->findOne()->asArray();
@ -135,6 +137,12 @@ class Segments {
wp_send_json($result);
}
function synchronize() {
$result = WP::synchronizeUsers();
wp_send_json($result);
}
function bulkAction($data = array()) {
$bulk_action = new Listing\BulkAction(
'\MailPoet\Models\Segment',

88
lib/Segments/WP.php Normal file
View File

@ -0,0 +1,88 @@
<?php
namespace MailPoet\Segments;
use \MailPoet\Models\Subscriber;
use \MailPoet\Models\Segment;
class WP {
static function synchronizeUser($wp_user_id) {
$wpUser = \get_userdata($wp_user_id);
if($wpUser === false) return;
$subscriber = Subscriber::where('wp_user_id', $wpUser->ID)
->findOne();
switch(current_filter()) {
case 'delete_user':
case 'deleted_user':
case 'remove_user_from_blog':
if($subscriber !== false && $subscriber->id()) {
$subscriber->delete();
}
break;
case 'user_register':
case 'added_existing_user':
case 'profile_update':
default:
// get first name & last name
$first_name = $wpUser->first_name;
$last_name = $wpUser->last_name;
if(empty($wpUser->first_name) && empty($wpUser->last_name)) {
$first_name = $wpUser->display_name;
}
// subscriber data
$data = array(
'wp_user_id'=> $wpUser->ID,
'email' => $wpUser->user_email,
'first_name' => $first_name,
'last_name' => $last_name,
'status' => 'subscribed'
);
if($subscriber !== false) {
$data['id'] = $subscriber->id();
}
$subscriber = Subscriber::createOrUpdate($data);
if($subscriber !== false && $subscriber->id()) {
$segment = Segment::getWPUsers();
$segment->addSubscriber($subscriber->id());
}
break;
}
}
static function synchronizeUsers() {
// get wordpress users list
$segment = Segment::getWPUsers();
// count WP users
$users_count = \count_users()['total_users'];
$linked_subscribers_count = $segment->subscribers()->count();
if($users_count !== $linked_subscribers_count) {
$linked_subscribers = Subscriber::select('wp_user_id')
->whereNotNull('wp_user_id')
->findArray();
$exclude_ids = array();
if(!empty($linked_subscribers)) {
$exclude_ids = array_map(function($subscriber) {
return $subscriber['wp_user_id'];
}, $linked_subscribers);
}
$users = \get_users(array(
'count_total' => false,
'fields' => 'ID',
'exclude' => $exclude_ids
));
foreach($users as $user) {
static::synchronizeUser($user);
}
}
return true;
}
}

View File

@ -9,7 +9,6 @@ use MailPoet\Models\SubscriberSegment;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Util\Helpers;
use MailPoet\Util\XLSXWriter;
use Symfony\Component\Console\Helper\Helper;
class Export {
public function __construct($data) {
@ -93,7 +92,12 @@ class Export {
);
$lastSegment = $subscriber['segment_name'];
}
$writer->writeSheet(array_merge($headerRow, $rows), 'MailPoet');
$writer->writeSheet(
array_merge($headerRow, $rows),
($this->groupBySegmentOption) ?
ucwords($subscriber['segment_name']) :
'MailPoet'
);
$writer->writeToFile($this->exportFile);
}
} catch (Exception $e) {
@ -155,6 +159,8 @@ class Export {
$subscribers =
$subscribers->where(Subscriber::$_table . '.status', 'subscribed');
}
$subscribers = $subscribers->whereNull(Subscriber::$_table . '.deleted_at');
return $subscribers->findArray();
}

View File

@ -1,10 +1,10 @@
<?php
namespace MailPoet\Subscribers\ImportExport\Import;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberCustomField;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Util\Helpers;
class Import {
@ -27,7 +27,9 @@ class Import {
$subscriberFields = $this->subscriberFields;
$subscriberCustomFields = $this->subscriberCustomFields;
$subscribersData = $this->subscribersData;
$subscribersData = $this->filterSubscriberStatus($subscribersData);
list ($subscribersData, $subscriberFields) =
$this->filterSubscriberStatus($subscribersData, $subscriberFields);
$this->deleteExistingTrashedSubscribers($subscribersData);
list($subscribersData, $subscriberFields) = $this->extendSubscribersAndFields(
$subscribersData, $subscriberFields
);
@ -48,7 +50,7 @@ class Import {
$updatedSubscribers =
$this->createOrUpdateSubscribers(
'update',
$existingSubscribers,
$existingSubscribers,
$subscriberFields,
$subscriberCustomFields
);
@ -84,6 +86,7 @@ class Import {
array_map(function ($subscriberEmails) {
return Subscriber::selectMany(array('email'))
->whereIn('email', $subscriberEmails)
->whereNull('deleted_at')
->findArray();
}, array_chunk($subscribersData['email'], 200))
);
@ -131,6 +134,25 @@ class Import {
);
}
function deleteExistingTrashedSubscribers($subscribersData) {
$existingTrashedRecords = array_filter(
array_map(function ($subscriberEmails) {
return Subscriber::selectMany(array('id'))
->whereIn('email', $subscriberEmails)
->whereNotNull('deleted_at')
->findArray();
}, array_chunk($subscribersData['email'], 200))
);
if(!$existingTrashedRecords) return;
$existingTrashedRecords = Helpers::flattenArray($existingTrashedRecords);
foreach (array_chunk($existingTrashedRecords, 200) as $subscriberIds) {
Subscriber::whereIn('id', $subscriberIds)
->deleteMany();
SubscriberSegment::whereIn('subscriber_id', $subscriberIds)
->deleteMany();
}
}
function extendSubscribersAndFields($subscribersData, $subscriberFields) {
$subscribersData['created_at'] = $this->filterSubscriberCreatedAtDate();
$subscriberFields[] = 'created_at';
@ -164,8 +186,16 @@ class Import {
return array_fill(0, $this->subscribersCount, $this->currentTime);
}
function filterSubscriberStatus($subscribersData) {
if(!in_array('status', $this->subscriberFields)) return $subscribersData;
function filterSubscriberStatus($subscribersData, $subscriberFields) {
if(!in_array('status', $subscriberFields)) {
$subscribersData['status'] =
array_fill(0, count($subscribersData['email']), 'subscribed');
$subscriberFields[] = 'status';
return array(
$subscribersData,
$subscriberFields
);
}
$statuses = array(
'subscribed' => array(
'subscribed',
@ -198,7 +228,10 @@ class Import {
}
return 'subscribed'; // make "subscribed" a default status
}, $subscribersData['status']);
return $subscribersData;
return array(
$subscribersData,
$subscriberFields
);
}
function createOrUpdateSubscribers(

View File

@ -76,12 +76,18 @@ class Helpers {
static function getMaxPostSize($bytes = false) {
$maxPostSize = ini_get('post_max_size');
if(!$bytes) return $maxPostSize;
switch (substr ($maxPostSize, -1))
{
case 'M': case 'm': return (int)$maxPostSize * 1048576;
case 'K': case 'k': return (int)$maxPostSize * 1024;
case 'G': case 'g': return (int)$maxPostSize * 1073741824;
default: return $maxPostSize;
switch (substr($maxPostSize, -1)) {
case 'M':
case 'm':
return (int) $maxPostSize * 1048576;
case 'K':
case 'k':
return (int) $maxPostSize * 1024;
case 'G':
case 'g':
return (int) $maxPostSize * 1073741824;
default:
return $maxPostSize;
}
}
@ -164,4 +170,18 @@ class Helpers {
}
return $resultArray;
}
static function underscoreToCamelCase($str, $capitalise_first_char = false) {
if($capitalise_first_char) {
$str[0] = strtoupper($str[0]);
}
$func = create_function('$c', 'return strtoupper($c[1]);');
return preg_replace_callback('/_([a-z])/', $func, $str);
}
static function camelCaseToUnderscore($str) {
$str[0] = strtolower($str[0]);
$func = create_function('$c', 'return "_" . strtolower($c[1]);');
return preg_replace_callback('/([A-Z])/', $func, $str);
}
}

View File

@ -1,8 +1,14 @@
<?php
namespace MailPoet\Util;
require_once(ABSPATH . 'wp-includes/pluggable.php');
class Security {
static function generateToken() {
return wp_create_nonce('mailpoet_token');
}
static function generateRandomString($length) {
return substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, $length);
}
}

View File

@ -5,7 +5,8 @@
},
"napa": {
"blob": "eligrey/Blob.js.git",
"filesaver": "eligrey/FileSaver.js.git"
"filesaver": "eligrey/FileSaver.js.git",
"sticky-kit": "leafo/sticky-kit.git"
},
"dependencies": {
"backbone": "1.2.3",

View File

@ -1,10 +1,10 @@
<?php
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Models\CustomField;
use MailPoet\Models\Segment;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
class BootStrapMenuCest {
function _before() {
@ -22,13 +22,13 @@ class BootStrapMenuCest {
array(
'first_name' => 'John',
'last_name' => 'Mailer',
'status' => 0,
'status' => 'unconfirmed',
'email' => 'john@mailpoet.com'
),
array(
'first_name' => 'Mike',
'last_name' => 'Smith',
'status' => 1,
'status' => 'subscribed',
'email' => 'mike@maipoet.com'
)
);
@ -52,6 +52,28 @@ class BootStrapMenuCest {
expect($segments[1]['subscriberCount'])->equals(1);
}
function itCanGetSegmentsForImportWithoutTrashedSubscribers() {
$this->_createSegmentsAndSubscribers();
$segments = $this->bootStrapImportMenu->getSegments();
expect(count($segments))->equals(2);
$subscriber = Subscriber::findOne(1);
$subscriber->deleted_at = date('Y-m-d H:i:s');
$subscriber->save();
$segments = $this->bootStrapImportMenu->getSegments();
expect(count($segments))->equals(1);
}
function itCanGetSegmentsForExportWithoutTrashedSubscribers() {
$this->_createSegmentsAndSubscribers();
$segments = $this->bootStrapExportMenu->getSegments();
expect(count($segments))->equals(2);
$subscriber = Subscriber::findOne(1);
$subscriber->deleted_at = date('Y-m-d H:i:s');
$subscriber->save();
$segments = $this->bootStrapExportMenu->getSegments();
expect(count($segments))->equals(1);
}
function itCanGetSegmentsForExport() {
$this->_createSegmentsAndSubscribers();
$segments = $this->bootStrapExportMenu->getSegments();
@ -270,13 +292,9 @@ class BootStrapMenuCest {
}
function _after() {
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(CustomField::$_table)
->deleteMany();
ORM::forTable(Segment::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)
->deleteMany();
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . Segment::$_table);
ORM::raw_execute('TRUNCATE ' . SubscriberSegment::$_table);
ORM::raw_execute('TRUNCATE ' . CustomField::$_table);
}
}

View File

@ -105,28 +105,39 @@ class ImportCest {
expect($fields)->equals(array(39));
}
function itCanFilterSubscriberState() {
$data = array(
function itCanFilterSubscriberStatus() {
$subscibersData = $this->subscribersData;
$subscriberFields = $this->subscriberFields;
list($subscibersData, $subsciberFields) =
$this->import->filterSubscriberStatus($subscibersData, $subscriberFields);
// subscribers' status was set to "subscribed" & status column was added
// to subscribers fields
expect(array_pop($subsciberFields))->equals('status');
expect($subscibersData['status'][0])->equals('subscribed');
expect(count($subscibersData['status']))->equals(2);
$subscriberFields[] = 'status';
$subscibersData = array(
'status' => array(
//subscribed
#subscribed
'subscribed',
'confirmed',
1,
'1',
'true',
//unconfirmed
#unconfirmed
'unconfirmed',
0,
"0",
//unsubscribed
#unsubscribed
'unsubscribed',
-1,
'-1',
'false'
),
);
$statuses = $this->import->filterSubscriberStatus($data);
expect($statuses)->equals(
list($subscibersData, $subsciberFields) =
$this->import->filterSubscriberStatus($subscibersData, $subscriberFields);
expect($subscibersData)->equals(
array(
'status' => array(
'subscribed',
@ -170,6 +181,41 @@ class ImportCest {
->equals($subscribersData['first_name'][1]);
}
function itCanDeleteTrashedSubscribers() {
$subscribersData = $this->subscribersData;
$subscriberFields = $this->subscriberFields;
$subscribersData['deleted_at'] = array(
null,
date('Y-m-d H:i:s')
);
$subscriberFields[] = 'deleted_at';
$this->import->createOrUpdateSubscribers(
'create',
$subscribersData,
$subscriberFields,
false
);
$dbSubscribers = Helpers::arrayColumn(
Subscriber::select('id')
->findArray(),
'id'
);
expect(count($dbSubscribers))->equals(2);
$this->import->addSubscribersToSegments(
$dbSubscribers,
$this->segments
);
$subscribersSegments = SubscriberSegment::findArray();
expect(count($subscribersSegments))->equals(4);
$this->import->deleteExistingTrashedSubscribers(
$subscribersData
);
$subscribersSegments = SubscriberSegment::findArray();
$dbSubscribers = Subscriber::findArray();
expect(count($subscribersSegments))->equals(2);
expect(count($dbSubscribers))->equals(1);
}
function itCanCreateOrUpdateCustomFields() {
$subscribersData = $this->subscribersData;
$this->import->createOrUpdateSubscribers(
@ -231,6 +277,22 @@ class ImportCest {
expect(count($subscribersSegments))->equals(4);
}
function itCanDeleteExistingTrashedSubscribers() {
$subscribersData = $this->subscribersData;
$subscriberFields = $this->subscriberFields;
$subscriberFields[] = 'deleted_at';
$subscribersData['deleted_at'] = array(
null,
date('Y-m-d H:i:s')
);
$this->import->createOrUpdateSubscribers(
'create',
$subscribersData,
$subscriberFields,
false
);
}
function itCanProcess() {
$import = clone($this->import);
$result = $import->process();
@ -253,11 +315,8 @@ class ImportCest {
}
function _after() {
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(SubscriberCustomField::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)
->deleteMany();
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SubscriberSegment::$_table);
ORM::raw_execute('TRUNCATE ' . SubscriberCustomField::$_table);
}
}

View File

@ -4,9 +4,15 @@
<h2 class="title">
<span id="mailpoet_form_name"><%= form.name %></span>
<input id="mailpoet_form_name_input" type="text" value="" style="display:none;" />
<span>
<a id="mailpoet_form_edit_name" class="button" href="javascript:;"><%= __('Edit name' ) %></a>
</span>
<a
id="mailpoet_form_edit_name"
class="add-new-h2"
href="javascript:;"
><%= __('Edit name' ) %></a>
<a
href="<%= admin_url('admin.php?page=mailpoet-forms') | raw %>"
class="add-new-h2"
><%= __('List of forms' ) %></a>
</h2>
<% endblock %>
@ -42,7 +48,7 @@
<div id="mailpoet_settings_segment_selection">
<!-- Form settings: list selection -->
<p>
<strong><%= __('This form adds subscribers to these lists:') %></strong>
<strong><%= __('This form adds subscribers to these lists:') %></strong>
</p>
<select
id="mailpoet_form_segments"

View File

@ -57,12 +57,10 @@
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Text') %></div>
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_button_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_button_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</div>
<div class="mailpoet_form_field">
<div class="mailpoet_form_field_input_option">

View File

@ -16,20 +16,16 @@
</label>
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="divider-color" class="mailpoet_field_divider_border_color mailpoet_color" value="{{ model.styles.block.borderColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Divider color') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="divider-color" class="mailpoet_field_divider_border_color mailpoet_color" value="{{ model.styles.block.borderColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Divider color') %></div>
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_divider_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_divider_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</div>
{{#ifCond renderOptions.hideApplyToAll '!==' true}}
<div class="mailpoet_form_field">

View File

@ -1,29 +1,25 @@
<h3><%= __('Footer') %></h3>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="font-color" id="mailpoet_field_footer_text_color" class="mailpoet_field_footer_text_color mailpoet_color" value="{{ model.styles.text.fontColor }}" />
<select id="mailpoet_field_footer_text_font_family" name="font-family" class="mailpoet_select mailpoet_select_medium mailpoet_field_footer_text_font_family mailpoet_font_family">
{{#each availableStyles.fonts}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontFamily}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
<select id="mailpoet_field_footer_text_size" name="font-size" class="mailpoet_select mailpoet_select_small mailpoet_field_footer_text_size mailpoet_font_size">
{{#each availableStyles.textSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Text') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="font-color" id="mailpoet_field_footer_text_color" class="mailpoet_field_footer_text_color mailpoet_color" value="{{ model.styles.text.fontColor }}" />
<select id="mailpoet_field_footer_text_font_family" name="font-family" class="mailpoet_select mailpoet_select_medium mailpoet_field_footer_text_font_family mailpoet_font_family">
{{#each availableStyles.fonts}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontFamily}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
<select id="mailpoet_field_footer_text_size" name="font-size" class="mailpoet_select mailpoet_select_small mailpoet_field_footer_text_size mailpoet_font_size">
{{#each availableStyles.textSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Text') %></div>
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" class="mailpoet_color" size="6" maxlength="6" name="link-color" value="{{ model.styles.link.fontColor }}" id="mailpoet_field_footer_link_color" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Links') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" class="mailpoet_color" size="6" maxlength="6" name="link-color" value="{{ model.styles.link.fontColor }}" id="mailpoet_field_footer_link_color" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Links') %></div>
<label>
<div class="mailpoet_form_field_checkbox_option mailpoet_option_offset_left_small">
<input type="checkbox" name="underline" value="underline" id="mailpoet_field_footer_link_underline" {{#ifCond model.styles.link.textDecoration '==' 'underline'}}CHECKED{{/ifCond}}/> <%= __('Underline') %>
@ -32,12 +28,10 @@
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_footer_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_footer_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</div>
<div class="mailpoet_form_field">

View File

@ -1,29 +1,25 @@
<h3><%= __('Header') %></h3>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="font-color" id="mailpoet_field_header_text_color" class="mailpoet_field_header_text_color mailpoet_color" value="{{ model.styles.text.fontColor }}" />
<select id="mailpoet_field_header_text_font_family" name="font-family" class="mailpoet_select mailpoet_select_medium mailpoet_field_header_text_font_family mailpoet_font_family">
{{#each availableStyles.fonts}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontFamily}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
<select id="mailpoet_field_header_text_size" name="font-size" class="mailpoet_select mailpoet_select_small mailpoet_field_header_text_size mailpoet_font_size">
{{#each availableStyles.textSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Text') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="font-color" id="mailpoet_field_header_text_color" class="mailpoet_field_header_text_color mailpoet_color" value="{{ model.styles.text.fontColor }}" />
<select id="mailpoet_field_header_text_font_family" name="font-family" class="mailpoet_select mailpoet_select_medium mailpoet_field_header_text_font_family mailpoet_font_family">
{{#each availableStyles.fonts}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontFamily}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
<select id="mailpoet_field_header_text_size" name="font-size" class="mailpoet_select mailpoet_select_small mailpoet_field_header_text_size mailpoet_font_size">
{{#each availableStyles.textSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.styles.text.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select>
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Text') %></div>
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" class="mailpoet_color" size="6" maxlength="6" name="link-color" value="{{ model.styles.link.fontColor }}" id="mailpoet_field_header_link_color" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Links') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" class="mailpoet_color" size="6" maxlength="6" name="link-color" value="{{ model.styles.link.fontColor }}" id="mailpoet_field_header_link_color" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Links') %></div>
<label>
<div class="mailpoet_form_field_checkbox_option mailpoet_option_offset_left_small">
<input type="checkbox" name="underline" value="underline" id="mailpoet_field_header_link_underline" {{#ifCond model.styles.link.textDecoration '==' 'underline'}}CHECKED{{/ifCond}}/> <%= __('Underline') %>
@ -32,12 +28,10 @@
</div>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_header_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_header_background_color mailpoet_color" value="{{ model.styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</div>
<div class="mailpoet_form_field">

View File

@ -1,11 +1,9 @@
<h3><%= __('Spacer') %></h3>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_spacer_background_color mailpoet_color" value="{{ styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</label>
<div class="mailpoet_form_field_input_option">
<input type="text" name="background-color" class="mailpoet_field_spacer_background_color mailpoet_color" value="{{ styles.block.backgroundColor }}" />
</div>
<div class="mailpoet_form_field_title mailpoet_form_field_title_inline"><%= __('Background') %></div>
</div>
<div class="mailpoet_form_field">

View File

@ -15,7 +15,7 @@
{{#each availableStyles.textSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.text.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select> <label for="mailpoet_text_font_color"><%= __('Text') %></label>
</select> <%= __('Text') %>
</div>
<div class="mailpoet_form_field">
<span>
@ -30,7 +30,7 @@
{{#each availableStyles.headingSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.h1.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select> <label for="mailpoet_h1_font_color"><%= __('Heading 1') %></label>
</select> <%= __('Heading 1') %>
</div>
<div class="mailpoet_form_field">
<span>
@ -45,7 +45,7 @@
{{#each availableStyles.headingSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.h2.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select> <label for="mailpoet_h2_font_color"><%= __('Heading 2') %></label>
</select> <%= __('Heading 2') %>
</div>
<div class="mailpoet_form_field">
<span>
@ -60,23 +60,23 @@
{{#each availableStyles.headingSizes}}
<option value="{{ this }}" {{#ifCond this '==' ../model.h3.fontSize}}SELECTED{{/ifCond}}>{{ this }}</option>
{{/each}}
</select> <label for="mailpoet_h3_font_color"><%= __('Heading 3') %></label>
</select> <%= __('Heading 3') %>
</div>
<div class="mailpoet_form_field">
<span>
<span><input type="text" class="mailpoet_color" size="6" maxlength="6" name="link-color" value="{{ model.link.fontColor }}" id="mailpoet_a_font_color"></span>
</span><label for="mailpoet_a_font_color"><%= __('Links') %></label> <input type="checkbox" name="underline" value="underline" id="mailpoet_a_font_underline" {{#ifCond model.link.textDecoration '==' 'underline'}}CHECKED{{/ifCond}} class="mailpoet_option_offset_left_small"/> <%= __('Underline') %>
</span><%= __('Links') %> <label><input type="checkbox" name="underline" value="underline" id="mailpoet_a_font_underline" {{#ifCond model.link.textDecoration '==' 'underline'}}CHECKED{{/ifCond}} class="mailpoet_option_offset_left_small"/> <%= __('Underline') %></label>
</div>
<hr />
<div class="mailpoet_form_field">
<span>
<span><input type="text" class="mailpoet_color" size="6" maxlength="6" name="newsletter-color" value="{{ model.wrapper.backgroundColor }}" id="mailpoet_newsletter_background_color"></span>
</span><label for="mailpoet_newsletter_background_color"><%= __('Newsletter') %></label>
</span><%= __('Newsletter') %>
</div>
<div class="mailpoet_form_field">
<span>
<span><input type="text" class="mailpoet_color" size="6" maxlength="6" name="background-color" value="{{ model.body.backgroundColor }}" id="mailpoet_background_color"></span>
</span><label for="mailpoet_background_color"><%= __('Background') %></label>
</span><%= __('Background') %>
</div>
</form>
</div>

10
views/queue.html Normal file
View File

@ -0,0 +1,10 @@
<% extends 'layout.html' %>
<% block content %>
<div id="queue_container"></div>
<script type="text/javascript">
</script>
<script>
var queueDaemon = <%= daemon|raw %>
</script>
<% endblock %>

View File

@ -123,7 +123,7 @@
exportData = {
segments: segments.length || null,
segmentsWithConfirmedSubscribers: segmentsWithConfirmedSubscribers.length || null,
exportConfirmedOption: false,
exportConfirmedOption: true,
groupBySegmentOption: (segments.length > 1 || segmentsWithConfirmedSubscribers.length > 1) ? true : null
};
</script>

View File

@ -1,5 +1,4 @@
<% extends 'layout.html' %>
<% block content %>
<div id="mailpoet_subscribers_import" class="wrap">
<h2 class="title">
@ -7,11 +6,11 @@
<a class="add-new-h2" href="?page=mailpoet-subscribers#/"><%= __('Back to list') %></a>
</h2>
<!-- STEP 1: method selection -->
<% include 'import/step1.html' %>
<% include 'subscribers/importExport/import/step1.html' %>
<!-- STEP 2: subscriber data manipulation -->
<% include 'import/step2.html' %>
<% include 'subscribers/importExport/import/step2.html' %>
<!-- STEP 3: results -->
<% include 'import/step3.html' %>
<% include 'subscribers/importExport/import/step3.html' %>
</div>
<%= stylesheet('importExport.css') %>
@ -64,6 +63,7 @@
<script type="text/javascript">
var
maxPostSize = '<%= maxPostSize %>',
maxPostSizeBytes = '<%= maxPostSizeBytes %>',
importData = {},
mailpoetColumnsSelect2 = <%= subscriberFieldsSelect2|raw %>,
mailpoetColumns = <%= subscriberFields|raw %>,

View File

@ -29,7 +29,8 @@ baseConfig = {
'blob$': 'blob/Blob.js',
'filesaver$': 'filesaver/FileSaver.js',
'papaparse': 'papaparse/papaparse.min.js',
'helpscout': 'helpscout.js'
'helpscout': 'helpscout.js',
'html2canvas': 'html2canvas/dist/html2canvas.js'
},
},
node: {
@ -73,6 +74,10 @@ baseConfig = {
include: /helpscout.js$/,
loader: 'exports-loader?window.HS',
},
{
include: /html2canvas.js$/,
loader: 'expose-loader?html2canvas',
},
]
}
};
@ -95,9 +100,10 @@ config.push(_.extend({}, baseConfig, {
'segments/segments.jsx',
'forms/forms.jsx',
'settings/tabs.js',
'import/import.js',
'export/export.js',
'helpscout'
'subscribers/importExport/import.js',
'subscribers/importExport/export.js',
'helpscout',
'queue.jsx'
],
form_editor: [
'form_editor/form_editor.js',