Merge pull request #543 from mailpoet/api_refactor

API refactor
This commit is contained in:
Tautvidas Sipavičius
2016-07-08 13:55:01 +03:00
committed by GitHub
15 changed files with 231 additions and 132 deletions

83
lib/API/API.php Normal file
View File

@@ -0,0 +1,83 @@
<?php
namespace MailPoet\API;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class API {
public $api_request;
public $endpoint;
public $action;
public $data;
const NAME = 'mailpoet_api';
const ENDPOINT_NAMESPACE = '\MailPoet\API\Endpoints\\';
const RESPONSE_ERROR = 404;
function __construct($api_data = false) {
$api_data = ($api_data) ? $api_data : $_GET;
$this->api_request = isset($api_data[self::NAME]);
$this->endpoint = isset($api_data['endpoint']) ?
Helpers::underscoreToCamelCase($api_data['endpoint']) :
false;
$this->action = isset($api_data['action']) ?
Helpers::underscoreToCamelCase($api_data['action']) :
false;
$this->data = isset($api_data['data']) ?
self::decodeRequestData($api_data['data']) :
false;
}
function init() {
$endpoint = self::ENDPOINT_NAMESPACE . ucfirst($this->endpoint);
if(!$this->api_request) return;
if(!$this->endpoint || !class_exists($endpoint)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
}
$this->callEndpoint(
$endpoint,
$this->action,
$this->data
);
}
function callEndpoint($endpoint, $action, $data) {
if(!method_exists($endpoint, $action)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
}
call_user_func(
array(
$endpoint,
$action
),
$data
);
}
static function decodeRequestData($data) {
$data = base64_decode($data);
return (is_serialized($data)) ?
unserialize($data) :
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API data format.'));
}
static function encodeRequestData($data) {
return rtrim(base64_encode(serialize($data)), '=');
}
static function buildRequest($endpoint, $action, $data) {
$data = self::encodeRequestData($data);
$params = array(
self::NAME => '',
'endpoint' => $endpoint,
'action' => $action,
'data' => $data
);
return add_query_arg($params, home_url());
}
function terminateRequest($code, $message) {
status_header($code, $message);
exit;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Cron\Daemon;
if(!defined('ABSPATH')) exit;
class Queue {
const ENDPOINT = 'queue';
const ACTION_RUN = 'run';
static function run($data) {
$queue = new Daemon($data);
$queue->run();
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Subscription as UserSubscription;
if(!defined('ABSPATH')) exit;
class Subscription {
const ENDPOINT = 'subscription';
static function confirm($data) {
$subscription = new UserSubscription\Pages('confirm', $data);
}
static function manage($data) {
$subscription = new UserSubscription\Pages('manage', $data);
}
static function unsubscribe($data) {
$subscription = new UserSubscription\Pages('unsubscribe', $data);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
if(!defined('ABSPATH')) exit;
class Track {
const ENDPOINT = 'track';
const ACTION_CLICK = 'click';
const ACTION_OPEN = 'open';
static function click($data) {
$clicks = new Clicks($data);
$clicks->track();
}
static function open($data) {
$opens = new Opens($data);
$opens->track();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Newsletter\ViewInBrowser as NewsletterViewInBrowser;
if(!defined('ABSPATH')) exit;
class ViewInBrowser {
const ENDPOINT = 'view_in_browser';
const ACTION_VIEW = 'view';
static function view($data) {
$viewer = new NewsletterViewInBrowser($data);
$viewer->view();
}
}

View File

@@ -48,7 +48,7 @@ class Initializer {
function onInit() { function onInit() {
$this->setupRouter(); $this->setupRouter();
$this->setupPublicAPI(); $this->setupAPI();
$this->setupPages(); $this->setupPages();
} }
@@ -165,9 +165,9 @@ class Initializer {
$hooks->init(); $hooks->init();
} }
function setupPublicAPI() { function setupAPI() {
$publicAPI = new PublicAPI(); $API = new \MailPoet\API\API();
$publicAPI->init(); $API->init();
} }
function runQueueSupervisor() { function runQueueSupervisor() {

View File

@@ -1,77 +0,0 @@
<?php
namespace MailPoet\Config;
use MailPoet\Cron\Daemon;
use MailPoet\Newsletter\ViewInBrowser;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
use MailPoet\Subscription;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class PublicAPI {
public $api;
public $endpoint;
public $action;
public $data;
function __construct() {
# http://example.com/?mailpoet&endpoint=&action=&data=
$this->api = isset($_GET['mailpoet']) ? true : false;
$this->endpoint = isset($_GET['endpoint']) ?
Helpers::underscoreToCamelCase($_GET['endpoint']) :
false;
$this->action = isset($_GET['action']) ?
Helpers::underscoreToCamelCase($_GET['action']) :
false;
$this->data = isset($_GET['data']) ?
unserialize(base64_decode($_GET['data'])) :
false;
}
function init() {
if(!$this->api && !$this->endpoint) return;
$this->_checkAndCallMethod($this, $this->endpoint, $terminate_request = true);
}
function queue() {
$queue = new Daemon($this->data);
$this->_checkAndCallMethod($queue, $this->action);
}
function subscription() {
$subscription = new Subscription\Pages($this->action, $this->data);
$this->_checkAndCallMethod($subscription, $this->action);
}
function track() {
if($this->action === 'click') {
$track_class = new Clicks($this->data);
}
if($this->action === 'open') {
$track_class = new Opens($this->data);
}
if(!isset($track_class)) return;
$track_class->track();
}
function viewInBrowser() {
$viewer = new ViewInBrowser($this->data);
$viewer->view();
}
private function _checkAndCallMethod($class, $method, $terminate_request = false) {
if(!method_exists($class, $method)) {
if(!$terminate_request) return;
header('HTTP/1.0 404 Not Found');
exit;
}
call_user_func(
array(
$class,
$method
)
);
}
}

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Cron; namespace MailPoet\Cron;
use MailPoet\API\API;
use MailPoet\API\Endpoints\Queue as QueueAPI;
use MailPoet\Models\Setting; use MailPoet\Models\Setting;
use MailPoet\Util\Security; use MailPoet\Util\Security;
@@ -14,7 +16,6 @@ class CronHelper {
static function createDaemon($token) { static function createDaemon($token) {
$daemon = array( $daemon = array(
'status' => Daemon::STATUS_STARTING, 'status' => Daemon::STATUS_STARTING,
'counter' => 0,
'token' => $token 'token' => $token
); );
self::saveDaemon($daemon); self::saveDaemon($daemon);
@@ -38,17 +39,17 @@ class CronHelper {
} }
static function accessDaemon($token, $timeout = self::DAEMON_REQUEST_TIMEOUT) { static function accessDaemon($token, $timeout = self::DAEMON_REQUEST_TIMEOUT) {
$data = serialize(array('token' => $token)); $data = array('token' => $token);
$url = '/?mailpoet&endpoint=queue&action=run&data=' . $url = API::buildRequest(
base64_encode($data); QueueAPI::ENDPOINT,
QueueAPI::ACTION_RUN,
$data
);
$args = array( $args = array(
'timeout' => $timeout, 'timeout' => $timeout,
'user-agent' => 'MailPoet (www.mailpoet.com) Cron' 'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
); );
$result = wp_remote_get( $result = wp_remote_get($url, $args);
self::getSiteUrl() . $url,
$args
);
return wp_remote_retrieve_body($result); return wp_remote_retrieve_body($result);
} }

View File

@@ -30,7 +30,6 @@ class Daemon {
function run() { function run() {
$daemon = $this->daemon; $daemon = $this->daemon;
set_time_limit(0);
if(!$daemon) { if(!$daemon) {
$this->abortWithError(__('Daemon does not exist.')); $this->abortWithError(__('Daemon does not exist.'));
} }
@@ -39,6 +38,8 @@ class Daemon {
) { ) {
$this->abortWithError(__('Invalid or missing token.')); $this->abortWithError(__('Invalid or missing token.'));
} }
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon);
$this->abortIfStopped($daemon); $this->abortIfStopped($daemon);
try { try {
$scheduler = new SchedulerWorker($this->timer); $scheduler = new SchedulerWorker($this->timer);
@@ -55,27 +56,25 @@ class Daemon {
// after each execution, re-read daemon data in case its status was changed // after each execution, re-read daemon data in case its status was changed
// its status has changed // its status has changed
$daemon = CronHelper::getDaemon(); $daemon = CronHelper::getDaemon();
if(!$daemon || $daemon['token'] !== $this->data['token']) { if(!$daemon || $daemon['token'] !== $this->token) {
self::terminate(); $this->terminateRequest();
} }
$daemon['counter']++;
$this->abortIfStopped($daemon); $this->abortIfStopped($daemon);
if($daemon['status'] === self::STATUS_STARTING) { if($daemon['status'] === self::STATUS_STARTING) {
$daemon['status'] = self::STATUS_STARTED; $daemon['status'] = self::STATUS_STARTED;
} }
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon); CronHelper::saveDaemon($daemon);
$this->callSelf(); $this->callSelf();
} }
function abortIfStopped($daemon) { function abortIfStopped($daemon) {
if($daemon['status'] === self::STATUS_STOPPED) { if($daemon['status'] === self::STATUS_STOPPED) {
self::terminate(); $this->terminateRequest();
} }
if($daemon['status'] === self::STATUS_STOPPING) { if($daemon['status'] === self::STATUS_STOPPING) {
$daemon['status'] = self::STATUS_STOPPED; $daemon['status'] = self::STATUS_STOPPED;
CronHelper::saveDaemon($daemon); CronHelper::saveDaemon($daemon);
self::terminate(); $this->terminateRequest();
} }
} }
@@ -85,10 +84,10 @@ class Daemon {
function callSelf() { function callSelf() {
CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT); CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT);
self::terminate(); $this->terminateRequest();
} }
function terminate() { function terminateRequest() {
exit; exit;
} }
} }

View File

@@ -1,13 +1,15 @@
<?php <?php
namespace MailPoet\Newsletter\Links; namespace MailPoet\Newsletter\Links;
use MailPoet\API\API;
use MailPoet\API\Endpoints\Track as TrackAPI;
use MailPoet\Models\NewsletterLink; use MailPoet\Models\NewsletterLink;
use MailPoet\Newsletter\Shortcodes\Shortcodes; use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security; use MailPoet\Util\Security;
class Links { class Links {
const DATA_TAG = '[mailpoet_data]'; const DATA_TAG_CLICK = '[mailpoet_click_data]';
const DATA_TAG_OPEN = '[mailpoet_open_data]';
const HASH_LENGTH = 5; const HASH_LENGTH = 5;
static function extract($content) { static function extract($content) {
@@ -32,7 +34,7 @@ class Links {
// extract shortcodes with [link:*] format // extract shortcodes with [link:*] format
$shortcodes = new Shortcodes(); $shortcodes = new Shortcodes();
$shortcodes = $shortcodes->extract($content, $categories = array('link')); $shortcodes = $shortcodes->extract($content, $categories = array('link'));
$extracted_links = array_map(function($shortcode) { $extracted_links = array_map(function ($shortcode) {
return array( return array(
'html' => $shortcode, 'html' => $shortcode,
'link' => $shortcode 'link' => $shortcode
@@ -61,13 +63,9 @@ class Links {
'hash' => $hash, 'hash' => $hash,
'url' => $extracted_link['link'] 'url' => $extracted_link['link']
); );
$params = array( // replace link with a temporary data tag + hash
'mailpoet' => '', // it will be further replaced with the proper track API URL during sending
'endpoint' => 'track', $tracked_link = self::DATA_TAG_CLICK . '-' . $hash;
'action' => 'click',
'data' => self::DATA_TAG . '-' . $hash
);
$tracked_link = add_query_arg($params, home_url());
// first, replace URL in the extracted HTML source with encoded link // first, replace URL in the extracted HTML source with encoded link
$tracked_link_html_source = str_replace( $tracked_link_html_source = str_replace(
$extracted_link['link'], $tracked_link, $extracted_link['link'], $tracked_link,
@@ -99,12 +97,17 @@ class Links {
$queue_id, $queue_id,
$content $content
) { ) {
$regex = sprintf('/data=(%s(?:-\w+)?)/', preg_quote(self::DATA_TAG)); // match data tags
preg_match_all($regex, $content, $links); $regex = sprintf(
foreach($links[1] as $link) { '/((%s|%s)(?:-\w+)?)/',
preg_quote(self::DATA_TAG_CLICK),
preg_quote(self::DATA_TAG_OPEN)
);
preg_match_all($regex, $content, $matches);
foreach($matches[1] as $index => $match) {
$hash = null; $hash = null;
if(preg_match('/-/', $link)) { if(preg_match('/-/', $match)) {
list(, $hash) = explode('-', $link); list(, $hash) = explode('-', $match);
} }
$data = array( $data = array(
'newsletter' => $newsletter_id, 'newsletter' => $newsletter_id,
@@ -112,8 +115,15 @@ class Links {
'queue' => $queue_id, 'queue' => $queue_id,
'hash' => $hash 'hash' => $hash
); );
$data = rtrim(base64_encode(serialize($data)), '='); $API_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
$content = str_replace($link, $data, $content); TrackAPI::ACTION_CLICK :
TrackAPI::ACTION_OPEN;
$link = API::buildRequest(
TrackAPI::ENDPOINT,
$API_action,
$data
);
$content = str_replace($match, $link, $content);
} }
return $content; return $content;
} }

View File

@@ -9,10 +9,12 @@ class OpenTracking {
$DOM = new \pQuery(); $DOM = new \pQuery();
$DOM = $DOM->parseStr($template); $DOM = $DOM->parseStr($template);
$template = $DOM->query('body'); $template = $DOM->query('body');
// url is a temporary data tag that will be further replaced with
// the proper track API URL during sending
$url = Links::DATA_TAG_OPEN;
$open_tracking_image = sprintf( $open_tracking_image = sprintf(
'<img alt="" class="" src="%s/%s"/>', '<img alt="" class="" src="%s"/>',
home_url(), $url
esc_attr('?mailpoet&endpoint=track&action=open&data=' . Links::DATA_TAG)
); );
$template->html($template->html() . $open_tracking_image); $template->html($template->html() . $open_tracking_image);
return $DOM->__toString(); return $DOM->__toString();

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Newsletter; namespace MailPoet\Newsletter;
use MailPoet\API\API;
use MailPoet\API\Endpoints\ViewInBrowser as ViewInBrowserAPI;
use MailPoet\Models\Subscriber; use MailPoet\Models\Subscriber;
class Url { class Url {
@@ -32,11 +34,10 @@ class Url {
$queue['id'] : $queue['id'] :
$queue $queue
); );
$params = array( return API::buildRequest(
'mailpoet' => '', ViewInBrowserAPI::ENDPOINT,
'endpoint' => 'view_in_browser', ViewInBrowserAPI::ACTION_VIEW,
'data' => base64_encode(serialize($data)) $data
); );
return add_query_arg($params, home_url());
} }
} }

View File

@@ -70,7 +70,7 @@ class ViewInBrowser {
} }
private function abort() { private function abort() {
header('HTTP/1.0 404 Not Found'); status_header(404);
exit; exit;
} }
} }

View File

@@ -89,7 +89,7 @@ class Clicks {
} }
function abort() { function abort() {
header('HTTP/1.0 404 Not Found'); status_header(404);
exit; exit;
} }

View File

@@ -1,8 +1,10 @@
<?php <?php
namespace MailPoet\Subscription; namespace MailPoet\Subscription;
use \MailPoet\Models\Subscriber; use MailPoet\API\API;
use \MailPoet\Models\Setting; use MailPoet\API\Endpoints\Subscription;
use MailPoet\Models\Subscriber;
use MailPoet\Models\Setting;
class Url { class Url {
static function getConfirmationUrl($subscriber = false) { static function getConfirmationUrl($subscriber = false) {
@@ -43,9 +45,10 @@ class Url {
} }
$params = array( $params = array(
'endpoint=subscription', API::NAME,
'endpoint='.Subscription::ENDPOINT,
'action='.$action, 'action='.$action,
'data='.rtrim(base64_encode(serialize($data)), '=') 'data='.API::encodeRequestData($data)
); );
// add parameters // add parameters