Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,32 @@
<?php
/**
* Base_Manager
*
* Singleton class that handles hooks that need to be registered only once.
*
* @package WP_Ultimo
* @subpackage Managers/Base_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Adds a lighter ajax option to WP Ultimo.
*
* @since 1.9.14
*/
class Base_Manager {
/**
* A valid init method is required.
*
* @since 2.0.11
* @return void
*/
public function init() {} // end init;
} // end class Base_Manager;

View File

@ -0,0 +1,64 @@
<?php
/**
* Block Manager
*
* Manages the registering of gutenberg blocks.
*
* @package WP_Ultimo
* @subpackage Managers/Block
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles the ajax form registering, rendering, and permissions checking.
*
* @since 2.0.0
*/
class Block_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
global $wp_version;
$hook = version_compare($wp_version, '5.8', '<') ? 'block_categories' : 'block_categories_all';
add_filter($hook, array($this, 'add_wp_ultimo_block_category'), 1, 2);
} // end init;
/**
* Adds wp-ultimo as a Block category on Gutenberg.
*
* @since 2.0.0
*
* @param array $categories List of categories.
* @param \WP_Post $post Post being edited.
* @return array
*/
public function add_wp_ultimo_block_category($categories, $post) {
return array_merge($categories, array(
array(
'slug' => 'wp-ultimo',
'title' => __('WP Ultimo', 'wp-ultimo'),
),
));
} // end add_wp_ultimo_block_category;
} // end class Block_Manager;

View File

@ -0,0 +1,411 @@
<?php
/**
* Broadcast Manager
*
* Handles processes related to products.
*
* @package WP_Ultimo
* @subpackage Managers/Broadcast_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Models\Broadcast;
use WP_Ultimo\Helpers\Sender;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to products.
*
* @since 2.0.0
*/
class Broadcast_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'broadcast';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Broadcast';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
/**
* Add unseen broadcast notices to the panel.
*/
if (!is_network_admin() && !is_main_site()) {
add_action('init', array($this, 'add_unseen_broadcast_notices'));
} // end if;
} // end init;
/**
* Add unseen broadcast messages.
*
* @since 2.0.0
* @return void
*/
public function add_unseen_broadcast_notices() {
$current_customer = wu_get_current_customer();
if (!$current_customer) {
return;
} // end if;
$all_broadcasts = Broadcast::query(array(
'number' => 10,
'order' => 'DESC',
'order_by' => 'id',
'type__in' => array('broadcast_notice'),
));
if (isset($all_broadcasts)) {
foreach ($all_broadcasts as $key => $broadcast) {
if (isset($broadcast) && 'broadcast_notice' === $broadcast->get_type()) {
$targets = $this->get_all_notice_customer_targets($broadcast->get_id());
if (!is_array($targets)) {
$targets = array($targets);
} // end if;
$dismissed = get_user_meta(get_current_user_id(), 'wu_dismissed_admin_notices');
if (in_array($current_customer->get_id(), $targets, true) && !in_array($broadcast->get_id(), $dismissed, true)) {
$notice = '<span><strong>' . $broadcast->get_title() . '</strong> ' . $broadcast->get_content() . '</span>';
WP_Ultimo()->notices->add($notice, $broadcast->get_notice_type(), 'admin', strval($broadcast->get_id()));
WP_Ultimo()->notices->add($notice, $broadcast->get_notice_type(), 'user', strval($broadcast->get_id()));
} // end if;
} // end if;
} // end foreach;
} // end if;
} // end add_unseen_broadcast_notices;
/**
* Handles the broadcast message send via modal.
*
* @since 2.0.0
*
* @return void
*/
public function handle_broadcast() {
$args = $_POST;
$target_customers = wu_request('target_customers', '');
$target_products = wu_request('target_products', '');
if (!$target_customers && !$target_products) {
wp_send_json_error(new \WP_Error('error', __('No product or customer target was selected.', 'wp-ultimo')));
} // end if;
$broadcast_type = wu_request('type', 'broadcast_notice');
$args['type'] = $broadcast_type;
if ($broadcast_type === 'broadcast_notice') {
$targets = array(
'customers' => $target_customers,
'products' => $target_products
);
$args['targets'] = $targets;
// then we save with the message status (success, fail)
$saved = $this->save_broadcast($args);
if (is_wp_error($saved)) {
wp_send_json_error($saved);
} // end if;
$redirect = current_user_can('wu_edit_broadcasts') ? 'wp-ultimo-edit-broadcast' : 'wp-ultimo-broadcasts';
wp_send_json_success(array(
'redirect_url' => add_query_arg('id', $saved->get_id(), wu_network_admin_url($redirect))
));
} // end if;
if ($args['type'] === 'broadcast_email') {
$to = array();
$bcc = array();
$targets = array();
if ($args['target_customers']) {
$customers = explode(',', (string) $args['target_customers']);
$targets = array_merge($targets, $customers);
} // end if;
if ($args['target_products']) {
$product_targets = explode(',', (string) $args['target_products']);
$customers = array();
foreach ($product_targets as $product_id) {
$customers = array_merge($customers, wu_get_membership_customers($product_id));
} // end foreach;
$targets = array_merge($targets, $customers);
} // end if;
$targets = array_unique($targets);
/**
* Get name and email based on user id
*/
foreach ($targets as $target) {
$customer = wu_get_customer($target);
if ($customer) {
$to[] = array(
'name' => $customer->get_display_name(),
'email' => $customer->get_email_address(),
);
} // end if;
} // end foreach;
if (!isset($args['custom_sender'])) {
$from = array(
'name' => wu_get_setting('from_name', get_network_option(null, 'site_name')),
'email' => wu_get_setting('from_email', get_network_option(null, 'admin_email')),
);
} else {
$from = array(
'name' => $args['custom_sender']['from_name'],
'email' => $args['custom_sender']['from_email'],
);
} // end if;
$template_type = wu_get_setting('email_template_type', 'html');
$template_type = $template_type ? $template_type : 'html';
$send_args = array(
'site_name' => get_network_option(null, 'site_name'),
'site_url' => get_site_url(),
'type' => $template_type,
'subject' => $args['subject'],
'content' => $args['content'],
);
try {
$status = Sender::send_mail($from, $to, $send_args);
} catch (\Throwable $e) {
$error = new \WP_Error($e->getCode(), $e->getMessage());
wp_send_json_error($error);
} // end try;
if ($status) {
$args['targets'] = array(
'customers' => $args['target_customers'],
'products' => $args['target_products'],
);
// then we save with the message status (success, fail)
$this->save_broadcast($args);
wp_send_json_success(array(
'redirect_url' => wu_network_admin_url('wp-ultimo-broadcasts')
));
} // end if;
} // end if;
$error = new \WP_Error('mail-error', __('Something wrong happened.', 'wp-ultimo'));
wp_send_json_error($error);
} // end handle_broadcast;
/**
* Saves the broadcast message in the database
*
* @since 2.0.0
*
* @param array $args With the message arguments.
* @return Broadcast|\WP_Error
*/
public function save_broadcast($args) {
$broadcast_data = array(
'type' => $args['type'],
'name' => $args['subject'],
'content' => $args['content'],
'status' => 'publish',
);
$broadcast = new Broadcast($broadcast_data);
if ($args['type'] === 'broadcast_notice') {
$broadcast->set_notice_type($args['notice_type']);
} // end if;
$broadcast->set_message_targets($args['targets']);
$saved = $broadcast->save();
return is_wp_error($saved) ? $saved : $broadcast;
} // end save_broadcast;
/**
* Returns targets for a specific broadcast.
*
* @since 2.0.0
*
* @param string $object_id The broadcast object id.
* @param string $type The broadcast target type.
* @return string Return the broadcast targets for the specific type.
*/
public function get_broadcast_targets($object_id, $type) {
$broadcast = Broadcast::get_by_id($object_id);
$targets = $broadcast->get_message_targets();
if (isset($targets[$type])) {
if (is_string($targets[$type])) {
return explode(',', $targets[$type]);
} // end if;
return (array) $targets[$type];
} // end if;
return array();
} // end get_broadcast_targets;
/**
* Returns all customer from targets.
*
* @since 2.0.0
*
* @param string $object_id The broadcast object id.
* @return array Return the broadcast targets for the specific type.
*/
public function get_all_notice_customer_targets($object_id): array {
$customers_targets = $this->get_broadcast_targets($object_id, 'customers');
$products = $this->get_broadcast_targets($object_id, 'products');
$product_customers = array();
if (is_array($products) && $products[0]) {
foreach ($products as $product_key => $product) {
$membership_customers = wu_get_membership_customers($product);
if ($membership_customers) {
if (is_array($membership_customers)) {
$product_customers = array_merge($membership_customers, $product_customers);
} else {
array_push($product_customers, $membership_customers);
} // end if;
} // end if;
} // end foreach;
} // end if;
if (isset($product_customers) ) {
$targets = array_merge($product_customers, $customers_targets);
} else {
$targets = $customers_targets;
} // end if;
return array_map('absint', array_filter(array_unique($targets)));
} // end get_all_notice_customer_targets;
} // end class Broadcast_Manager;

View File

@ -0,0 +1,209 @@
<?php
/**
* Cache Manager Class
*
* Handles processes related to cache.
*
* @package WP_Ultimo
* @subpackage Managers/Cache_Manager
* @since 2.1.2
*/
namespace WP_Ultimo\Managers;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to cache.
*
* @since 2.1.2
*/
class Cache_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Flush known caching plugins, offers hooks to add more plugins in the future
*
* @since 2.1.2
* @return void
*/
public function flush_known_caches() {
/**
* Iterate through known caching plugins methods and flush them
* This is done by calling this class' methods ended in '_cache_flush'
*
* To support more caching plugins, just add a method to this class suffixed with '_cache_flush'
*/
foreach (get_class_methods($this) as $method) {
if (substr_compare($method, '_cache_flush', -strlen('_cache_flush')) === 0) {
$this->$method();
} // end if;
} // end foreach;
/**
* Hook to additional cleaning
*/
do_action('wu_flush_known_caches');
} // end flush_known_caches;
/**
* Flush WPEngine Cache
*
* @since 2.1.2
* @return void
*/
protected function wp_engine_cache_flush() {
if (class_exists('\WpeCommon') && method_exists('\WpeCommon', 'purge_varnish_cache')) {
\WpeCommon::purge_memcached(); // WPEngine Cache Flushing
\WpeCommon::clear_maxcdn_cache(); // WPEngine Cache Flushing
\WpeCommon::purge_varnish_cache(); // WPEngine Cache Flushing
} // end if;
} // end wp_engine_cache_flush;
/**
* Flush WP Rocket Cache
*
* @since 2.1.2
* @return void
*/
protected function wp_rocket_cache_flush() {
if (function_exists('rocket_clean_domain')) {
\rocket_clean_domain();
} // end if;
} // end wp_rocket_cache_flush;
/**
* Flush WP Super Cache
*
* @since 2.1.2
* @return void
*/
protected function wp_super_cache_flush() {
if (function_exists('wp_cache_clear_cache')) {
\wp_cache_clear_cache(); // WP Super Cache Flush
} // end if;
} // end wp_super_cache_flush;
/**
* Flush WP Fastest Cache
*
* @since 2.1.2
* @return void
*/
protected function wp_fastest_cache_flush() {
if (function_exists('wpfc_clear_all_cache')) {
\wpfc_clear_all_cache(); // WP Fastest Cache Flushing
} // end if;
} // end wp_fastest_cache_flush;
/**
* Flush W3 Total Cache
*
* @since 2.1.2
* @return void
*/
protected function w3_total_cache_flush() {
if (function_exists('w3tc_pgcache_flush')) {
\w3tc_pgcache_flush(); // W3TC Cache Flushing
} // end if;
} // end w3_total_cache_flush;
/**
* Flush Hummingbird Cache
*
* @since 2.1.2
* @return void
*/
protected function hummingbird_cache_flush() {
if (class_exists('\Hummingbird\WP_Hummingbird') && method_exists('\Hummingbird\WP_Hummingbird', 'flush_cache')) {
\Hummingbird\WP_Hummingbird::flush_cache(); // Hummingbird Cache Flushing
} // end if;
} // end hummingbird_cache_flush;
/**
* Flush WP Optimize Cache
*
* @since 2.1.2
* @return void
*/
protected function wp_optimize_cache_flush() {
if (class_exists('\WP_Optimize') && method_exists('\WP_Optimize', 'get_page_cache')) {
$wp_optimize = \WP_Optimize()->get_page_cache();
if (method_exists($wp_optimize, 'purge')) {
$wp_optimize->purge(); // WP Optimize Cache Flushing
} // end if;
} // end if;
} // end wp_optimize_cache_flush;
/**
* Flush Comet Cache
*
* @since 2.1.2
* @return void
*/
protected function comet_cache_flush() {
if (class_exists('\Comet_Cache') && method_exists('\Comet_Cache', 'clear')) {
\Comet_Cache::clear(); // Comet Cache Flushing
} // end if;
} // end comet_cache_flush;
/**
* Flush LiteSpeed Cache
*
* @since 2.1.2
* @return void
*/
protected function litespeed_cache_flush() {
if (class_exists('\LiteSpeed_Cache_API') && method_exists('\LiteSpeed_Cache_API', 'purge_all')) {
\LiteSpeed_Cache_API::purge_all(); // LiteSpeed Cache Flushing
} // end if;
} // end litespeed_cache_flush;
} // end class Cache_Manager;

View File

@ -0,0 +1,58 @@
<?php
/**
* Checkout Form Manager
*
* Handles processes related to Checkout Forms.
*
* @package WP_Ultimo
* @subpackage Managers/Checkout_Form_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to checkout forms.
*
* @since 2.0.0
*/
class Checkout_Form_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'checkout_form';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Checkout_Form';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
} // end init;
} // end class Checkout_Form_Manager;

View File

@ -0,0 +1,363 @@
<?php
/**
* Customer Manager
*
* Handles processes related to Customers.
*
* @package WP_Ultimo
* @subpackage Managers/Customer_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Models\Customer;
use \WP_Ultimo\Database\Memberships\Membership_Status;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to webhooks.
*
* @since 2.0.0
*/
class Customer_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'customer';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Customer';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
Event_Manager::register_model_events('customer', __('Customer', 'wp-ultimo'), array('created', 'updated'));
add_action('wp_login', array($this, 'log_ip_and_last_login'), 10, 2);
add_filter('heartbeat_send', array($this, 'on_heartbeat_send'));
add_action('wu_transition_customer_email_verification', array($this, 'transition_customer_email_verification'), 10, 3);
add_action('init', array($this, 'maybe_verify_email_address'));
add_action('wu_maybe_create_customer', array($this, 'maybe_add_to_main_site'), 10, 2);
add_action('wp_ajax_wu_resend_verification_email', array($this, 'handle_resend_verification_email'));
} // end init;
/**
* Handle the resend verification email ajax action.
*
* @since 2.0.4
* @return void
*/
public function handle_resend_verification_email() {
if (!check_ajax_referer('wu_resend_verification_email_nonce', false, false)) {
wp_send_json_error(new \WP_Error('not-allowed', __('Error: you are not allowed to perform this action.', 'wp-ultimo')));
exit;
} // end if;
$customer = wu_get_current_customer();
if (!$customer) {
wp_send_json_error(new \WP_Error('customer-not-found', __('Error: customer not found.', 'wp-ultimo')));
exit;
} // end if;
$customer->send_verification_email();
wp_send_json_success();
exit;
} // end handle_resend_verification_email;
/**
* Handle heartbeat response sent.
*
* @since 2.0.0
*
* @param array $response The Heartbeat response.
* @return array $response The Heartbeat response
*/
public function on_heartbeat_send($response) {
$this->log_ip_and_last_login(wp_get_current_user());
return $response;
} // end on_heartbeat_send;
/**
* Saves the IP address and last_login date onto the user.
*
* @since 2.0.0
*
* @param WP_User $user The WP User object of the user that logged in.
* @return void
*/
public function log_ip_and_last_login($user) {
if (!is_a($user, '\WP_User')) {
$user = get_user_by('login', $user);
} // end if;
if (!$user) {
return;
} // end if;
$customer = wu_get_customer_by_user_id($user->ID);
if (!$customer) {
return;
} // end if;
$customer->update_last_login();
} // end log_ip_and_last_login;
/**
* Watches the change in customer verification status to take action when needed.
*
* @since 2.0.0
*
* @param string $old_status The old status of the customer verification.
* @param string $new_status The new status of the customer verification.
* @param integer $customer_id Customer ID.
* @return void
*/
public function transition_customer_email_verification($old_status, $new_status, $customer_id) {
if ($new_status !== 'pending') {
return;
} // end if;
$customer = wu_get_customer($customer_id);
if ($customer) {
$customer->send_verification_email();
} // end if;
} // end transition_customer_email_verification;
/**
* Verifies a customer by checking the email key.
*
* If only one membership is available and that's pending,
* we set it to active. That will trigger the publication of
* pending websites as well.
*
* @since 2.0.0
* @return void
*/
public function maybe_verify_email_address() {
$email_verify_key = wu_request('email-verification-key');
if (!$email_verify_key) {
return;
} // end if;
$customer_hash = wu_request('customer');
$customer_to_verify = wu_get_customer_by_hash($customer_hash);
if (!is_user_logged_in()) {
wp_die(
sprintf(
/* translators: the placeholder is the login URL */
__('You must be authenticated in order to verify your email address. <a href=%s>Click here</a> to access your account.', 'wp-ultimo'),
wp_login_url(add_query_arg(
array(
'email-verification-key' => $email_verify_key,
'customer' => $customer_hash,
)
))
)
);
} // end if;
if (!$customer_to_verify) {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
$current_customer = wu_get_current_customer();
if (!$current_customer) {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
if ($current_customer->get_id() !== $customer_to_verify->get_id()) {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
if ($customer_to_verify->get_email_verification() !== 'pending') {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
$key = $customer_to_verify->get_verification_key();
if (!$key) {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
if ($key !== $email_verify_key) {
wp_die(__('Invalid verification key.', 'wp-ultimo'));
} // end if;
/*
* Uff! If we got here, we can verify the customer.
*/
$customer_to_verify->disable_verification_key();
$customer_to_verify->set_email_verification('verified');
$customer_to_verify->save();
/*
* Checks for memberships and pending sites.
*/
$memberships = $customer_to_verify->get_memberships();
/*
* We can only take action if the customer has
* only one membership, otherwise we can't be sure about
* which one to manage.
*/
if (count($memberships) === 1) {
$membership = current($memberships);
/*
* Only publish pending memberships
*/
if ($membership->get_status() === Membership_Status::PENDING) {
$membership->publish_pending_site_async();
if ($membership->get_date_trial_end() <= gmdate('Y-m-d 23:59:59')) {
$membership->set_status(Membership_Status::ACTIVE);
} // end if;
$membership->save();
} elseif ($membership->get_status() === Membership_Status::TRIALING) {
$membership->publish_pending_site_async();
} // end if;
$payments = $membership->get_payments();
if ($payments) {
$redirect_url = add_query_arg(array(
'payment' => $payments[0]->get_hash(),
'status' => 'done',
), wu_get_registration_url());
wp_redirect($redirect_url);
exit;
} // end if;
} // end if;
wp_redirect(get_admin_url($customer_to_verify->get_primary_site_id()));
exit;
} // end maybe_verify_email_address;
/**
* Maybe adds the customer to the main site.
*
* @since 2.0.0
*
* @param Customer $customer The customer object.
* @param Checkout $checkout The checkout object.
* @return void
*/
public function maybe_add_to_main_site($customer, $checkout) {
if (!wu_get_setting('add_users_to_main_site')) {
return;
} // end if;
$user_id = $customer->get_user_id();
$is_already_user = is_user_member_of_blog($user_id, wu_get_main_site_id());
if ($is_already_user === false) {
$role = wu_get_setting('main_site_default_role', 'subscriber');
add_user_to_blog(wu_get_main_site_id(), $user_id, $role);
} // end if;
} // end maybe_add_to_main_site;
} // end class Customer_Manager;

View File

@ -0,0 +1,114 @@
<?php
/**
* Discount_Codes Manager
*
* Handles processes related to events.
*
* @package WP_Ultimo
* @subpackage Managers/Discount_Code_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Logger;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to events.
*
* @since 2.0.0
*/
class Discount_Code_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'discount_code';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Discount_Code';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
add_action('wu_gateway_payment_processed', array($this, 'maybe_add_use_on_payment_received'));
} // end init;
/**
* Listens for payments received in order to increase the discount code uses.
*
* @since 2.0.4
*
* @param \WP_Ultimo\Models\Payment $payment The payment received.
* @return void
*/
public function maybe_add_use_on_payment_received($payment) {
if (!$payment) {
return;
} // end if;
/*
* Try to fetch the original cart of the payment.
* We want only to increase the number of uses
* for the first time payments are done.
*/
$original_cart = $payment->get_meta('wu_original_cart');
if (is_a($original_cart, \WP_Ultimo\Checkout\Cart::class) === false) {
return;
} // end if;
$discount_code = $original_cart->get_discount_code();
if (!$discount_code) {
return;
} // end if;
/*
* Refetch the object, as the original version
* might be too old and out-of-date by now.
*/
$discount_code = wu_get_discount_code($discount_code->get_id());
if ($discount_code) {
$discount_code->add_use();
$discount_code->save();
} // end if;
} // end maybe_add_use_on_payment_received;
} // end class Discount_Code_Manager;

View File

@ -0,0 +1,828 @@
<?php
/**
* Domain Mapping Manager
*
* Handles processes related to domain mappings,
* things like adding hooks to add asynchronous checking of DNS settings and SSL certs and more.
*
* @package WP_Ultimo
* @subpackage Managers/Domain_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Domain_Mapping\Helper;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to domain mappings.
*
* @since 2.0.0
*/
class Domain_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'domain';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Domain';
/**
* Holds a list of the current integrations for domain mapping.
*
* @since 2.0.0
* @var array
*/
protected $integrations = array();
/**
* Returns the list of available host integrations.
*
* This needs to be a filterable method to allow integrations to self-register.
*
* @since 2.0.0
* @return array
*/
public function get_integrations() {
return apply_filters('wu_domain_manager_get_integrations', $this->integrations, $this);
} // end get_integrations;
/**
* Get the instance of one of the integrations classes.
*
* @since 2.0.0
*
* @param string $id The id of the integration. e.g. runcloud.
* @return WP_Ultimo\Integrations\Host_Providers\Base_Host_Provider
*/
public function get_integration_instance($id) {
$integrations = $this->get_integrations();
if (isset($integrations[$id])) {
$class_name = $integrations[$id];
return $class_name::get_instance();
} // end if;
return false;
} // end get_integration_instance;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
$this->set_cookie_domain();
add_action('plugins_loaded', array($this, 'load_integrations'));
add_action('wp_ajax_wu_test_hosting_integration', array($this, 'test_integration'));
add_action('wp_ajax_wu_get_dns_records', array($this, 'get_dns_records'));
add_action('wu_async_remove_old_primary_domains', array($this, 'async_remove_old_primary_domains'));
add_action('wu_async_process_domain_stage', array($this, 'async_process_domain_stage'), 10, 2);
add_action('wu_transition_domain_domain', array($this, 'send_domain_to_host'), 10, 3);
add_action('wu_settings_domain_mapping', array($this, 'add_domain_mapping_settings'));
add_action('wu_settings_sso', array($this, 'add_sso_settings'));
/*
* Add and remove mapped domains
*/
add_action('wu_domain_created', array($this, 'handle_domain_created'), 10, 3);
add_action('wu_domain_post_delete', array($this, 'handle_domain_deleted'), 10, 3);
/*
* Add and remove sub-domains
*/
add_action('wp_insert_site', array($this, 'handle_site_created'));
add_action('wp_delete_site', array($this, 'handle_site_deleted'));
} // end init;
/**
* Set COOKIE_DOMAIN if not defined in sites with mapped domains.
*
* @since 2.0.12
*
* @return void
*/
protected function set_cookie_domain() {
if (defined('DOMAIN_CURRENT_SITE') && !defined('COOKIE_DOMAIN') && !preg_match('/' . DOMAIN_CURRENT_SITE . '$/', '.' . $_SERVER['HTTP_HOST'])) {
define( 'COOKIE_DOMAIN', '.' . $_SERVER['HTTP_HOST'] );
} // end if;
} // end set_cookie_domain;
/**
* Triggers subdomain mapping events on site creation.
*
* @since 2.0.0
*
* @param \WP_Site $site The site being added.
* @return void
*/
public function handle_site_created($site) {
global $current_site;
$has_subdomain = str_replace($current_site->domain, '', $site->domain);
if (!$has_subdomain) {
return;
} // end if;
$args = array(
'subdomain' => $site->domain,
'site_id' => $site->blog_id,
);
wu_enqueue_async_action('wu_add_subdomain', $args, 'domain');
} // end handle_site_created;
/**
* Triggers subdomain mapping events on site deletion.
*
* @since 2.0.0
*
* @param \WP_Site $site The site being removed.
* @return void
*/
public function handle_site_deleted($site) {
global $current_site;
$has_subdomain = str_replace($current_site->domain, '', $site->domain);
if (!$has_subdomain) {
return;
} // end if;
$args = array(
'subdomain' => $site->domain,
'site_id' => $site->blog_id,
);
wu_enqueue_async_action('wu_remove_subdomain', $args, 'domain');
} // end handle_site_deleted;
/**
* Triggers the do_event of the payment successful.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Domain $domain The domain.
* @param \WP_Ultimo\Models\Site $site The site.
* @param \WP_Ultimo\Models\Membership $membership The membership.
* @return void
*/
public function handle_domain_created($domain, $site, $membership) {
$payload = array_merge(
wu_generate_event_payload('domain', $domain),
wu_generate_event_payload('site', $site),
wu_generate_event_payload('membership', $membership),
wu_generate_event_payload('customer', $membership->get_customer())
);
wu_do_event('domain_created', $payload);
} // end handle_domain_created;
/**
* Remove send domain removal event.
*
* @since 2.0.0
*
* @param boolean $result The result of the deletion.
* @param \WP_Ultimo\Models\Domain $domain The domain being deleted.
* @return void
*/
public function handle_domain_deleted($result, $domain) {
if ($result) {
$args = array(
'domain' => $domain->get_domain(),
'site_id' => $domain->get_site_id(),
);
wu_enqueue_async_action('wu_remove_domain', $args, 'domain');
} // end if;
} // end handle_domain_deleted;
/**
* Add all domain mapping settings.
*
* @since 2.0.0
* @return void
*/
public function add_domain_mapping_settings() {
wu_register_settings_field('domain-mapping', 'domain_mapping_header', array(
'title' => __('Domain Mapping Settings', 'wp-ultimo'),
'desc' => __('Define the domain mapping settings for your network.', 'wp-ultimo'),
'type' => 'header',
));
wu_register_settings_field('domain-mapping', 'enable_domain_mapping', array(
'title' => __('Enable Domain Mapping?', 'wp-ultimo'),
'desc' => __('Do you want to enable domain mapping?', 'wp-ultimo'),
'type' => 'toggle',
'default' => 1,
));
wu_register_settings_field('domain-mapping', 'force_admin_redirect', array(
'title' => __('Force Admin Redirect', 'wp-ultimo'),
'desc' => __('Select how you want your users to access the admin panel if they have mapped domains.', 'wp-ultimo') . '<br><br>' . __('Force Redirect to Mapped Domain: your users with mapped domains will be redirected to theirdomain.com/wp-admin, even if they access using yournetworkdomain.com/wp-admin.', 'wp-ultimo') . '<br><br>' . __('Force Redirect to Network Domain: your users with mapped domains will be redirect to yournetworkdomain.com/wp-admin, even if they access using theirdomain.com/wp-admin.', 'wp-ultimo'),
'tooltip' => '',
'type' => 'select',
'default' => 'both',
'require' => array('enable_domain_mapping' => 1),
'options' => array(
'both' => __('Allow access to the admin by both mapped domain and network domain', 'wp-ultimo'),
'force_map' => __('Force Redirect to Mapped Domain', 'wp-ultimo'),
'force_network' => __('Force Redirect to Network Domain', 'wp-ultimo'),
),
));
wu_register_settings_field('domain-mapping', 'custom_domains', array(
'title' => __('Enable Custom Domains?', 'wp-ultimo'),
'desc' => __('Toggle this option if you wish to allow end-customers to add their own domains. This can be controlled on a plan per plan basis.', 'wp-ultimo'),
'type' => 'toggle',
'default' => 1,
'require' => array(
'enable_domain_mapping' => true,
),
));
wu_register_settings_field('domain-mapping', 'domain_mapping_instructions', array(
'title' => __('Add New Domain Instructions', 'wp-ultimo'),
'tooltip' => __('Display a customized message with instructions for the mapping and alerting the end-user of the risks of mapping a misconfigured domain.', 'wp-ultimo'),
'desc' => __('You can use the placeholder <code>%NETWORK_DOMAIN%</code> and <code>%NETWORK_IP%</code>.', 'wp-ultimo'),
'type' => 'textarea',
'default' => array($this, 'default_domain_mapping_instructions'),
'html_attr' => array(
'rows' => 8,
),
'require' => array(
'enable_domain_mapping' => true,
'custom_domains' => true,
),
));
} // end add_domain_mapping_settings;
/**
* Add all SSO settings.
*
* @since 2.0.0
* @return void
*/
public function add_sso_settings() {
wu_register_settings_field('sso', 'sso_header', array(
'title' => __('Single Sign-On Settings', 'wp-ultimo'),
'desc' => __('Settings to configure the Single Sign-On functionality of WP Ultimo, responsible for keeping customers and admins logged in across all network domains.', 'wp-ultimo'),
'type' => 'header',
));
wu_register_settings_field('sso', 'enable_sso', array(
'title' => __('Enable Single Sign-On', 'wp-ultimo'),
'desc' => __('Enables the Single Sign-on functionality.', 'wp-ultimo'),
'type' => 'toggle',
'default' => 1,
));
wu_register_settings_field('sso', 'restrict_sso_to_login_pages', array(
'title' => __('Restrict SSO Checks to Login Pages', 'wp-ultimo'),
'desc' => __('The Single Sign-on feature adds one extra ajax calls to every page load on sites with custom domains active to check if it should perform an auth loopback. You can restrict these extra calls to the login pages of sub-sites using this option. If enabled, SSO will only work on login pages.', 'wp-ultimo'),
'type' => 'toggle',
'default' => 0,
'require' => array(
'enable_sso' => true,
),
));
wu_register_settings_field('sso', 'enable_sso_loading_overlay', array(
'title' => __('Enable SSO Loading Overlay', 'wp-ultimo'),
'desc' => __('When active, a loading overlay will be added on-top of the site currently being viewed while the SSO auth loopback is performed on the background.', 'wp-ultimo'),
'type' => 'toggle',
'default' => 1,
'require' => array(
'enable_sso' => true,
),
));
} // end add_sso_settings;
/**
* Returns the default instructions for domain mapping.
*
* @since 2.0.0
*/
public function default_domain_mapping_instructions(): string {
$instructions = array();
$instructions[] = __("Cool! You're about to make this site accessible using your own domain name!", 'wp-ultimo');
$instructions[] = __("For that to work, you'll need to create a new CNAME record pointing to <code>%NETWORK_DOMAIN%</code> on your DNS manager.", 'wp-ultimo');
$instructions[] = __('After you finish that step, come back to this screen and click the button below.', 'wp-ultimo');
return implode(PHP_EOL . PHP_EOL, $instructions);
} // end default_domain_mapping_instructions;
/**
* Gets the instructions, filtered and without the shortcodes.
*
* @since 2.0.0
* @return string
*/
public function get_domain_mapping_instructions() {
global $current_site;
$instructions = wu_get_setting('domain_mapping_instructions');
if (!$instructions) {
$instructions = $this->default_domain_mapping_instructions();
} // end if;
$domain = $current_site->domain;
$ip = Helper::get_network_public_ip();
/*
* Replace placeholders
*/
$instructions = str_replace('%NETWORK_DOMAIN%', $domain, (string) $instructions);
$instructions = str_replace('%NETWORK_IP%', $ip, $instructions);
return apply_filters('wu_get_domain_mapping_instructions', $instructions, $domain, $ip);
} // end get_domain_mapping_instructions;
/**
* Creates the event to save the transition.
*
* @since 2.0.0
*
* @param mixed $old_value The old value, before the transition.
* @param mixed $new_value The new value, after the transition.
* @param int $item_id The id of the element transitioning.
* @return void
*/
public function send_domain_to_host($old_value, $new_value, $item_id) {
if ($old_value !== $new_value) {
$domain = wu_get_domain($item_id);
$args = array(
'domain' => $new_value,
'site_id' => $domain->get_site_id(),
);
wu_enqueue_async_action('wu_add_domain', $args, 'domain');
} // end if;
} // end send_domain_to_host;
/**
* Checks the DNS and SSL status of a domain.
*
* @since 2.0.0
*
* @param int $domain_id The domain mapping ID.
* @param int $tries Number of tries.
* @return void
*/
public function async_process_domain_stage($domain_id, $tries = 0) {
$domain = wu_get_domain($domain_id);
if (!$domain) {
return;
} // end if;
$max_tries = apply_filters('wu_async_process_domain_stage_max_tries', 5, $domain);
$try_again_time = apply_filters('wu_async_process_domains_try_again_time', 5, $domain); // minutes
$tries++;
$stage = $domain->get_stage();
$domain_url = $domain->get_domain();
// translators: %s is the domain name
wu_log_add("domain-{$domain_url}", sprintf(__('Starting Check for %s', 'wp-ultimo'), $domain_url));
if ($stage === 'checking-dns') {
if ($domain->has_correct_dns()) {
$domain->set_stage('checking-ssl-cert');
$domain->save();
wu_log_add(
"domain-{$domain_url}",
__('- DNS propagation finished, advancing domain to next step...', 'wp-ultimo')
);
wu_enqueue_async_action('wu_async_process_domain_stage', array('domain_id' => $domain_id, 'tries' => 0), 'domain');
do_action('wu_domain_manager_dns_propagation_finished', $domain);
return;
} else {
/*
* Max attempts
*/
if ($tries > $max_tries) {
$domain->set_stage('failed');
$domain->save();
wu_log_add(
"domain-{$domain_url}",
// translators: %d is the number of minutes to try again.
sprintf(__('- DNS propagation checks tried for the max amount of times (5 times, one every %d minutes). Marking as failed.', 'wp-ultimo'), $try_again_time)
);
return;
} // end if;
wu_log_add(
"domain-{$domain_url}",
// translators: %d is the number of minutes before trying again.
sprintf(__('- DNS propagation not finished, retrying in %d minutes...', 'wp-ultimo'), $try_again_time)
);
wu_schedule_single_action(
strtotime("+{$try_again_time} minutes"),
'wu_async_process_domain_stage',
array(
'domain_id' => $domain_id,
'tries' => $tries,
),
'domain'
);
return;
} // end if;
} elseif ($stage === 'checking-ssl-cert') {
if ($domain->has_valid_ssl_certificate()) {
$domain->set_stage('done');
$domain->set_secure(true);
$domain->save();
wu_log_add(
"domain-{$domain_url}",
__('- Valid SSL cert found. Marking domain as done.', 'wp-ultimo')
);
return;
} else {
/*
* Max attempts
*/
if ($tries > $max_tries) {
$domain->set_stage('done-without-ssl');
$domain->save();
wu_log_add(
"domain-{$domain_url}",
// translators: %d is the number of minutes to try again.
sprintf(__('- SSL checks tried for the max amount of times (5 times, one every %d minutes). Marking as ready without SSL.', 'wp-ultimo'), $try_again_time)
);
return;
} // end if;
wu_log_add(
"domain-{$domain_url}",
// translators: %d is the number of minutes before trying again.
sprintf(__('- SSL Cert not found, retrying in %d minute(s)...', 'wp-ultimo'), $try_again_time)
);
wu_schedule_single_action(strtotime("+{$try_again_time} minutes"), 'wu_async_process_domain_stage', array('domain_id' => $domain_id, 'tries' => $tries), 'domain');
return;
} // end if;
} // end if;
} // end async_process_domain_stage;
/**
* Alternative implementation for PHP's native dns_get_record.
*
* @since 2.0.0
* @param string $domain The domain to check.
* @return array
*/
public static function dns_get_record($domain) {
$results = array();
wu_setup_memory_limit_trap('json');
wu_try_unlimited_server_limits();
$record_types = array(
'NS',
'CNAME',
'A',
);
foreach ($record_types as $record_type) {
$chain = new \WP_Ultimo\Dependencies\RemotelyLiving\PHPDNS\Resolvers\Chain(
new \WP_Ultimo\Dependencies\RemotelyLiving\PHPDNS\Resolvers\CloudFlare(),
new \WP_Ultimo\Dependencies\RemotelyLiving\PHPDNS\Resolvers\GoogleDNS(),
new \WP_Ultimo\Dependencies\RemotelyLiving\PHPDNS\Resolvers\LocalSystem(),
new \WP_Ultimo\Dependencies\RemotelyLiving\PHPDNS\Resolvers\Dig(),
);
$records = $chain->getRecords($domain, $record_type);
foreach ($records as $record_data) {
$record = array();
$record['type'] = $record_type;
$record['data'] = (string) $record_data->getData();
if (empty($record['data'])) {
$record['data'] = (string) $record_data->getIPAddress();
} // end if;
// Some DNS providers return a trailing dot.
$record['data'] = rtrim($record['data'], '.');
$record['ip'] = (string) $record_data->getIPAddress();
$record['ttl'] = $record_data->getTTL();
$record['host'] = $domain;
$record['tag'] = ''; // Used by integrations.
$results[] = $record;
} // end foreach;
} // end foreach;
return apply_filters('wu_domain_dns_get_record', $results, $domain);
} // end dns_get_record;
/**
* Get the DNS records for a given domain.
*
* @since 2.0.0
* @return void
*/
public function get_dns_records() {
$domain = wu_request('domain');
if (!$domain) {
wp_send_json_error(new \WP_Error('domain-missing', __('A valid domain was not passed.', 'wp-ultimo')));
} // end if;
$auth_ns = array();
$additional = array();
try {
$result = self::dns_get_record($domain);
} catch (\Throwable $e) {
wp_send_json_error(new \WP_Error('error', __('Not able to fetch DNS entries.', 'wp-ultimo'), array(
'exception' => $e->getMessage(),
)));
} // end try;
if ($result === false) {
wp_send_json_error(new \WP_Error('error', __('Not able to fetch DNS entries.', 'wp-ultimo')));
} // end if;
wp_send_json_success(array(
'entries' => $result,
'auth' => $auth_ns,
'additional' => $additional,
'network_ip' => Helper::get_network_public_ip(),
));
} // end get_dns_records;
/**
* Takes the list of domains and set them to non-primary when a new primary is added.
*
* This is triggered when a new domain is added as primary_domain.
*
* @since 2.0.0
*
* @param array $domains List of domain ids.
* @return void
*/
public function async_remove_old_primary_domains($domains) {
foreach ($domains as $domain_id) {
$domain = wu_get_domain($domain_id);
if ($domain) {
$domain->set_primary_domain(false);
$domain->save();
} // end if;
} // end foreach;
} // end async_remove_old_primary_domains;
/**
* Tests the integration in the Wizard context.
*
* @since 2.0.0
* @return mixed
*/
public function test_integration() {
$integration_id = wu_request('integration', 'none');
$integration = $this->get_integration_instance($integration_id);
if (!$integration) {
wp_send_json_error(array(
'message' => __('Invalid Integration ID', 'wp-ultimo'),
));
} // end if;
/*
* Checks for the constants...
*/
if (!$integration->is_setup()) {
wp_send_json_error(array(
'message' => sprintf(
__('The necessary constants were not found on your wp-config.php file: %s', 'wp-ultimo'),
implode(', ', $integration->get_missing_constants())
),
));
} // end if;
return $integration->test_connection();
} // end test_integration;
/**
* Loads all the host provider integrations we have available.
*
* @since 2.0.0
* @return void
*/
public function load_integrations() {
/*
* Loads our RunCloud integration.
*/
\WP_Ultimo\Integrations\Host_Providers\Runcloud_Host_Provider::get_instance();
/*
* Loads our Closte integration.
*/
\WP_Ultimo\Integrations\Host_Providers\Closte_Host_Provider::get_instance();
/*
* Loads our WP Engine integration.
*/
\WP_Ultimo\Integrations\Host_Providers\WPEngine_Host_Provider::get_instance();
/*
* Loads our Gridpane integration.
*/
\WP_Ultimo\Integrations\Host_Providers\Gridpane_Host_Provider::get_instance();
/*
* Loads our WPMU DEV integration.
*/
\WP_Ultimo\Integrations\Host_Providers\WPMUDEV_Host_Provider::get_instance();
/*
* Loads our Cloudways integration.
*/
\WP_Ultimo\Integrations\Host_Providers\Cloudways_Host_Provider::get_instance();
/*
* Loads our ServerPilot integration.
*/
\WP_Ultimo\Integrations\Host_Providers\ServerPilot_Host_Provider::get_instance();
/*
* Loads our cPanel integration.
*/
\WP_Ultimo\Integrations\Host_Providers\CPanel_Host_Provider::get_instance();
/*
* Loads our Cloudflare integration.
*/
\WP_Ultimo\Integrations\Host_Providers\Cloudflare_Host_Provider::get_instance();
/**
* Allow developers to add their own host provider integrations via wp plugins.
*
* @since 2.0.0
*/
do_action('wp_ultimo_host_providers_load');
} // end load_integrations;
} // end class Domain_Manager;

View File

@ -0,0 +1,567 @@
<?php
/**
* Email Manager
*
* Handles processes related to Emails.
*
* @package WP_Ultimo
* @subpackage Managers/Email_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Dependencies\Psr\Log\LogLevel;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Models\Email;
use \WP_Ultimo\Helpers\Sender;
use \WP_Ultimo\Models\Base_Model;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to emails.
*
* @since 2.0.0
*/
class Email_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'email';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Email';
/**
* All default system emails and their original content.
*
* @since 2.0.0
* @var array
*/
protected $registered_default_system_emails;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
$this->register_all_default_system_emails();
/*
* Adds the Email fields
*/
add_action('wu_settings_emails', array($this, 'add_email_fields'));
add_action('wu_event', array($this, 'send_system_email'), 10, 2);
/*
* Registering a callback action to a schedule send.
*/
add_action('wu_send_schedule_system_email', array($this, 'send_schedule_system_email'), 10, 5);
/*
* Create a log when a mail send fails.
*/
add_action('wp_mail_failed', array($this, 'log_mailer_failure'));
add_action('wp_ajax_wu_get_event_payload_placeholders', array($this, 'get_event_placeholders'));
} // end init;
/**
* Send the email related to the current event.
*
* @param string $slug Slug of the event.
* @param array $payload Payload of the event.
* @return void.
*/
public function send_system_email($slug, $payload) {
$all_emails = wu_get_emails(array(
'event' => $slug,
));
$original_from = array(
'name' => wu_get_setting('from_name'),
'email' => wu_get_setting('from_email'),
);
/*
* Loop through all the emails registered.
*/
foreach ($all_emails as $email) {
if ($email->get_custom_sender()) {
$from = array(
'name' => $email->get_custom_sender_name(),
'email' => $email->get_custom_sender_email(),
);
} else {
$from = $original_from;
} // end if;
/*
* Compiles the target list.
*/
$to = $email->get_target_list($payload);
if (empty($to)) {
wu_log_add('mailer', __('No targets found.', 'wp-ultimo'));
return;
} // end if;
$args = array(
'style' => $email->get_style(),
'content' => $email->get_content(),
'subject' => get_network_option(null, 'site_name') . ' - ' . $email->get_title(),
'payload' => $payload,
);
/*
* Add the invoice attachment, if need be.
*/
if (wu_get_isset($payload, 'payment_invoice_url') && wu_get_setting('attach_invoice_pdf', true)) {
$file_name = 'invoice-' . $payload['payment_reference_code'] . '.pdf';
$this->attach_file_by_url($payload['payment_invoice_url'], $file_name, $args['subject']);
} // end if;
$when_to_send = $email->get_when_to_send();
if ($when_to_send) {
$args['schedule'] = $when_to_send;
} // end if;
Sender::send_mail($from, $to, $args);
} // end foreach;
} // end send_system_email;
/**
* Attach a file by a URL
*
* @since 2.0.0
*
* @param string $file_url The URL of the file to attach.
* @param string $file_name The name to save the file with.
* @param string $email_subject The email subject, to avoid attaching a file to the wrong email.
* @return void
*/
public function attach_file_by_url($file_url, $file_name, $email_subject = '') {
add_action('phpmailer_init', function($mail) use ($file_url, $file_name, $email_subject) {
if ($email_subject && $mail->Subject !== $email_subject) { // phpcs:ignore
return;
} // end if;
$response = wp_remote_get($file_url, array(
'timeout' => 50,
));
if (is_wp_error($response)) {
return;
} // end if;
$file = wp_remote_retrieve_body($response);
/*
* Use the default PHPMailer APIs to attach the file.
*/
$mail->addStringAttachment($file, $file_name);
});
} // end attach_file_by_url;
/**
* Add all email fields.
*
* @since 2.0.0
* @return void
*/
public function add_email_fields() {
wu_register_settings_field('emails', 'sender_header', array(
'title' => __('Sender Settings', 'wp-ultimo'),
'desc' => __('Change the settings of the email headers, like from and name.', 'wp-ultimo'),
'type' => 'header',
));
wu_register_settings_field('emails', 'from_name', array(
'title' => __('"From" Name', 'wp-ultimo'),
'desc' => __('How the sender name will appear in emails sent by WP Ultimo.', 'wp-ultimo'),
'type' => 'text',
'placeholder' => get_network_option(null, 'site_name'),
'default' => get_network_option(null, 'site_name'),
'html_attr' => array(
'v-model' => 'from_name',
),
));
wu_register_settings_field('emails', 'from_email', array(
'title' => __('"From" E-mail', 'wp-ultimo'),
'desc' => __('How the sender email will appear in emails sent by WP Ultimo.', 'wp-ultimo'),
'type' => 'email',
'placeholder' => get_network_option(null, 'admin_email'),
'default' => get_network_option(null, 'admin_email'),
'html_attr' => array(
'v-model' => 'from_email',
),
));
wu_register_settings_field('emails', 'template_header', array(
'title' => __('Template Settings', 'wp-ultimo'),
'desc' => __('Change the settings of the email templates.', 'wp-ultimo'),
'type' => 'header',
));
wu_register_settings_field('emails', 'email_template_type', array(
'title' => __('Email Templates Style', 'wp-ultimo'),
'desc' => __('Choose if email body will be sent using the HTML template or in plain text.', 'wp-ultimo'),
'type' => 'select',
'default' => 'html',
'options' => array(
'html' => __('HTML Emails', 'wp-ultimo'),
'plain' => __('Plain Emails', 'wp-ultimo'),
),
'html_attr' => array(
'v-model' => 'emails_template',
),
));
wu_register_settings_field('emails', 'expiring_header', array(
'title' => __('Expiring Notification Settings', 'wp-ultimo'),
'desc' => __('Change the settings for the expiring notification (trials and subscriptions) emails.', 'wp-ultimo'),
'type' => 'header',
));
wu_register_settings_field('emails', 'expiring_days', array(
'title' => __('Days to Expire', 'wp-ultimo'),
'desc' => __('Select when we should send the notification email. If you select 3 days, for example, a notification email will be sent to every membership (or trial period) expiring in the next 3 days. Memberships are checked hourly.', 'wp-ultimo'),
'type' => 'number',
'placeholder' => __('e.g. 3', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'expiring_days',
),
));
} // end add_email_fields;
/**
* Register in the global variable all the default system emails.
*
* @since 2.0.0
*
* @param array $args System email params.
* @return void.
*/
public function register_default_system_email($args) {
$this->registered_default_system_emails[$args['slug']] = $args;
} // end register_default_system_email;
/**
* Create a system email.
*
* @since 2.0.0
*
* @param array $args with the system email details to register.
* @return bool
*/
public function create_system_email($args) {
if ($this->is_created($args['slug'])) {
return;
} // end if;
$email_args = wp_parse_args($args, array(
'event' => '',
'title' => '',
'content' => '',
'slug' => '',
'target' => 'admin',
'style' => 'use_default',
'send_copy_to_admin' => true,
'active' => true,
'legacy' => false,
'date_registered' => wu_get_current_time('mysql', true),
'date_modified' => wu_get_current_time('mysql', true),
'status' => 'publish'
));
$email = new Email($email_args);
$saved = $email->save();
return is_wp_error($saved) ? $saved : $email;
} // end create_system_email;
/**
* Register all default system emails.
*
* @since 2.0.0
*
* @return void
*/
public function create_all_system_emails() {
$system_emails = wu_get_default_system_emails();
foreach ($system_emails as $email_key => $email_value) {
$this->create_system_email($email_value);
} // end foreach;
} // end create_all_system_emails;
/**
* Register all default system emails.
*
* @since 2.0.0
*
* @return void
*/
public function register_all_default_system_emails() {
/*
* Payment Successful - Admin
*/
$this->register_default_system_email(array(
'event' => 'payment_received',
'slug' => 'payment_received_admin',
'target' => 'admin',
'title' => __('You got a new payment!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/admin/payment-received'),
));
/*
* Payment Successful - Customer
*/
$this->register_default_system_email(array(
'event' => 'payment_received',
'slug' => 'payment_received_customer',
'target' => 'customer',
'title' => __('We got your payment!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/customer/payment-received'),
));
/*
* Site Published - Admin
*/
$this->register_default_system_email(array(
'event' => 'site_published',
'target' => 'admin',
'slug' => 'site_published_admin',
'title' => __('A new site was created on your Network!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/admin/site-published'),
));
/*
* Site Published - Customer
*/
$this->register_default_system_email(array(
'event' => 'site_published',
'target' => 'customer',
'slug' => 'site_published_customer',
'title' => __('Your site is ready!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/customer/site-published'),
));
/*
* Site Published - Customer
*/
$this->register_default_system_email(array(
'event' => 'confirm_email_address',
'target' => 'customer',
'slug' => 'confirm_email_address',
'title' => __('Confirm your email address!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/customer/confirm-email-address'),
));
/*
* Domain Created - Admin
*/
$this->register_default_system_email(array(
'event' => 'domain_created',
'target' => 'admin',
'slug' => 'domain_created_admin',
'title' => __('A new domain was added to your Network!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/admin/domain-created'),
));
/*
* Pending Renewal Payment Created - Customer
*/
$this->register_default_system_email(array(
'event' => 'renewal_payment_created',
'target' => 'customer',
'slug' => 'renewal_payment_created',
'title' => __('You have a new pending payment!', 'wp-ultimo'),
'content' => wu_get_template_contents('emails/customer/renewal-payment-created'),
));
do_action('wu_system_emails_after_register');
} // end register_all_default_system_emails;
/**
* Get a single or all default registered system emails.
*
* @since 2.0.0
*
* @param string $slug Default system email slug.
* @return array All default system emails.
*/
public function get_default_system_emails($slug = '') {
if ($slug && isset($this->registered_default_system_emails[$slug])) {
return $this->registered_default_system_emails[$slug];
} // end if;
return $this->registered_default_system_emails;
} // end get_default_system_emails;
/**
* Check if the system email already exists.
*
* @param mixed $slug Email slug to use as reference.
* @return bool Return email object or false.
*/
public function is_created($slug): bool {
return (bool) wu_get_email_by('slug', $slug);
} // end is_created;
/**
* Get the default template email.
*
* @since 2.0.0
*
* @param string $slug With the event slug.
* @return array With the email template.
*/
public function get_event_placeholders($slug = '') {
$placeholders = array();
if (wu_request('email_event')) {
$slug = wu_request('email_event');
} // end if;
if ($slug) {
$event = wu_get_event_type($slug);
if ($event) {
foreach (wu_maybe_lazy_load_payload($event['payload']) as $placeholder => $value) {
$name = ucwords(str_replace('_', ' ', $placeholder));
$placeholders[] = array(
'name' => $name,
'placeholder' => $placeholder
);
} // end foreach;
} // end if;
} // end if;
if (wu_request('email_event')) {
wp_send_json($placeholders);
} else {
return $placeholders;
} // end if;
} // end get_event_placeholders;
/**
* Sends a schedule email.
*
* @since 2.0.0
*
* @param array $to Email targets.
* @param string $subject Email subject.
* @param string $template Email content.
* @param array $headers Email headers.
* @param array $attachments Email attachments.
* @return mixed
*/
public function send_schedule_system_email($to, $subject, $template, $headers, $attachments) {
return Sender::send_mail($to, $subject, $template, $headers, $attachments);
} // end send_schedule_system_email;
/**
* Log failures on the WordPress mailer, just so we have a copy of the issues for debugging.
*
* @since 2.0.0
*
* @param WP_Error $error The error with the mailer.
* @return void.
*/
public function log_mailer_failure($error) {
if (is_wp_error($error)) {
wu_log_add('mailer-errors', $error->get_error_message(), LogLevel::ERROR);
} // end if;
} // end log_mailer_failure;
} // end class Email_Manager;

View File

@ -0,0 +1,728 @@
<?php
/**
* Events Manager
*
* Handles processes related to events.
*
* @package WP_Ultimo
* @subpackage Managers/Event_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Models\Base_Model;
use \WP_Ultimo\Models\Event;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to events.
*
* @since 2.0.0
*/
class Event_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'event';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Event';
/**
* Holds the list of available events for webhooks.
*
* @since 2.0.0
* @var array
*/
protected $events = array();
/**
* The list of registered models events.
*
* @since 2.1.4
* @var array
*/
protected $models_events = array();
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
add_action('plugins_loaded', array($this, 'register_all_events'));
add_action('wp_ajax_wu_get_event_payload_preview', array($this, 'event_payload_preview'));
add_action('rest_api_init', array($this, 'hooks_endpoint'));
add_action('wu_model_post_save', array($this, 'log_transitions'), 10, 4);
add_action('wu_daily', array($this, 'clean_old_events'));
} // end init;
/**
* Returns the payload to be displayed in the payload preview field.
* Log model transitions.
*
* @since 2.0.0
*
* @param string $model The model name.
* @param array $data The data being saved, serialized.
* @param array $data_unserialized The data being saved, un-serialized.
* @param Base_Model $object The object being saved.
* @return void
*/
public function log_transitions($model, $data, $data_unserialized, $object) {
if ($model === 'event') {
return;
} // end if;
/*
* Editing Model
*/
if (wu_get_isset($data_unserialized, 'id')) {
$original = $object->_get_original();
$diff = wu_array_recursive_diff($data_unserialized, $original);
$keys_to_remove = apply_filters('wu_exclude_transitions_keys', array(
'meta',
'last_login',
'ips',
'query_class',
'settings',
'_compiled_product_list',
'_gateway_info',
'_limitations',
));
foreach ($keys_to_remove as $key_to_remove) {
unset($diff[$key_to_remove]);
} // end foreach;
/**
* If empty, go home.
*/
if (empty($diff)) {
return;
} // end if;
$changed = array();
/**
* Loop changed data.
*/
foreach ($diff as $key => $new_value) {
$old_value = wu_get_isset($original, $key, '');
if ($key === 'id' && intval($old_value) === 0) {
return;
} // end if;
if (empty(json_encode($old_value)) && empty(json_encode($new_value))) {
return;
} // end if;
$changed[$key] = array(
'old_value' => $old_value,
'new_value' => $new_value,
);
} // end foreach;
$event_data = array(
'severity' => Event::SEVERITY_INFO,
'slug' => 'changed',
'object_type' => $model,
'object_id' => $object->get_id(),
'payload' => $changed,
);
} else {
$event_data = array(
'severity' => Event::SEVERITY_INFO,
'slug' => 'created',
'object_type' => $model,
'object_id' => $object->get_id(),
'payload' => array(),
);
} // end if;
if (!empty($_POST) && is_user_logged_in()) {
$event_data['initiator'] = 'manual';
$event_data['author_id'] = get_current_user_id();
} // end if;
return wu_create_event($event_data);
} // end log_transitions;
/**
* Returns the payload to be displayed in the payload preview field.
*
* @since 2.0.0
* @return void
*/
public function event_payload_preview() {
if (!wu_request('event')) {
wp_send_json_error(new \WP_Error('error', __('No event was selected.', 'wp-ultimo')));
} // end if;
$slug = wu_request('event');
if (!$slug) {
wp_send_json_error(new \WP_Error('not-found', __('Event was not found.', 'wp-ultimo')));
} // end if;
$event = wu_get_event_type($slug);
if (!$event) {
wp_send_json_error(new \WP_Error('not-found', __('Data not found.', 'wp-ultimo')));
} else {
$payload = isset($event['payload']) ? wu_maybe_lazy_load_payload($event['payload']) : '{}';
$payload = array_map('htmlentities2', $payload);
wp_send_json_success($payload);
} // end if;
} // end event_payload_preview;
/**
* Returns the list of event types to register.
*
* @since 2.0.0
* @return array
*/
public function get_event_type_as_options() {
/*
* We use this to order the options.
*/
$event_type_settings = wu_get_setting('saving_type', array());
$types = array(
'id' => '$id',
'title' => '$title',
'desc' => '$desc',
'class_name' => '$class_name',
'active' => 'in_array($id, $active_gateways, true)',
'active' => 'in_array($id, $active_gateways, true)',
'gateway' => '$class_name', // Deprecated.
'hidden' => false,
);
$types = array_filter($types, fn($item) => $item['hidden'] === false);
return $types;
} // end get_event_type_as_options;
/**
* Add a new event.
*
* @since 2.0.0
*
* @param string $slug The slug of the event. Something like payment_received.
* @param array $payload with the events information.
*
* @return array with returns message for now.
*/
public function do_event($slug, $payload) {
$registered_event = $this->get_event($slug);
if (!$registered_event) {
return array('error' => 'Event not found');
} // end if;
$payload_diff = array_diff_key(wu_maybe_lazy_load_payload($registered_event['payload']), $payload);
if (isset($payload_diff[0])) {
foreach ($payload_diff[0] as $diff_key => $diff_value) {
return array('error' => 'Param required:' . $diff_key);
} // end foreach;
} // end if;
$payload['wu_version'] = wu_get_version();
do_action('wu_event', $slug, $payload);
do_action("wu_event_{$slug}", $payload);
/**
* Saves in the database
*/
$this->save_event($slug, $payload);
} // end do_event;
/**
* Register a new event to be used as param.
*
* @since 2.0.0
*
* @param string $slug The slug of the event. Something like payment_received.
* @param array $args with the events information.
*
* @return true
*/
public function register_event($slug, $args): bool {
$this->events[$slug] = $args;
return true;
} // end register_event;
/**
* Returns the list of available webhook events.
*
* @since 2.0.0
* @return array $events with all events.
*/
public function get_events() {
return $this->events;
} // end get_events;
/**
* Returns the list of available webhook events.
*
* @since 2.0.0
*
* @param string $slug of the event.
* @return array $event with event params.
*/
public function get_event($slug) {
$events = $this->get_events();
if ($events) {
foreach ($events as $key => $event) {
if ($key === $slug) {
return $event;
} // end if;
} // end foreach;
} // end if;
return false;
} // end get_event;
/**
* Saves event in the database.
*
* @param string $slug of the event.
* @param array $payload with event params.
* @return void.
*/
public function save_event($slug, $payload) {
$event = new Event(array(
'object_id' => wu_get_isset($payload, 'object_id', ''),
'object_type' => wu_get_isset($payload, 'object_type', ''),
'severity' => wu_get_isset($payload, 'type', Event::SEVERITY_INFO),
'date_created' => wu_get_current_time('mysql', true),
'slug' => strtolower($slug),
'payload' => $payload,
));
$event->save();
} // end save_event;
/**
* Registers the list of default events.
*
* @since 2.0.0
* @return void
*/
public function register_all_events() {
/**
* Payment Received.
*/
wu_register_event_type('payment_received', array(
'name' => __('Payment Received', 'wp-ultimo'),
'desc' => __('This event is fired every time a new payment is received, regardless of the payment status.', 'wp-ultimo'),
'payload' => fn() => array_merge(
wu_generate_event_payload('payment'),
wu_generate_event_payload('membership'),
wu_generate_event_payload('customer')
),
'deprecated_args' => array(
'user_id' => 'customer_user_id',
'amount' => 'payment_total',
'gateway' => 'payment_gateway',
'status' => 'payment_status',
'date' => 'payment_date_created',
),
));
/**
* Site Published.
*/
wu_register_event_type('site_published', array(
'name' => __('Site Published', 'wp-ultimo'),
'desc' => __('This event is fired every time a new site is created tied to a membership, or transitions from a pending state to a published state.', 'wp-ultimo'),
'payload' => fn() => array_merge(
wu_generate_event_payload('site'),
wu_generate_event_payload('customer'),
wu_generate_event_payload('membership')
),
'deprecated_args' => array(),
));
/**
* Confirm Email Address
*/
wu_register_event_type('confirm_email_address', array(
'name' => __('Email Verification Needed', 'wp-ultimo'),
'desc' => __('This event is fired every time a new customer is added with an email verification status of pending.', 'wp-ultimo'),
'payload' => fn() => array_merge(
array(
'verification_link' => 'https://linktoverifyemail.com',
),
wu_generate_event_payload('customer')
),
'deprecated_args' => array(),
));
/**
* Domain Mapping Added
*/
wu_register_event_type('domain_created', array(
'name' => __('New Domain Mapping Added', 'wp-ultimo'),
'desc' => __('This event is fired every time a new domain mapping is added by a customer.', 'wp-ultimo'),
'payload' => fn() => array_merge(
wu_generate_event_payload('domain'),
wu_generate_event_payload('site'),
wu_generate_event_payload('membership'),
wu_generate_event_payload('customer')
),
'deprecated_args' => array(
'user_id' => 1,
'user_site_id' => 1,
'mapped_domain' => 'mydomain.com',
'user_site_url' => 'http://test.mynetwork.com/',
'network_ip' => '125.399.3.23',
),
));
/**
* Renewal payment created
*/
wu_register_event_type('renewal_payment_created', array(
'name' => __('New Renewal Payment Created', 'wp-ultimo'),
'desc' => __('This event is fired every time a new renewal payment is created by WP Ultimo.', 'wp-ultimo'),
'payload' => fn() => array_merge(
array(
'default_payment_url' => 'https://linktopayment.com',
),
wu_generate_event_payload('payment'),
wu_generate_event_payload('membership'),
wu_generate_event_payload('customer')
),
'deprecated_args' => array(),
));
$models = $this->models_events;
foreach ($models as $model => $params) {
foreach ($params['types'] as $type) {
wu_register_event_type($model . '_' . $type, array(
'name' => sprintf(__('%1$s %2$s', 'wp-ultimo'), $params['label'], ucfirst($type)),
'desc' => sprintf(__('This event is fired every time a %1$s is %2$s by WP Ultimo.', 'wp-ultimo'), $params['label'], $type),
'deprecated_args' => array(),
'payload' => fn() => $this->get_model_payload($model),
));
} // end foreach;
add_action("wu_{$model}_post_save", array($this, 'dispatch_base_model_event'), 10, 3);
} // end foreach;
do_action('wu_register_all_events');
} // end register_all_events;
/**
* Register models events
*
* @param string $slug slug of event.
* @param string $label label of event.
* @param array $event_types event types allowed.
* @since 2.1.4
*/
public static function register_model_events(string $slug, string $label, array $event_types): void {
$instance = self::get_instance();
$instance->models_events[$slug] = array(
'label' => $label,
'types' => $event_types,
);
} // end register_model_events;
/**
* Dispatch registered model events
*
* @param array $data Data.
* @param mixed $obj Object.
* @param bool $new New.
*
* @since 2.1.4
*/
public function dispatch_base_model_event(array $data, $obj, bool $new): void {
$model = $obj->model;
$type = $new ? 'created' : 'updated';
$registered_model = wu_get_isset($this->models_events, $model);
if (!$registered_model || !in_array($type, $registered_model['types'], true)) {
return;
} // end if;
$payload = $this->get_model_payload($model, $obj);
wu_do_event($model . '_' . $type, $payload);
} // end dispatch_base_model_event;
/**
* Returns the full payload for a given model.
*
* @param string $model The model name.
* @param object|null $model_object The model object.
* @return array
*
* @since 2.3.0
*/
public function get_model_payload(string $model, ?object $model_object = null) {
$obj = $model_object ?? call_user_func("wu_mock_{$model}");
$payload = wu_generate_event_payload($model, $obj);
if (method_exists($obj, 'get_membership')) {
$membership = $model_object ? $obj->get_membership() : false;
$payload = array_merge(
$payload,
wu_generate_event_payload('membership', $membership)
);
} // end if;
if (method_exists($obj, 'get_customer')) {
$customer = $model_object ? $obj->get_customer() : false;
$payload = array_merge(
$payload,
wu_generate_event_payload('customer', $customer)
);
} // end if;
if (method_exists($obj, 'get_billing_address') || method_exists($obj, 'get_membership')) {
if ($model_object) {
$payload = method_exists($obj, 'get_billing_address')
? array_merge(
$payload,
$obj->get_billing_address()->to_array()
) : array_merge(
$payload,
$obj->get_membership()->get_billing_address()->to_array()
);
} else {
$payload = array_merge(
$payload,
array_map(function () {
return '';
}, \WP_Ultimo\Objects\Billing_Address::fields())
);
} // end if;
} // end if;
return $payload;
} // end get_model_payload;
/**
* Every day, deletes old events that we don't want to keep.
*
* @since 2.0.0
*/
public function clean_old_events(): bool {
/*
* Add a filter setting this to 0 or false
* to prevent old events from being ever deleted.
*/
$threshold_days = apply_filters('wu_events_threshold_days', 1);
if (empty($threshold_days)) {
return false;
} // end if;
$events_to_remove = wu_get_events(array(
'number' => 100,
'date_query' => array(
'column' => 'date_created',
'before' => "-{$threshold_days} days",
'inclusive' => true,
),
));
$success_count = 0;
foreach ($events_to_remove as $event) {
$status = $event->delete();
if (!is_wp_error($status) && $status) {
$success_count++;
} // end if;
} // end foreach;
wu_log_add('wu-cron', sprintf(__('Removed %1$d events successfully. Failed to remove %2$d events.', 'wp-ultimo'), $success_count, count($events_to_remove) - $success_count));
return true;
} // end clean_old_events;
/**
* Create a endpoint to retrieve all available event hooks.
*
* @since 2.0.0
*
* @return mixed
*/
public function hooks_endpoint() {
if (!wu_get_setting('enable_api', true)) {
return;
} // end if;
$api = \WP_Ultimo\API::get_instance();
register_rest_route($api->get_namespace(), '/hooks', array(
'methods' => 'GET',
'callback' => array($this, 'get_hooks_rest'),
'permission_callback' => array($api, 'check_authorization'),
));
} // end hooks_endpoint;
/**
* Return all event types for the REST API request.
*
* @since 2.0.0
*
* @param WP_REST_Request $request The request sent.
* @return mixed
*/
public function get_hooks_rest($request) {
$response = wu_get_event_types();
foreach ($response as $key => $value) {
$payload = wu_get_isset($value, 'payload');
if (is_callable($payload)) {
$response[$key]['payload'] = $payload();
} // end if;
} // end foreach;
return rest_ensure_response($response);
} // end get_hooks_rest;
} // end class Event_Manager;

View File

@ -0,0 +1,312 @@
<?php
/**
* Field templates manager
*
* Keeps track of registered field templates.
*
* @package WP_Ultimo
* @subpackage Managers/Signup_Fields
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Managers\Base_Manager;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Keeps track of registered field templates.
*
* @since 2.0.0
*/
class Field_Templates_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Holds the instantiated field templates.
*
* @since 2.2.0
* @var array
*/
protected $holders = array();
/**
* Initialize the managers with the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
add_action('wu_ajax_nopriv_wu_render_field_template', array($this, 'serve_field_template'));
add_action('wu_ajax_wu_render_field_template', array($this, 'serve_field_template'));
} // end init;
/**
* Serve the HTML markup for the templates.
*
* @since 2.0.0
* @return void
*/
public function serve_field_template() {
$template = wu_replace_dashes(wu_request('template'));
$template_parts = explode('/', (string) $template);
$template_class = $this->get_template_class($template_parts[0], $template_parts[1]);
if (!$template_class) {
wp_send_json_error(new \WP_Error('template', __('Template not found.', 'wp-ultimo')));
} // end if;
$key = $template_parts[0];
$attributes = apply_filters("wu_{$key}_render_attributes", wu_request('attributes'));
wp_send_json_success(array(
'html' => $template_class->render($attributes),
));
} // end serve_field_template;
/**
* Returns the list of registered signup field types.
*
* Developers looking for add new types of fields to the signup
* should use the filter wu_checkout_forms_field_types to do so.
*
* @see wu_checkout_forms_field_types
*
* @since 2.0.0
* @return array
*/
public function get_field_templates() {
$field_templates = array();
/*
* Adds default template selection templates.
*/
$field_templates['template_selection'] = array(
'clean' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Template_Selection\\Clean_Template_Selection_Field_Template',
'minimal' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Template_Selection\\Minimal_Template_Selection_Field_Template',
'legacy' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Template_Selection\\Legacy_Template_Selection_Field_Template',
);
/*
* Adds the default period selector templates.
*/
$field_templates['period_selection'] = array(
'clean' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Period_Selection\\Clean_Period_Selection_Field_Template',
'legacy' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Period_Selection\\Legacy_Period_Selection_Field_Template',
);
/*
* Adds the default pricing table templates.
*/
$field_templates['pricing_table'] = array(
'list' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Pricing_Table\\List_Pricing_Table_Field_Template',
'legacy' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Pricing_Table\\Legacy_Pricing_Table_Field_Template',
);
/*
* Adds the default order-bump templates.
*/
$field_templates['order_bump'] = array(
'simple' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Order_Bump\\Simple_Order_Bump_Field_Template',
);
/*
* Adds the default order-summary templates.
*/
$field_templates['order_summary'] = array(
'clean' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Order_Summary\\Clean_Order_Summary_Field_Template',
);
/*
* Adds the default order-summary templates.
*/
$field_templates['steps'] = array(
'clean' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Steps\\Clean_Steps_Field_Template',
'minimal' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Steps\\Minimal_Steps_Field_Template',
'legacy' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Field_Templates\\Steps\\Legacy_Steps_Field_Template',
);
/*
* Allow developers to add new field templates
*/
do_action('wu_register_field_templates');
/**
* Our APIs to add new field templates hook into here.
* Do not use this filter directly. Use the wu_register_field_template()
* function instead.
*
* @see wu_register_field_template()
*
* @since 2.0.0
* @param array $field_templates
* @return array
*/
return apply_filters('wu_checkout_field_templates', $field_templates);
} // end get_field_templates;
/**
* Get the field templates for a field type. Returns only the class names.
*
* @since 2.0.0
*
* @param string $field_type The field type id.
* @return array
*/
public function get_templates($field_type) {
return wu_get_isset($this->get_field_templates(), $field_type, array());
} // end get_templates;
/**
* Get the instance of the template class.
*
* @since 2.0.0
*
* @param string $field_type The field type id.
* @param string $field_template_id The field template id.
* @return object
*/
public function get_template_class($field_type, $field_template_id) {
$templates = $this->get_instantiated_field_types($field_type);
return wu_get_isset($templates, $field_template_id);
} // end get_template_class;
/**
* Returns the field templates as a key => title array of options.
*
* @since 2.0.0
*
* @param string $field_type The field type id.
* @return array
*/
public function get_templates_as_options($field_type) {
$templates = $this->get_instantiated_field_types($field_type);
$options = array();
foreach ($templates as $template_id => $template) {
$options[$template_id] = $template->get_title();
} // end foreach;
return $options;
} // end get_templates_as_options;
/**
* Returns the field templates as a key => info_array array of fields.
*
* @since 2.0.0
*
* @param string $field_type The field type id.
* @return array
*/
public function get_templates_info($field_type) {
$templates = $this->get_instantiated_field_types($field_type);
$options = array();
foreach ($templates as $template_id => $template) {
$options[$template_id] = array(
'id' => $template_id,
'title' => $template->get_title(),
'description' => $template->get_description(),
'preview' => $template->get_preview(),
);
} // end foreach;
return $options;
} // end get_templates_info;
/**
* Instantiate a field template.
*
* @since 2.0.0
*
* @param string $class_name The class name.
* @return \WP_Ultimo\Checkout\Signup_Fields\Base_Signup_Field
*/
public function instantiate_field_template($class_name) {
return new $class_name();
} // end instantiate_field_template;
/**
* Returns an array with all fields, instantiated.
*
* @since 2.0.0
* @param string $field_type The field type id.
* @return array
*/
public function get_instantiated_field_types($field_type) {
$holder_name = "instantiated_{$field_type}_templates";
if (!isset($this->holders[$holder_name]) || $this->holders[$holder_name] === null) {
$this->holders[$holder_name] = array_map(array($this, 'instantiate_field_template'), $this->get_templates($field_type));
} // end if;
return $this->holders[$holder_name];
} // end get_instantiated_field_types;
/**
* Render preview block.
*
* @since 2.0.0
*
* @param string $field_type The field type id.
* @return string
*/
public function render_preview_block($field_type) {
$preview_block = '<div class="wu-w-full">';
foreach (Field_Templates_Manager::get_instance()->get_templates_info($field_type) as $template_slug => $template_info) {
$image_tag = $template_info['preview'] ? sprintf('<img class="wu-object-cover wu-image-preview wu-w-7 wu-h-7 wu-rounded wu-mr-3" src="%1$s" data-image="%1$s">', $template_info['preview']) : '<div class="wu-w-7 wu-h-7 wu-bg-gray-200 wu-rounded wu-text-gray-600 wu-flex wu-items-center wu-justify-center wu-mr-2">
<span class="dashicons-wu-image"></span>
</div>';
$preview_block .= sprintf("<div v-show='%4\$s_template === \"%1\$s\"' class='wu-w-full wu-flex wu-items-center'>
<div class='wu-flex wu-items-center'>%2\$s</div><div class='wu-flex-wrap wu-overflow-hidden'>%3\$s</div>
</div>", $template_info['id'], $image_tag, $template_info['description'], $field_type);
} // end foreach;
$preview_block .= '</div>';
return $preview_block;
} // end render_preview_block;
} // end class Field_Templates_Manager;

View File

@ -0,0 +1,629 @@
<?php
/**
* Gateway Manager
*
* Manages the registering and activation of gateways.
*
* @package WP_Ultimo
* @subpackage Managers/Gateway
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles the ajax form registering, rendering, and permissions checking.
*
* @since 2.0.0
*/
class Form_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Keeps the registered forms.
*
* @since 2.0.0
* @var array
*/
protected $registered_forms = array();
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
add_action('wu_ajax_wu_form_display', array($this, 'display_form'));
add_action('wu_ajax_wu_form_handler', array($this, 'handle_form'));
add_action('wu_register_forms', array($this, 'register_action_forms'));
add_action('wu_page_load', 'add_wubox');
do_action('wu_register_forms');
} // end init;
/**
* Displays the form unavailable message.
*
* This is returned when the form doesn't exist, or the
* logged user doesn't have the required permissions to see the form.
*
* @since 2.0.0
* @param \WP_Error|false $error Error message, if applicable.
* @return void
*/
public function display_form_unavailable($error = false) {
$message = __('Form not available', 'wp-ultimo');
if (is_wp_error($error)) {
$message = $error->get_error_message();
} // end if;
echo sprintf('
<div class="wu-modal-form wu-h-full wu-flex wu-items-center wu-justify-center wu-bg-gray-200 wu-m-0 wu-mt-0 wu--mb-3">
<div>
<span class="dashicons dashicons-warning wu-h-8 wu-w-8 wu-mx-auto wu-text-center wu-text-4xl wu-block"></span>
<span class="wu-block wu-text-sm">%s</span>
</div>
</div>
', $message);
do_action('wu_form_scripts', false);
die;
} // end display_form_unavailable;
/**
* Renders a registered form, when requested.
*
* @since 2.0.0
* @return void
*/
public function display_form() {
$this->security_checks();
$form = $this->get_form(wu_request('form'));
echo sprintf("<form class='wu_form wu-styling' id='%s' action='%s' method='post'>",
$form['id'],
$this->get_form_url($form['id'], array(
'action' => 'wu_form_handler',
)));
echo sprintf('
<div v-cloak data-wu-app="%s" data-state="%s">
<ul class="wu-p-4 wu-bg-red-200 wu-m-0 wu-list-none" v-if="errors.length">
<li class="wu-m-0 wu-p-0" v-for="error in errors">{{ error.message }}</li>
</ul>
</div>', $form['id'] . '_errors', htmlspecialchars(json_encode(array('errors' => array()))));
call_user_func($form['render']);
echo '<input type="hidden" name="action" value="wu_form_handler">';
wp_nonce_field('wu_form_' . $form['id']);
echo '</form>';
do_action('wu_form_scripts', $form);
exit;
} // end display_form;
/**
* Handles the submission of a registered form.
*
* @since 2.0.0
* @return void
*/
public function handle_form() {
$this->security_checks();
$form = $this->get_form(wu_request('form'));
if (!wp_verify_nonce(wu_request('_wpnonce'), 'wu_form_' . $form['id'])) {
wp_send_json_error();
} // end if;
/**
* The handler is supposed to send a wp_json message back.
* However, if it returns a WP_Error object, we know
* something went wrong and that we should display the error message.
*/
$check = call_user_func($form['handler']);
if (is_wp_error($check)) {
$this->display_form_unavailable($check);
} // end if;
exit;
} // end handle_form;
/**
* Checks that the form exists and that the user has permission to see it.
*
* @since 2.0.0
* @return mixed
*/
public function security_checks() {
/*
* We only want ajax requests.
*/
if ((empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower((string) $_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest')) {
wp_die(0);
} // end if;
$form = $this->get_form(wu_request('form'));
if (!$form) {
return $this->display_form_unavailable();
} // end if;
if (!current_user_can($form['capability'])) {
return $this->display_form_unavailable();
} // end if;
} // end security_checks;
/**
* Returns a list of all the registered gateways.
*
* @since 2.0.0
* @return array
*/
public function get_registered_forms() {
return $this->registered_forms;
} // end get_registered_forms;
/**
* Checks if a form is already registered.
*
* @since 2.0.0
*
* @param string $id The id of the form.
* @return boolean
*/
public function is_form_registered($id) {
return is_array($this->registered_forms) && isset($this->registered_forms[$id]);
} // end is_form_registered;
/**
* Returns a registered form.
*
* @since 2.0.0
*
* @param string $id The id of the form to return.
* @return array
*/
public function get_form($id) {
return $this->is_form_registered($id) ? $this->registered_forms[$id] : false;
} // end get_form;
/**
* Registers a new Ajax Form.
*
* Ajax forms are forms that get loaded via an ajax call using thickbox (or rather our fork).
* This is useful for displaying inline edit forms that support Vue and our
* Form/Fields API.
*
* @since 2.0.0
*
* @param string $id Form id.
* @param array $atts Form attributes, check wp_parse_atts call below.
* @return void
*/
public function register_form($id, $atts = array()) {
$atts = wp_parse_args($atts, array(
'id' => $id,
'form' => '',
'capability' => 'manage_network',
'handler' => '__return_false',
'render' => '__return_empty_string',
));
// Checks if gateway was already added
if ($this->is_form_registered($id)) {
return;
} // end if;
$this->registered_forms[$id] = $atts;
return true;
} // end register_form;
/**
* Returns the ajax URL for a given form.
*
* @since 2.0.0
*
* @param string $form_id The id of the form to return.
* @param array $atts List of parameters, check wp_parse_args below.
* @return string
*/
public function get_form_url($form_id, $atts = array()) {
$atts = wp_parse_args($atts, array(
'form' => $form_id,
'action' => 'wu_form_display',
'width' => '400',
'height' => '360',
));
return add_query_arg($atts, wu_ajax_url('init'));
} // end get_form_url;
/**
* Register the confirmation modal form to delete a customer.
*
* @since 2.0.0
*/
public function register_action_forms() {
$model = wu_request('model');
wu_register_form('delete_modal', array(
'render' => array($this, 'render_model_delete_form'),
'handler' => array($this, 'handle_model_delete_form'),
'capability' => "wu_delete_{$model}s",
));
wu_register_form('bulk_actions', array(
'render' => array($this, 'render_bulk_action_form'),
'handler' => array($this, 'handle_bulk_action_form'),
));
add_action('wu_handle_bulk_action_form', array($this, 'default_bulk_action_handler'), 100, 3);
} // end register_action_forms;
/**
* Renders the deletion confirmation form.
*
* @since 2.0.0
* @return void
*/
public function render_model_delete_form() {
$model = wu_request('model');
$id = wu_request('id');
$meta_key = false;
if ($model) {
/*
* Handle metadata elements passed as model
*/
if (strpos((string) $model, '_meta_') !== false) {
$elements = explode('_meta_', (string) $model);
$model = $elements[0];
$meta_key = $elements[1];
} // end if;
try {
$object = call_user_func("wu_get_{$model}", $id);
} catch (\Throwable $exception) {
// No need to do anything, but cool to stop fatal errors.
} // end try;
$object = apply_filters("wu_delete_form_get_object_{$model}", $object, $id, $model);
if (!$object) {
$this->display_form_unavailable(new \WP_Error('not-found', __('Object not found.', 'wp-ultimo')));
return;
} // end if;
$fields = apply_filters(
"wu_form_fields_delete_{$model}_modal",
array(
'confirm' => array(
'type' => 'toggle',
'title' => __('Confirm Deletion', 'wp-ultimo'),
'desc' => __('This action can not be undone.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'confirmed',
),
),
'submit_button' => array(
'type' => 'submit',
'title' => __('Delete', 'wp-ultimo'),
'placeholder' => __('Delete', 'wp-ultimo'),
'value' => 'save',
'classes' => 'button button-primary wu-w-full',
'wrapper_classes' => 'wu-items-end',
'html_attr' => array(
'v-bind:disabled' => '!confirmed',
),
),
'id' => array(
'type' => 'hidden',
'value' => $object->get_id(),
),
'meta_key' => array(
'type' => 'hidden',
'value' => $meta_key,
),
'redirect_to' => array(
'type' => 'hidden',
'value' => wu_request('redirect_to'),
),
'model' => array(
'type' => 'hidden',
'value' => $model,
),
),
$object
);
$form_attributes = apply_filters("wu_form_attributes_delete_{$model}_modal", array(
'title' => 'Delete',
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'true',
'data-state' => json_encode(array(
'confirmed' => false,
)),
),
));
$form = new \WP_Ultimo\UI\Form('total-actions', $fields, $form_attributes);
do_action("wu_before_render_delete_{$model}_modal", $form);
$form->render();
} // end if;
} // end render_model_delete_form;
/**
* Handles the deletion of customer.
*
* @since 2.0.0
* @return void
*/
public function handle_model_delete_form() {
global $wpdb;
$model = wu_request('model');
$id = wu_request('id');
$meta_key = wu_request('meta_key');
$redirect_to = wu_request('redirect_to', wp_get_referer());
$plural_name = str_replace('_', '-', (string) $model) . 's';
if ($model) {
/*
* Handle meta key deletion
*/
if ($meta_key) {
$status = delete_metadata('wu_membership', wu_request('id'), 'pending_site');
$data_json_success = array(
'redirect_url' => add_query_arg('deleted', 1, $redirect_to),
);
wp_send_json_success($data_json_success);
exit;
} // end if;
try {
$object = call_user_func("wu_get_{$model}", $id);
} catch (\Throwable $exception) {
// No need to do anything, but cool to stop fatal errors.
} // end try;
$object = apply_filters("wu_delete_form_get_object_{$model}", $object, $id, $model);
if (!$object) {
wp_send_json_error(new \WP_Error('not-found', __('Object not found.', 'wp-ultimo')));
} // end if;
/*
* Handle objects (default state)
*/
do_action("wu_before_delete_{$model}_modal", $object);
$saved = $object->delete();
if (is_wp_error($saved)) {
wp_send_json_error($saved);
} // end if;
do_action("wu_after_delete_{$model}_modal", $object);
$data_json_success = apply_filters("wu_data_json_success_delete_{$model}_modal", array(
'redirect_url' => wu_network_admin_url("wp-ultimo-{$plural_name}", array('deleted' => 1))
));
wp_send_json_success($data_json_success);
} else {
wp_send_json_error(new \WP_Error('model-not-found', __('Something went wrong.', 'wp-ultimo')));
} // end if;
} // end handle_model_delete_form;
/**
* Renders the deletion confirmation form.
*
* @since 2.0.0
* @return void
*/
public function render_bulk_action_form() {
$action = wu_request('bulk_action');
$model = wu_request('model');
$fields = apply_filters("wu_bulk_actions_{$model}_{$action}", array(
'confirm' => array(
'type' => 'toggle',
'title' => __('Confirm Action', 'wp-ultimo'),
'desc' => __('Review this action carefully.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'confirmed',
),
),
'submit_button' => array(
'type' => 'submit',
'title' => wu_slug_to_name($action),
'placeholder' => wu_slug_to_name($action),
'value' => 'save',
'classes' => 'button button-primary wu-w-full',
'wrapper_classes' => 'wu-items-end',
'html_attr' => array(
'v-bind:disabled' => '!confirmed',
),
),
'model' => array(
'type' => 'hidden',
'value' => $model,
),
'bulk_action' => array(
'type' => 'hidden',
'value' => wu_request('bulk_action'),
),
'ids' => array(
'type' => 'hidden',
'value' => implode(',', wu_request('bulk-delete', '')),
),
));
$form_attributes = apply_filters("wu_bulk_actions_{$action}_form", array(
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'true',
'data-state' => json_encode(array(
'confirmed' => false,
)),
),
));
$form = new \WP_Ultimo\UI\Form('total-actions', $fields, $form_attributes);
$form->render();
} // end render_bulk_action_form;
/**
* Handles the deletion of customer.
*
* @since 2.0.0
* @return void
*/
public function handle_bulk_action_form() {
global $wpdb;
$action = wu_request('bulk_action');
$model = wu_request('model');
$ids = explode(',', (string) wu_request('ids', ''));
do_action("wu_handle_bulk_action_form_{$model}_{$action}", $action, $model, $ids);
do_action('wu_handle_bulk_action_form', $action, $model, $ids);
} // end handle_bulk_action_form;
/**
* Default handler for bulk actions.
*
* @since 2.0.0
*
* @param string $action The action.
* @param string $model The model.
* @param array $ids The ids list.
* @return void
*/
public function default_bulk_action_handler($action, $model, $ids) {
$status = \WP_Ultimo\List_Tables\Base_List_Table::process_bulk_action();
if (is_wp_error($status)) {
wp_send_json_error($status);
} // end if;
wp_send_json_success(array(
'redirect_url' => add_query_arg($action, count($ids), wu_get_current_url()),
));
} // end default_bulk_action_handler;
} // end class Form_Manager;

View File

@ -0,0 +1,582 @@
<?php
/**
* Gateway Manager
*
* Manages the registering and activation of gateways.
*
* @package WP_Ultimo
* @subpackage Managers/Gateway
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Dependencies\Psr\Log\LogLevel;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Gateways\Ignorable_Exception;
use \WP_Ultimo\Gateways\Free_Gateway;
use \WP_Ultimo\Gateways\Stripe_Gateway;
use \WP_Ultimo\Gateways\Stripe_Checkout_Gateway;
use \WP_Ultimo\Gateways\PayPal_Gateway;
use \WP_Ultimo\Gateways\Manual_Gateway;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Manages the registering and activation of gateways.
*
* @since 2.0.0
*/
class Gateway_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Lists the registered gateways.
*
* @since 2.0.0
* @var array
*/
protected $registered_gateways = array();
/**
* Lists the gateways that are enabled.
*
* @since 2.0.0
* @var array
*/
protected $enabled_gateways = array();
/**
* Keeps a list of the gateways with auto-renew.
*
* @since 2.0.0
* @var array
*/
protected $auto_renewable_gateways = array();
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
add_action('plugins_loaded', array($this, 'on_load'));
} // end init;
/**
* Runs after all plugins have been loaded to allow for add-ons to hook into it correctly.
*
* @since 2.0.0
* @return void
*/
public function on_load() {
/*
* Adds our own default gateways.
*/
add_action('wu_register_gateways', array($this, 'add_default_gateways'), 5);
/*
* Adds the Gateway selection fields
*/
add_action('init', array($this, 'add_gateway_selector_field'));
/*
* Handle gateway confirmations.
* We need it both on the front-end and the back-end.
*/
add_action('template_redirect', array($this, 'process_gateway_confirmations'), -99999);
add_action('load-admin_page_wu-checkout', array($this, 'process_gateway_confirmations'), -99999);
/*
* Waits for webhook signals and deal with them.
*/
add_action('init', array($this, 'maybe_process_webhooks'), 1);
/*
* Waits for webhook signals and deal with them.
*/
add_action('admin_init', array($this, 'maybe_process_v1_webhooks'), 1);
/*
* Allow developers to add new gateways.
*/
do_action('wu_register_gateways');
} // end on_load;
/**
* Checks if we need to process webhooks received by gateways.
*
* @since 2.0.0
* @return void
*/
public function maybe_process_webhooks() {
$gateway = wu_request('wu-gateway');
if ($gateway && !is_admin() && is_main_site()) {
/*
* Do not cache this!
*/
!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore
try {
/*
* Passes it down to Gateways.
*
* Gateways will hook into here
* to handle their respective webhook
* calls.
*
* We also wrap it inside a try/catch
* to make sure we log errors,
* tell network admins, and make sure
* the gateway tries again by sending back
* a non-200 HTTP code.
*/
do_action("wu_{$gateway}_process_webhooks");
http_response_code(200);
die('Thanks!');
} catch (Ignorable_Exception $e) {
$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());
wu_log_add("wu-{$gateway}-webhook-errors", $message);
/*
* Send the error back, but with a 200.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 200);
} catch (\Throwable $e) {
$file = $e->getFile();
$line = $e->getLine();
$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());
$message .= PHP_EOL . "Location: {$file}:{$line}";
$message .= PHP_EOL . $e->getTraceAsString();
wu_log_add("wu-{$gateway}-webhook-errors", $message, LogLevel::ERROR);
/*
* Force a 500.
*
* Most gateways will try again later when
* a non-200 code is returned.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 500);
} // end try;
} // end if;
} // end maybe_process_webhooks;
/**
* Checks if we need to process webhooks received by legacy gateways.
*
* @since 2.0.4
* @return void
*/
public function maybe_process_v1_webhooks() {
$action = wu_request('action', '');
if ($action && strpos((string) $action, 'notify_gateway_') !== false) {
/*
* Get the gateway id from the action.
*/
$gateway_id = str_replace(array('nopriv_', 'notify_gateway_'), '', (string) $action);
$gateway = wu_get_gateway($gateway_id);
if ($gateway) {
$gateway->before_backwards_compatible_webhook();
/*
* Do not cache this!
*/
!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore
try {
/*
* Passes it down to Gateways.
*
* Gateways will hook into here
* to handle their respective webhook
* calls.
*
* We also wrap it inside a try/catch
* to make sure we log errors,
* tell network admins, and make sure
* the gateway tries again by sending back
* a non-200 HTTP code.
*/
do_action("wu_{$gateway_id}_process_webhooks");
http_response_code(200);
die('Thanks!');
} catch (Ignorable_Exception $e) {
$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());
wu_log_add("wu-{$gateway_id}-webhook-errors", $message);
/*
* Send the error back, but with a 200.
*/
wp_send_json_error(new \WP_Error('webhook-error', $message), 200);
} catch (\Throwable $e) {
$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());
wu_log_add("wu-{$gateway_id}-webhook-errors", $message, LogLevel::ERROR);
/*
* Force a 500.
*
* Most gateways will try again later when
* a non-200 code is returned.
*/
http_response_code(500);
wp_send_json_error(new \WP_Error('webhook-error', $message));
} // end try;
} // end if;
} // end if;
} // end maybe_process_v1_webhooks;
/**
* Let gateways deal with their confirmation steps.
*
* This is the case for PayPal Express.
*
* @since 2.0.0
* @return void
*/
public function process_gateway_confirmations() {
/*
* First we check for the confirmation parameter.
*/
if (!wu_request('wu-confirm') || (wu_request('status') && wu_request('status') === 'done')) {
return;
} // end if;
ob_start();
add_filter('body_class', fn($classes) => array_merge($classes, array('wu-process-confirmation')));
$gateway_id = sanitize_text_field(wu_request('wu-confirm'));
$gateway = wu_get_gateway($gateway_id);
if (!$gateway) {
$error = new \WP_Error('missing_gateway', __('Missing gateway parameter.', 'wp-ultimo'));
wp_die($error, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));
} // end if;
try {
$payment_hash = wu_request('payment');
$payment = wu_get_payment_by_hash($payment_hash);
if ($payment) {
$gateway->set_payment($payment);
} // end if;
/*
* Pass it down to the gateway.
*
* Here you can throw exceptions, that
* we will catch it and throw it as a wp_die
* message.
*/
$results = $gateway->process_confirmation();
if (is_wp_error($results)) {
wp_die($results, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));
} // end if;
} catch (\Throwable $e) {
$error = new \WP_Error('confirm-error-' . $e->getCode(), $e->getMessage());
wp_die($error, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));
} // end try;
$output = ob_get_clean();
if (!empty($output)) {
/*
* Add a filter to bypass the checkout form.
* This is used for PayPal confirmation page.
*/
add_action('wu_bypass_checkout_form', fn($bypass, $atts) => $output, 10, 2);
} // end if;
} // end process_gateway_confirmations;
/**
* Adds the field that enabled and disables Payment Gateways on the settings.
*
* @since 2.0.0
* @return void
*/
public function add_gateway_selector_field() {
wu_register_settings_field('payment-gateways', 'active_gateways', array(
'title' => __('Active Payment Gateways', 'wp-ultimo'),
'desc' => __('Payment gateways are what your customers will use to pay.', 'wp-ultimo'),
'type' => 'multiselect',
'columns' => 2,
'options' => array($this, 'get_gateways_as_options'),
'default' => array(),
));
} // end add_gateway_selector_field;
/**
* Returns the list of registered gateways as options for the gateway selector setting.
*
* @since 2.0.0
* @return array
*/
public function get_gateways_as_options() {
/*
* We use this to order the options.
*/
$active_gateways = wu_get_setting('active_gateways', array());
$gateways = $this->get_registered_gateways();
$gateways = array_filter($gateways, fn($item) => $item['hidden'] === false);
return $gateways;
} // end get_gateways_as_options;
/**
* Loads the default gateways.
*
* @since 2.0.0
* @return void
*/
public function add_default_gateways() {
/*
* Free Payments
*/
wu_register_gateway('free', __('Free', 'wp-ultimo'), '', Free_Gateway::class, true);
/*
* Stripe Payments
*/
$stripe_desc = __('Stripe is a suite of payment APIs that powers commerce for businesses of all sizes, including subscription management.', 'wp-ultimo');
wu_register_gateway('stripe', __('Stripe', 'wp-ultimo'), $stripe_desc, Stripe_Gateway::class);
/*
* Stripe Checkout Payments
*/
$stripe_checkout_desc = __('Stripe Checkout is the hosted solution for checkouts using Stripe.', 'wp-ultimo');
wu_register_gateway('stripe-checkout', __('Stripe Checkout', 'wp-ultimo'), $stripe_checkout_desc, Stripe_Checkout_Gateway::class);
/*
* PayPal Payments
*/
$paypal_desc = __('PayPal is the leading provider in checkout solutions and it is the easier way to get your network subscriptions going.', 'wp-ultimo');
wu_register_gateway('paypal', __('PayPal', 'wp-ultimo'), $paypal_desc, PayPal_Gateway::class);
/*
* Manual Payments
*/
$manual_desc = __('Use the Manual Gateway to allow users to pay you directly via bank transfers, checks, or other channels.', 'wp-ultimo');
wu_register_gateway('manual', __('Manual', 'wp-ultimo'), $manual_desc, Manual_Gateway::class);
} // end add_default_gateways;
/**
* Checks if a gateway was already registered.
*
* @since 2.0.0
* @param string $id The id of the gateway.
* @return boolean
*/
public function is_gateway_registered($id) {
return is_array($this->registered_gateways) && isset($this->registered_gateways[$id]);
} // end is_gateway_registered;
/**
* Returns a list of all the registered gateways
*
* @since 2.0.0
* @return array
*/
public function get_registered_gateways() {
return $this->registered_gateways;
} // end get_registered_gateways;
/**
* Returns a particular Gateway registered
*
* @since 2.0.0
* @param string $id The id of the gateway.
* @return array
*/
public function get_gateway($id) {
return $this->is_gateway_registered($id) ? $this->registered_gateways[$id] : false;
} // end get_gateway;
/**
* Adds a new Gateway to the System. Used by gateways to make themselves visible.
*
* @since 2.0.0
*
* @param string $id ID of the gateway. This is how we will identify the gateway in the system.
* @param string $title Name of the gateway.
* @param string $desc A description of the gateway to help super admins understand what services they integrate with.
* @param string $class_name Gateway class name.
* @param bool $hidden If we need to hide this gateway publicly.
* @return bool
*/
public function register_gateway($id, $title, $desc, $class_name, $hidden = false) {
// Checks if gateway was already added
if ($this->is_gateway_registered($id)) {
return;
} // end if;
$active_gateways = (array) wu_get_setting('active_gateways', array());
// Adds to the global
$this->registered_gateways[$id] = array(
'id' => $id,
'title' => $title,
'desc' => $desc,
'class_name' => $class_name,
'active' => in_array($id, $active_gateways, true),
'active' => in_array($id, $active_gateways, true),
'hidden' => (bool) $hidden,
'gateway' => $class_name, // Deprecated.
);
$this->install_hooks($class_name);
// Return the value
return true;
} // end register_gateway;
/**
* Adds additional hooks for each of the gateway registered.
*
* @since 2.0.0
*
* @param string $class_name Gateway class name.
* @return void
*/
public function install_hooks($class_name) {
$gateway = new $class_name();
$gateway_id = $gateway->get_id();
/*
* If the gateway supports recurring
* payments, add it to the list.
*/
if ($gateway->supports_recurring()) {
$this->auto_renewable_gateways[] = $gateway_id;
} // end if;
add_action('wu_checkout_scripts', array($gateway, 'register_scripts'));
add_action('init', array($gateway, 'hooks'));
add_action('init', array($gateway, 'settings'));
add_action("wu_{$gateway_id}_process_webhooks", array($gateway, 'process_webhooks'));
add_action("wu_{$gateway_id}_remote_payment_url", array($gateway, 'get_payment_url_on_gateway'));
add_action("wu_{$gateway_id}_remote_subscription_url", array($gateway, 'get_subscription_url_on_gateway'));
add_action("wu_{$gateway_id}_remote_customer_url", array($gateway, 'get_customer_url_on_gateway'));
/*
* Renders the gateway fields.
*/
add_action('wu_checkout_gateway_fields', function($checkout) use ($gateway) {
$field_content = call_user_func(array($gateway, 'fields'));
ob_start();
?>
<div v-cloak v-show="gateway == '<?php echo esc_attr($gateway->get_id()); ?>' && order && order.should_collect_payment" class="wu-overflow">
<?php echo $field_content; ?>
</div>
<?php
echo ob_get_clean();
});
} // end install_hooks;
/**
* Returns an array with the list of gateways that support auto-renew.
*
* @since 2.0.0
* @return mixed
*/
public function get_auto_renewable_gateways() {
return (array) $this->auto_renewable_gateways;
} // end get_auto_renewable_gateways;
} // end class Gateway_Manager;

View File

@ -0,0 +1,38 @@
<?php
/**
* Broadcast Manager
*
* Handles processes related to products.
*
* @package WP_Ultimo
* @subpackage Managers/Job
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Logger;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to products.
*
* @since 2.0.0
*/
class Job_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
} // end init;
} // end class Job_Manager;

View File

@ -0,0 +1,918 @@
<?php
/**
* Limitation Manager
*
* Handles processes related to limitations.
*
* @package WP_Ultimo
* @subpackage Managers/Limitation_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
// Exit if accessed directly
defined('ABSPATH') || exit;
use \WP_Ultimo\Dependencies\Psr\Log\LogLevel;
use \WP_Ultimo\Objects\Limitations;
use \WP_Ultimo\Database\Sites\Site_Type;
/**
* Handles processes related to limitations.
*
* @since 2.0.0
*/
class Limitation_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
if (WP_Ultimo()->is_loaded() === false) {
return;
} // end if;
add_filter('wu_product_options_sections', array($this, 'add_limitation_sections'), 10, 2);
add_filter('wu_membership_options_sections', array($this, 'add_limitation_sections'), 10, 2);
add_filter('wu_site_options_sections', array($this, 'add_limitation_sections'), 10, 2);
add_action('plugins_loaded', array($this, 'register_forms'));
add_action('wu_async_handle_plugins', array($this, 'async_handle_plugins'), 10, 5);
add_action('wu_async_switch_theme', array($this, 'async_switch_theme'), 10, 2);
} // end init;
/**
* Handles async plugin activation and deactivation.
*
* @since 2.0.0
*
* @param string $action The action to perform, can be either 'activate' or 'deactivate'.
* @param int $site_id The site ID.
* @param string|array $plugins The plugin or list of plugins to (de)activate.
* @param boolean $network_wide If we want to (de)activate it network-wide.
* @param boolean $silent IF we should do the process silently - true by default.
* @return bool
*/
public function async_handle_plugins($action, $site_id, $plugins, $network_wide = false, $silent = true) {
$results = false;
// Avoid doing anything on the main site.
if (wu_get_main_site_id() === $site_id) {
return $results;
} // end if;
switch_to_blog($site_id);
if ($action === 'activate') {
$results = activate_plugins($plugins, '', $network_wide, $silent);
} elseif ($action === 'deactivate') {
$results = deactivate_plugins($plugins, $silent, $network_wide);
} // end if;
if (is_wp_error($results)) {
wu_log_add('plugins', $results, LogLevel::ERROR);
} // end if;
restore_current_blog();
return $results;
} // end async_handle_plugins;
/**
* Switch themes via Job Queue.
*
* @since 2.0.0
*
* @param int $site_id The site ID.
* @param string $theme_stylesheet The theme stylesheet.
* @return true
*/
public function async_switch_theme($site_id, $theme_stylesheet): bool {
switch_to_blog($site_id);
switch_theme($theme_stylesheet);
restore_current_blog();
return true;
} // end async_switch_theme;
/**
* Register the modal windows to confirm resetting the limitations.
*
* @since 2.0.0
* @return void
*/
public function register_forms() {
wu_register_form('confirm_limitations_reset', array(
'render' => array($this, 'render_confirm_limitations_reset'),
'handler' => array($this, 'handle_confirm_limitations_reset'),
));
} // end register_forms;
/**
* Renders the conformation modal to reset limitations.
*
* @since 2.0.0
* @return void
*/
public function render_confirm_limitations_reset() {
$fields = array(
'confirm' => array(
'type' => 'toggle',
'title' => __('Confirm Reset', 'wp-ultimo'),
'desc' => __('This action can not be undone.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'confirmed',
),
),
'submit_button' => array(
'type' => 'submit',
'title' => __('Reset Limitations', 'wp-ultimo'),
'value' => 'save',
'classes' => 'button button-primary wu-w-full',
'wrapper_classes' => 'wu-items-end',
'html_attr' => array(
'v-bind:disabled' => '!confirmed',
),
),
'id' => array(
'type' => 'hidden',
'value' => wu_request('id'),
),
'model' => array(
'type' => 'hidden',
'value' => wu_request('model'),
),
);
$form_attributes = array(
'title' => __('Reset', 'wp-ultimo'),
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'reset_limitations',
'data-state' => json_encode(array(
'confirmed' => false,
)),
),
);
$form = new \WP_Ultimo\UI\Form('reset_limitations', $fields, $form_attributes);
$form->render();
} // end render_confirm_limitations_reset;
/**
* Handles the reset of permissions.
*
* @since 2.0.0
* @return void
*/
public function handle_confirm_limitations_reset() {
$id = wu_request('id');
$model = wu_request('model');
if (!$id || !$model) {
wp_send_json_error(new \WP_Error(
'parameters-not-found',
__('Required parameters are missing.', 'wp-ultimo')
));
} // end if;
/*
* Remove limitations object
*/
Limitations::remove_limitations($model, $id);
wp_send_json_success(array(
'redirect_url' => wu_network_admin_url("wp-ultimo-edit-{$model}", array(
'id' => $id,
'updated' => 1,
))
));
} // end handle_confirm_limitations_reset;
/**
* Returns the type of the object that has limitations.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object Model to test.
* @return string
*/
public function get_object_type($object) {
$model = false;
if (is_a($object, \WP_Ultimo\Models\Site::class)) {
$model = 'site';
} elseif (is_a($object, WP_Ultimo\Models\Membership::class)) {
$model = 'membership';
} elseif (is_a($object, \WP_Ultimo\Models\Product::class)) {
$model = 'product';
} // end if;
return apply_filters('wu_limitations_get_object_type', $model);
} // end get_object_type;
/**
* Injects the limitations panels when necessary.
*
* @since 2.0.0
*
* @param array $sections List of tabbed widget sections.
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The model being edited.
* @return array
*/
public function add_limitation_sections($sections, $object) {
if ($this->get_object_type($object) === 'site' && $object->get_type() !== Site_Type::CUSTOMER_OWNED) {
$html = sprintf('<span class="wu--mt-4 wu-p-2 wu-bg-blue-100 wu-text-blue-600 wu-rounded wu-block">%s</span>', __('Limitations are only available for customer-owned sites. You need to change the type to Customer-owned and save this site before the options are shown.', 'wp-ultimo'));
$sections['sites'] = array(
'title' => __('Limits', 'wp-ultimo'),
'desc' => __('Only customer-owned sites have limitations.', 'wp-ultimo'),
'icon' => 'dashicons-wu-browser',
'fields' => array(
'note' => array(
'type' => 'html',
'content' => $html,
)
)
);
return $sections;
} // end if;
if ($this->get_object_type($object) !== 'site') {
$sections['sites'] = array(
'title' => __('Sites', 'wp-ultimo'),
'desc' => __('Control limitations imposed to the number of sites allowed for memberships attached to this product.', 'wp-ultimo'),
'icon' => 'dashicons-wu-browser',
'fields' => $this->get_sites_fields($object),
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'limit_sites' => $object->get_limitations()->sites->is_enabled(),
),
);
} // end if;
/*
* Add Visits limitation control
*/
if ((bool) wu_get_setting('enable_visits_limiting', true)) {
$sections['visits'] = array(
'title' => __('Visits', 'wp-ultimo'),
'desc' => __('Control limitations imposed to the number of unique visitors allowed for memberships attached to this product.', 'wp-ultimo'),
'icon' => 'dashicons-wu-man',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'limit_visits' => $object->get_limitations()->visits->is_enabled(),
),
'fields' => array(
'modules[visits][enabled]' => array(
'type' => 'toggle',
'title' => __('Limit Unique Visits', 'wp-ultimo'),
'desc' => __('Toggle this option to enable unique visits limitation.', 'wp-ultimo'),
'value' => 10,
'html_attr' => array(
'v-model' => 'limit_visits'
),
),
),
);
if ($object->model !== 'product') {
$sections['visits']['fields']['modules_visits_overwrite'] = $this->override_notice($object->get_limitations(false)->visits->has_own_enabled());
} // end if;
$sections['visits']['fields']['modules[visits][limit]'] = array(
'type' => 'number',
'title' => __('Unique Visits Quota', 'wp-ultimo'),
'desc' => __('Set a top limit for the number of monthly unique visits. Leave empty or 0 to allow for unlimited visits.', 'wp-ultimo'),
'placeholder' => __('e.g. 10000', 'wp-ultimo'),
'value' => $object->get_limitations()->visits->get_limit(),
'wrapper_html_attr' => array(
'v-show' => 'limit_visits',
'v-cloak' => '1',
),
'html_attr' => array(
':min' => 'limit_visits ? 1 : -999',
),
);
if ($object->model !== 'product') {
$sections['visits']['fields']['allowed_visits_overwrite'] = $this->override_notice($object->get_limitations(false)->visits->has_own_limit(), array('limit_visits'));
} // end if;
/*
* If this is a site edit screen, show the current values
* for visits and the reset date
*/
if ($this->get_object_type($object) === 'site') {
$sections['visits']['fields']['visits_count'] = array(
'type' => 'text-display',
'title' => __('Current Unique Visits Count this Month', 'wp-ultimo'),
'desc' => __('Current visits count for this particular site.', 'wp-ultimo'),
'display_value' => sprintf('%s visit(s)', $object->get_visits_count()),
'wrapper_html_attr' => array(
'v-show' => 'limit_visits',
'v-cloak' => '1',
),
);
} // end if;
} // end if;
$sections['users'] = array(
'title' => __('Users', 'wp-ultimo'),
'desc' => __('Control limitations imposed to the number of user allowed for memberships attached to this product.', 'wp-ultimo'),
'icon' => 'dashicons-wu-users',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'limit_users' => $object->get_limitations()->users->is_enabled(),
),
'fields' => array(
'modules[users][enabled]' => array(
'type' => 'toggle',
'title' => __('Limit User', 'wp-ultimo'),
'desc' => __('Enable user limitations for this product.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'limit_users'
),
),
),
);
if ($object->model !== 'product') {
$sections['users']['fields']['modules_user_overwrite'] = $this->override_notice($object->get_limitations(false)->users->has_own_enabled());
} // end if;
$this->register_user_fields($sections, $object);
$sections['post_types'] = array(
'title' => __('Post Types', 'wp-ultimo'),
'desc' => __('Control limitations imposed to the number of posts allowed for memberships attached to this product.', 'wp-ultimo'),
'icon' => 'dashicons-wu-book',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'limit_post_types' => $object->get_limitations()->post_types->is_enabled(),
),
'fields' => array(
'modules[post_types][enabled]' => array(
'type' => 'toggle',
'title' => __('Limit Post Types', 'wp-ultimo'),
'desc' => __('Toggle this option to set limits to each post type.', 'wp-ultimo'),
'value' => false,
'html_attr' => array(
'v-model' => 'limit_post_types',
),
),
),
);
if ($object->model !== 'product') {
$sections['post_types']['fields']['post_quota_overwrite'] = $this->override_notice($object->get_limitations(false)->post_types->has_own_enabled());
} // end if;
$sections['post_types']['post_quota_note'] = array(
'type' => 'note',
'desc' => __('<strong>Note:</strong> Using the fields below you can set a post limit for each of the post types activated. <br>Toggle the switch to <strong>deactivate</strong> the post type altogether. Leave 0 or blank for unlimited posts.', 'wp-ultimo'),
'wrapper_html_attr' => array(
'v-show' => 'limit_post_types',
'v-cloak' => '1',
),
);
$this->register_post_type_fields($sections, $object);
$sections['limit_disk_space'] = array(
'title' => __('Disk Space', 'wp-ultimo'),
'desc' => __('Control limitations imposed to the disk space allowed for memberships attached to this entity.', 'wp-ultimo'),
'icon' => 'dashicons-wu-drive',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'limit_disk_space' => $object->get_limitations()->disk_space->is_enabled(),
),
'fields' => array(
'modules[disk_space][enabled]' => array(
'type' => 'toggle',
'title' => __('Limit Disk Space per Site', 'wp-ultimo'),
'desc' => __('Enable disk space limitations for this entity.', 'wp-ultimo'),
'value' => true,
'html_attr' => array(
'v-model' => 'limit_disk_space',
),
),
),
);
if ($object->model !== 'product') {
$sections['limit_disk_space']['fields']['disk_space_modules_overwrite'] = $this->override_notice($object->get_limitations(false)->disk_space->has_own_enabled());
} // end if;
$sections['limit_disk_space']['fields']['modules[disk_space][limit]'] = array(
'type' => 'number',
'title' => __('Disk Space Allowance', 'wp-ultimo'),
'desc' => __('Set a limit in MBs for the disk space for <strong>each</strong> individual site.', 'wp-ultimo'),
'min' => 0,
'placeholder' => 100,
'value' => $object->get_limitations()->disk_space->get_limit(),
'wrapper_html_attr' => array(
'v-show' => "get_state_value('product_type', 'none') !== 'service' && limit_disk_space",
'v-cloak' => '1',
),
);
if ($object->model !== 'product') {
$sections['limit_disk_space']['fields']['disk_space_override'] = $this->override_notice($object->get_limitations(false)->disk_space->has_own_limit(), array('limit_disk_space'));
} // end if;
$sections['custom_domain'] = array(
'title' => __('Custom Domains', 'wp-ultimo'),
'desc' => __('Limit the number of users on each role, posts, pages, and more.', 'wp-ultimo'),
'icon' => 'dashicons-wu-link1',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'allow_domain_mapping' => $object->get_limitations()->domain_mapping->is_enabled(),
),
'fields' => array(
'modules[domain_mapping][enabled]' => array(
'type' => 'toggle',
'title' => __('Allow Custom Domains', 'wp-ultimo'),
'desc' => __('Toggle this option on to allow this plan to enable custom domains for sign-ups on this plan.', 'wp-ultimo'),
'value' => $object->get_limitations()->domain_mapping->is_enabled(),
'wrapper_html_attr' => array(
'v-cloak' => '1',
),
'html_attr' => array(
'v-model' => 'allow_domain_mapping',
),
),
),
);
if ($object->model !== 'product') {
$sections['custom_domain']['fields']['custom_domain_override'] = $this->override_notice($object->get_limitations(false)->domain_mapping->has_own_enabled(), array('allow_domain_mapping'));
} // end if;
$sections['allowed_themes'] = array(
'title' => __('Themes', 'wp-ultimo'),
'desc' => __('Limit the number of users on each role, posts, pages, and more.', 'wp-ultimo'),
'icon' => 'dashicons-wu-palette',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'state' => array(
'force_active_theme' => '',
),
'fields' => array(
'themes' => array(
'type' => 'html',
'title' => __('Themes', 'wp-ultimo'),
'desc' => __('Select how the themes installed on the network should behave.', 'wp-ultimo'),
'content' => fn() => $this->get_theme_selection_list($object, $sections['allowed_themes']),
),
),
);
$sections['allowed_plugins'] = array(
'title' => __('Plugins', 'wp-ultimo'),
'desc' => __('You can choose the behavior of each plugin installed on the platform.', 'wp-ultimo'),
'icon' => 'dashicons-wu-power-plug',
'v-show' => "get_state_value('product_type', 'none') !== 'service'",
'fields' => array(
'plugins' => array(
'type' => 'html',
'title' => __('Plugins', 'wp-ultimo'),
'desc' => __('Select how the plugins installed on the network should behave.', 'wp-ultimo'),
'content' => fn() => $this->get_plugin_selection_list($object),
),
),
);
$reset_url = wu_get_form_url('confirm_limitations_reset', array(
'id' => $object->get_id(),
'model' => $object->model,
));
$sections['reset_limitations'] = array(
'title' => __('Reset Limitations', 'wp-ultimo'),
'desc' => __('Reset the limitations applied to this element.', 'wp-ultimo'),
'icon' => 'dashicons-wu-back-in-time',
'fields' => array(
'reset_permissions' => array(
'type' => 'note',
'title' => sprintf("%s<span class='wu-normal-case wu-block wu-text-xs wu-font-normal wu-mt-1'>%s</span>", __('Reset Limitations', 'wp-ultimo'), __('Use this option to reset the custom limitations applied to this object.', 'wp-ultimo')),
'desc' => sprintf('<a href="%s" title="%s" class="wubox button-primary">%s</a>', $reset_url, __('Reset Limitations', 'wp-ultimo'), __('Reset Limitations', 'wp-ultimo')),
),
),
);
return $sections;
} // end add_limitation_sections;
/**
* Generates the override notice.
*
* @since 2.0.0
*
* @param boolean $show Wether or not to show the field.
* @param array $additional_checks Array containing javascript conditions that need to be met.
* @return array
*/
protected function override_notice($show = false, $additional_checks = array()) {
$text = sprintf('<p class="wu-m-0 wu-p-2 wu-bg-blue-100 wu-text-blue-600 wu-rounded">%s</p>', __('This value is being applied only to this entity. Changes made to the membership or product permissions will not affect this particular value.', 'wp-ultimo'));
return array(
'desc' => $text,
'type' => 'note',
'wrapper_classes' => 'wu-pt-0',
'wrapper_html_attr' => array(
'v-show' => ($additional_checks ? (implode(' && ', $additional_checks) . ' && ') : '') . var_export((bool) $show, true),
'v-cloak' => '1',
'style' => 'border-top-width: 0 !important',
),
);
} // end override_notice;
/**
* Register the user roles fields
*
* @since 2.0.0
*
* @param array $sections Sections and fields.
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The object being edit.
* @return void
*/
public function register_user_fields(&$sections, $object) {
$user_roles = get_editable_roles();
$sections['users']['state']['roles'] = array();
foreach ($user_roles as $user_role_slug => $user_role) {
$sections['users']['state']['roles'][$user_role_slug] = $object->get_limitations()->users->{$user_role_slug};
$sections['users']['fields']["control_{$user_role_slug}"] = array(
'type' => 'group',
'title' => sprintf(__('Limit %s Role', 'wp-ultimo'), $user_role['name']),
'desc' => sprintf(__('The customer will be able to create %s users(s) of this user role.', 'wp-ultimo'), "{{ roles['{$user_role_slug}'].enabled ? ( parseInt(roles['{$user_role_slug}'].number, 10) ? roles['{$user_role_slug}'].number : '" . __('unlimited', 'wp-ultimo') . "' ) : '" . __('no', 'wp-ultimo') . "' }}"),
'tooltip' => '',
'wrapper_html_attr' => array(
'v-bind:class' => "!roles['{$user_role_slug}'].enabled ? 'wu-opacity-75' : ''",
'v-show' => 'limit_users',
'v-cloak' => '1',
),
'fields' => array(
"modules[users][limit][{$user_role_slug}][number]" => array(
'type' => 'number',
'placeholder' => sprintf(__('%s Role Quota. e.g. 10', 'wp-ultimo'), $user_role['name']),
'min' => 0,
'wrapper_classes' => 'wu-w-full',
'html_attr' => array(
'v-model' => "roles['{$user_role_slug}'].number",
'v-bind:readonly' => "!roles['{$user_role_slug}'].enabled",
),
),
"modules[users][limit][{$user_role_slug}][enabled]" => array(
'type' => 'toggle',
'wrapper_classes' => 'wu-mt-1',
'html_attr' => array(
'v-model' => "roles['{$user_role_slug}'].enabled",
),
),
),
);
/*
* Add override notice.
*/
if ($object->model !== 'product') {
$sections['users']['fields']["override_{$user_role_slug}"] = $this->override_notice($object->get_limitations(false)->users->exists($user_role_slug), array('limit_users'));
} // end if;
} // end foreach;
} // end register_user_fields;
/**
* Register the post type fields
*
* @since 2.0.0
*
* @param array $sections Sections and fields.
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The object being edit.
* @return void
*/
public function register_post_type_fields(&$sections, $object) {
$post_types = get_post_types(array(), 'objects');
$sections['post_types']['state']['types'] = array();
foreach ($post_types as $post_type_slug => $post_type) {
$sections['post_types']['state']['types'][$post_type_slug] = $object->get_limitations()->post_types->{$post_type_slug};
$sections['post_types']['fields']["control_{$post_type_slug}"] = array(
'type' => 'group',
'title' => sprintf(__('Limit %s', 'wp-ultimo'), $post_type->label),
'desc' => sprintf(__('The customer will be able to create %s post(s) of this post type.', 'wp-ultimo'), "{{ types['{$post_type_slug}'].enabled ? ( parseInt(types['{$post_type_slug}'].number, 10) ? types['{$post_type_slug}'].number : '" . __('unlimited', 'wp-ultimo') . "' ) : '" . __('no', 'wp-ultimo') . "' }}"),
'tooltip' => '',
'wrapper_html_attr' => array(
'v-bind:class' => "!types['{$post_type_slug}'].enabled ? 'wu-opacity-75' : ''",
'v-show' => 'limit_post_types',
'v-cloak' => '1',
),
'fields' => array(
"modules[post_types][limit][{$post_type_slug}][number]" => array(
'type' => 'number',
'placeholder' => sprintf(__('%s Quota. e.g. 200', 'wp-ultimo'), $post_type->label),
'min' => 0,
'wrapper_classes' => 'wu-w-full',
'html_attr' => array(
'v-model' => "types['{$post_type_slug}'].number",
'v-bind:readonly' => "!types['{$post_type_slug}'].enabled",
),
),
"modules[post_types][limit][{$post_type_slug}][enabled]" => array(
'type' => 'toggle',
'wrapper_classes' => 'wu-mt-1',
'html_attr' => array(
'v-model' => "types['{$post_type_slug}'].enabled",
),
),
),
);
/*
* Add override notice.
*/
if ($object->model !== 'product') {
$sections['post_types']['fields']["override_{$post_type_slug}"] = $this->override_notice($object->get_limitations(false)->post_types->exists($post_type_slug), array(
'limit_post_types'
));
} // end if;
} // end foreach;
} // end register_post_type_fields;
/**
* Returns the list of fields for the site tab.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The model being edited.
* @return array
*/
protected function get_sites_fields($object) {
$fields = array(
'modules[sites][enabled]' => array(
'type' => 'toggle',
'title' => __('Limit Sites', 'wp-ultimo'),
'desc' => __('Enable site limitations for this product.', 'wp-ultimo'),
'value' => $object->get_limitations()->sites->is_enabled(),
'html_attr' => array(
'v-model' => 'limit_sites'
),
),
);
if ($object->model !== 'product') {
$fields['sites_overwrite'] = $this->override_notice($object->get_limitations(false)->sites->has_own_enabled());
} // end if;
/*
* Sites not supported on this type
*/
$fields['site_not_allowed_note'] = array(
'type' => 'note',
'desc' => __('The product type selection does not support allowing for the creating of extra sites.', 'wp-ultimo'),
'tooltip' => '',
'wrapper_html_attr' => array(
'v-show' => "get_state_value('product_type', 'none') === 'service' && limit_sites",
'v-cloak' => '1',
),
);
$fields['modules[sites][limit]'] = array(
'type' => 'number',
'min' => 1,
'title' => __('Site Allowance', 'wp-ultimo'),
'desc' => __('This is the number of sites the customer will be able to create under this membership.', 'wp-ultimo'),
'placeholder' => 1,
'value' => $object->get_limitations()->sites->get_limit(),
'wrapper_html_attr' => array(
'v-show' => "get_state_value('product_type', 'none') !== 'service' && limit_sites",
'v-cloak' => '1',
),
);
if ($object->model !== 'product') {
$fields['sites_overwrite_2'] = $this->override_notice($object->get_limitations(false)->sites->has_own_limit(), array("get_state_value('product_type', 'none') !== 'service' && limit_sites"));
} // end if;
return apply_filters('wu_limitations_get_sites_fields', $fields, $object, $this);
} // end get_sites_fields;
/**
* Returns the HTML markup for the plugin selector list.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The model being edited.
* @return string
*/
public function get_plugin_selection_list($object) {
$all_plugins = $this->get_all_plugins();
return wu_get_template_contents('limitations/plugin-selector', array(
'plugins' => $all_plugins,
'object' => $object,
));
} // end get_plugin_selection_list;
/**
* Returns the HTML markup for the plugin selector list.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Trait\Trait_Limitable $object The model being edited.
* @param array $section The section array.
* @return string
*/
public function get_theme_selection_list($object, &$section) {
$all_themes = $this->get_all_themes();
return wu_get_template_contents('limitations/theme-selector', array(
'section' => $section,
'themes' => $all_themes,
'object' => $object,
));
} // end get_theme_selection_list;
/**
* Returns a list of all plugins available as options, excluding WP Ultimo.
*
* We also exclude a couple more.
*
* @since 2.0.0
* @return array
*/
public function get_all_plugins() {
$all_plugins = get_plugins();
$listed_plugins = array();
foreach ($all_plugins as $plugin_path => $plugin_info) {
if (wu_get_isset($plugin_info, 'Network') === true) {
continue;
} // end if;
if (in_array($plugin_path, $this->plugin_exclusion_list(), true)) {
continue;
} // end if;
$listed_plugins[$plugin_path] = $plugin_info;
} // end foreach;
return $listed_plugins;
} // end get_all_plugins;
/**
* Returns a list of all themes available as options, after filtering.
*
* @since 2.0.0
*/
public function get_all_themes(): array {
$all_plugins = wp_get_themes();
return array_filter($all_plugins, fn($path) => !in_array($path, $this->theme_exclusion_list(), true), ARRAY_FILTER_USE_KEY);
} // end get_all_themes;
/**
* Returns the exclusion list for plugins.
*
* We don't want people forcing WP Ultimo to be deactivated, do we?
*
* @since 2.0.0
* @return array
*/
protected function plugin_exclusion_list() {
$exclusion_list = array(
'wp-ultimo/wp-ultimo.php',
'user-switching/user-switching.php',
);
return apply_filters('wu_limitations_plugin_exclusion_list', $exclusion_list);
} // end plugin_exclusion_list;
/**
* Returns the exclusion list for themes.
*
* @since 2.0.0
* @return array
*/
protected function theme_exclusion_list() {
$exclusion_list = array();
return apply_filters('wu_limitations_theme_exclusion_list', $exclusion_list);
} // end theme_exclusion_list;
} // end class Limitation_Manager;

View File

@ -0,0 +1,479 @@
<?php
/**
* Membership Manager
*
* Handles processes related to memberships.
*
* @package WP_Ultimo
* @subpackage Managers/Membership_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Dependencies\Psr\Log\LogLevel;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Database\Memberships\Membership_Status;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to memberships.
*
* @since 2.0.0
*/
class Membership_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'membership';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Membership';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
Event_Manager::register_model_events('membership', __('Membership', 'wp-ultimo'), array('created', 'updated'));
add_action('wu_async_transfer_membership', array($this, 'async_transfer_membership'), 10, 2);
add_action('wu_async_delete_membership', array($this, 'async_delete_membership'), 10);
/*
* Transitions
*/
add_action('wu_transition_membership_status', array($this, 'mark_cancelled_date'), 10, 3);
add_action('wu_transition_membership_status', array($this, 'transition_membership_status'), 10, 3);
/*
* Deal with delayed/schedule swaps
*/
add_action('wu_async_membership_swap', array($this, 'async_membership_swap'), 10);
/*
* Deal with pending sites creation
*/
add_action('wp_ajax_wu_publish_pending_site', array($this, 'publish_pending_site'));
add_action('wp_ajax_wu_check_pending_site_created', array($this, 'check_pending_site_created'));
add_action('wu_async_publish_pending_site', array($this, 'async_publish_pending_site'), 10);
} // end init;
/**
* Processes a delayed site publish action.
*
* @since 2.0.11
*/
public function publish_pending_site() {
check_ajax_referer('wu_publish_pending_site');
ignore_user_abort(true);
// Don't make the request block till we finish, if possible.
if ( function_exists( 'fastcgi_finish_request' ) && version_compare( phpversion(), '7.0.16', '>=' ) ) {
wp_send_json(array( 'status' => 'creating-site'));
fastcgi_finish_request();
} // end if;
$membership_id = wu_request('membership_id');
$this->async_publish_pending_site($membership_id);
exit; // Just exit the request
} // end publish_pending_site;
/**
* Processes a delayed site publish action.
*
* @since 2.0.0
*
* @param int $membership_id The membership id.
* @return bool|\WP_Error
*/
public function async_publish_pending_site($membership_id) {
$membership = wu_get_membership($membership_id);
if (!$membership) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$status = $membership->publish_pending_site();
if (is_wp_error($status)) {
wu_log_add('site-errors', $status, LogLevel::ERROR);
} // end if;
return $status;
} // end async_publish_pending_site;
/**
* Processes a delayed site publish action.
*
* @since 2.0.11
*/
public function check_pending_site_created() {
$membership_id = wu_request('membership_hash');
$membership = wu_get_membership_by_hash($membership_id);
if (!$membership) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$pending_site = $membership->get_pending_site();
if (!$pending_site) {
/**
* We do not have a pending site, so we can assume the site was created.
*/
wp_send_json(array('publish_status' => 'completed'));
exit;
} // end if;
wp_send_json(array('publish_status' => $pending_site->is_publishing() ? 'running' : 'stopped'));
exit;
} // end check_pending_site_created;
/**
* Processes a membership swap.
*
* @since 2.0.0
*
* @param int $membership_id The membership id.
* @return bool|\WP_Error
*/
public function async_membership_swap($membership_id) {
global $wpdb;
$membership = wu_get_membership($membership_id);
if (!$membership) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$scheduled_swap = $membership->get_scheduled_swap();
if (empty($scheduled_swap)) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$order = $scheduled_swap->order;
$wpdb->query('START TRANSACTION');
try {
$membership->swap($order);
$status = $membership->save();
if (is_wp_error($status)) {
$wpdb->query('ROLLBACK');
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
} catch (\Throwable $exception) {
$wpdb->query('ROLLBACK');
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end try;
/*
* Clean up the membership swap order.
*/
$membership->delete_scheduled_swap();
$wpdb->query('COMMIT');
return true;
} // end async_membership_swap;
/**
* Watches the change in payment status to take action when needed.
*
* @todo Publishing sites should be done in async.
*
* @since 2.0.0
*
* @param string $old_status The old status of the membership.
* @param string $new_status The new status of the membership.
* @param integer $membership_id Payment ID.
* @return void
*/
public function transition_membership_status($old_status, $new_status, $membership_id) {
$allowed_previous_status = array(
Membership_Status::PENDING,
Membership_Status::ON_HOLD,
);
if (!in_array($old_status, $allowed_previous_status, true)) {
return;
} // end if;
$allowed_status = array(
Membership_Status::ACTIVE,
Membership_Status::TRIALING,
);
if (!in_array($new_status, $allowed_status, true)) {
return;
} // end if;
/*
* Create pending sites.
*/
$membership = wu_get_membership($membership_id);
$status = $membership->publish_pending_site_async();
if (is_wp_error($status)) {
wu_log_add('site-errors', $status, LogLevel::ERROR);
} // end if;
} // end transition_membership_status;
/**
* Mark the membership date of cancellation.
*
* @since 2.0.0
*
* @param string $old_value Old status value.
* @param string $new_value New status value.
* @param int $item_id The membership id.
* @return void
*/
public function mark_cancelled_date($old_value, $new_value, $item_id) {
if ($new_value === 'cancelled' && $new_value !== $old_value) {
$membership = wu_get_membership($item_id);
$membership->set_date_cancellation(wu_get_current_time('mysql', true));
$membership->save();
} // end if;
} // end mark_cancelled_date;
/**
* Transfer a membership from a user to another.
*
* @since 2.0.0
*
* @param int $membership_id The ID of the membership being transferred.
* @param int $target_customer_id The new owner.
* @return mixed
*/
public function async_transfer_membership($membership_id, $target_customer_id) {
global $wpdb;
$membership = wu_get_membership($membership_id);
$target_customer = wu_get_customer($target_customer_id);
if (!$membership || !$target_customer || absint($membership->get_customer_id()) === absint($target_customer->get_id())) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$wpdb->query('START TRANSACTION');
try {
/*
* Get Sites and move them over.
*/
$sites = wu_get_sites(array(
'meta_query' => array(
'membership_id' => array(
'key' => 'wu_membership_id',
'value' => $membership->get_id(),
),
),
));
foreach ($sites as $site) {
$site->set_customer_id($target_customer_id);
$saved = $site->save();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} // end foreach;
/*
* Change the membership
*/
$membership->set_customer_id($target_customer_id);
$saved = $membership->save();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return new \WP_Error('exception', $e->getMessage());
} // end try;
$wpdb->query('COMMIT');
$membership->unlock();
return true;
} // end async_transfer_membership;
/**
* Delete a membership.
*
* @since 2.0.0
*
* @param int $membership_id The ID of the membership being deleted.
* @return mixed
*/
public function async_delete_membership($membership_id) {
global $wpdb;
$membership = wu_get_membership($membership_id);
if (!$membership) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$wpdb->query('START TRANSACTION');
try {
/*
* Get Sites and delete them.
*/
$sites = wu_get_sites(array(
'meta_query' => array(
'membership_id' => array(
'key' => 'wu_membership_id',
'value' => $membership->get_id(),
),
),
));
foreach ($sites as $site) {
$saved = $site->delete();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} // end foreach;
/*
* Delete the membership
*/
$saved = $membership->delete();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return new \WP_Error('exception', $e->getMessage());
} // end try;
$wpdb->query('COMMIT');
return true;
} // end async_delete_membership;
} // end class Membership_Manager;

View File

@ -0,0 +1,467 @@
<?php
/**
* Notes Manager
*
* Handles processes related to notes.
*
* @package WP_Ultimo
* @subpackage Managers/Notes
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Managers\Base_Manager;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to notes.
*
* @since 2.0.0
*/
class Notes_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'notes';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Notes';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
add_action('plugins_loaded', array($this, 'register_forms'));
add_filter('wu_membership_options_sections', array($this, 'add_notes_options_section'), 10, 2);
add_filter('wu_payments_options_sections', array($this, 'add_notes_options_section'), 10, 2);
add_filter('wu_customer_options_sections', array($this, 'add_notes_options_section'), 10, 2);
add_filter('wu_site_options_sections', array($this, 'add_notes_options_section'), 10, 2);
} // end init;
/**
* Register ajax forms that we use for object.
*
* @since 2.0.0
* @return void
*/
public function register_forms() {
/*
* Add note
*/
wu_register_form('add_note', array(
'render' => array($this, 'render_add_note_modal'),
'handler' => array($this, 'handle_add_note_modal'),
'capability' => 'edit_notes',
));
/*
* Clear notes
*/
wu_register_form('clear_notes', array(
'render' => array($this, 'render_clear_notes_modal'),
'handler' => array($this, 'handle_clear_notes_modal'),
'capability' => 'delete_notes',
));
/*
* Clear notes
*/
wu_register_form('delete_note', array(
'render' => array($this, 'render_delete_note_modal'),
'handler' => array($this, 'handle_delete_note_modal'),
'capability' => 'delete_notes',
));
} // end register_forms;
/**
* Add all domain mapping settings.
*
* @since 2.0.0
*
* @param array $sections Array sections.
* @param object $object The object.
*
* @return array
*/
public function add_notes_options_section($sections, $object) {
if (!current_user_can('read_notes') && !current_user_can('edit_notes')) {
return $sections;
} // end if;
$fields = array();
$fields['notes_panel'] = array(
'type' => 'html',
'wrapper_classes' => 'wu-m-0 wu-p-2 wu-notes-wrapper',
'wrapper_html_attr' => array(
'style' => sprintf('min-height: 500px; background: url("%s");', wu_get_asset('pattern-wp-ultimo.png')),
),
'content' => wu_get_template_contents('base/edit/display-notes', array(
'notes' => $object->get_notes(),
'model' => $object->model,
)),
);
$fields_buttons = array();
if (current_user_can('delete_notes')) {
$fields_buttons['button_clear_notes'] = array(
'type' => 'link',
'display_value' => __('Clear Notes', 'wp-ultimo'),
'wrapper_classes' => 'wu-mb-0',
'classes' => 'button wubox',
'html_attr' => array(
'href' => wu_get_form_url('clear_notes', array(
'object_id' => $object->get_id(),
'model' => $object->model,
)),
'title' => __('Clear Notes', 'wp-ultimo'),
),
);
} // end if;
if (current_user_can('edit_notes')) {
$fields_buttons['button_add_note'] = array(
'type' => 'link',
'display_value' => __('Add new Note', 'wp-ultimo'),
'wrapper_classes' => 'wu-mb-0',
'classes' => 'button button-primary wubox wu-absolute wu-right-5',
'html_attr' => array(
'href' => wu_get_form_url('add_note', array(
'object_id' => $object->get_id(),
'model' => $object->model,
'height' => 306,
)),
'title' => __('Add new Note', 'wp-ultimo'),
),
);
} // end if;
$fields['buttons'] = array(
'type' => 'group',
'wrapper_classes' => 'wu-bg-white',
'fields' => $fields_buttons,
);
$sections['notes'] = array(
'title' => __('Notes', 'wp-ultimo'),
'desc' => __('Add notes to this model.', 'wp-ultimo'),
'icon' => 'dashicons-wu-text-document',
'order' => 1001,
'fields' => $fields,
);
return $sections;
} // end add_notes_options_section;
/**
* Renders the notes form.
*
* @since 2.0.0
* @return void
*/
public function render_add_note_modal() {
$fields = array(
'content' => array(
'id' => 'content',
'type' => 'wp-editor',
'title' => __('Note Content', 'wp-ultimo'),
'desc' => __('Basic formatting is supported.', 'wp-ultimo'),
'settings' => array(
'tinymce' => array(
'toolbar1' => 'bold,italic,strikethrough,link,unlink,undo,redo,pastetext',
),
),
'html_attr' => array(
'v-model' => 'content',
),
),
'submit_add_note' => array(
'type' => 'submit',
'title' => __('Add Note', 'wp-ultimo'),
'placeholder' => __('Add Note', 'wp-ultimo'),
'value' => 'save',
'classes' => 'wu-w-full button button-primary',
'wrapper_classes' => 'wu-items-end',
),
'object_id' => array(
'type' => 'hidden',
'value' => wu_request('object_id'),
),
'model' => array(
'type' => 'hidden',
'value' => wu_request('model'),
),
);
$fields = apply_filters('wu_notes_options_section_fields', $fields);
$form = new \WP_Ultimo\UI\Form('add_note', $fields, array(
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'add_note',
'data-state' => wu_convert_to_state(array(
'content' => '',
)),
),
));
$form->render();
} // end render_add_note_modal;
/**
* Handles the notes form.
*
* @since 2.0.0
* @return void
*/
public function handle_add_note_modal() {
$model = wu_request('model');
$function_name = "wu_get_{$model}";
$object = $function_name(wu_request('object_id'));
$status = $object->add_note(array(
'text' => wu_remove_empty_p(wu_request('content')),
'author_id' => get_current_user_id(),
'note_id' => uniqid(),
));
if (is_wp_error($status)) {
wp_send_json_error($status);
} // end if;
wp_send_json_success(array(
'redirect_url' => wu_network_admin_url("wp-ultimo-edit-{$model}", array(
'id' => $object->get_id(),
'updated' => 1,
'options' => 'notes',
)),
));
} // end handle_add_note_modal;
/**
* Renders the clear notes confirmation form.
*
* @since 2.0.0
* @return void
*/
public function render_clear_notes_modal() {
$fields = array(
'confirm_clear_notes' => array(
'type' => 'toggle',
'title' => __('Confirm clear all notes?', 'wp-ultimo'),
'desc' => __('This action can not be undone.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'confirmed',
),
),
'submit_clear_notes' => array(
'type' => 'submit',
'title' => __('Clear Notes', 'wp-ultimo'),
'placeholder' => __('Clear Notes', 'wp-ultimo'),
'value' => 'save',
'classes' => 'wu-w-full button button-primary',
'wrapper_classes' => 'wu-items-end',
'html_attr' => array(
'v-bind:disabled' => '!confirmed',
),
),
'object_id' => array(
'type' => 'hidden',
'value' => wu_request('object_id'),
),
'model' => array(
'type' => 'hidden',
'value' => wu_request('model'),
),
);
$form = new \WP_Ultimo\UI\Form('clear_notes', $fields, array(
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'clear_notes',
'data-state' => wu_convert_to_state(array(
'confirmed' => false,
)),
),
));
$form->render();
} // end render_clear_notes_modal;
/**
* Handles the clear notes modal.
*
* @since 2.0.0
* @return void
*/
public function handle_clear_notes_modal() {
$model = wu_request('model');
$function_name = "wu_get_{$model}";
$object = $function_name(wu_request('object_id'));
if (!$object) {
return;
} // end if;
$status = $object->clear_notes();
if (is_wp_error($status)) {
wp_send_json_error($status);
} // end if;
wp_send_json_success(array(
'redirect_url' => wu_network_admin_url("wp-ultimo-edit-{$model}", array(
'id' => $object->get_id(),
'deleted' => 1,
'options' => 'notes',
)),
));
} // end handle_clear_notes_modal;
/**
* Renders the delete note confirmation form.
*
* @since 2.0.0
* @return void
*/
public function render_delete_note_modal() {
$fields = array(
'confirm_delete_note' => array(
'type' => 'toggle',
'title' => __('Confirm clear the note?', 'wp-ultimo'),
'desc' => __('This action can not be undone.', 'wp-ultimo'),
'html_attr' => array(
'v-model' => 'confirmed',
),
),
'submit_delete_note' => array(
'type' => 'submit',
'title' => __('Clear Note', 'wp-ultimo'),
'placeholder' => __('Clear Note', 'wp-ultimo'),
'value' => 'save',
'classes' => 'wu-w-full button button-primary',
'wrapper_classes' => 'wu-items-end',
'html_attr' => array(
'v-bind:disabled' => '!confirmed',
),
),
'object_id' => array(
'type' => 'hidden',
'value' => wu_request('object_id'),
),
'model' => array(
'type' => 'hidden',
'value' => wu_request('model'),
),
'note_id' => array(
'type' => 'hidden',
'value' => wu_request('note_id'),
),
);
$form = new \WP_Ultimo\UI\Form('delete_note', $fields, array(
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
'html_attr' => array(
'data-wu-app' => 'delete_note',
'data-state' => wu_convert_to_state(array(
'confirmed' => false,
)),
),
));
$form->render();
} // end render_delete_note_modal;
/**
* Handles the delete note modal.
*
* @since 2.0.0
* @return void
*/
public function handle_delete_note_modal() {
$model = wu_request('model');
$function_name = "wu_get_{$model}";
$object = $function_name(wu_request('object_id'));
$note_id = wu_request('note_id');
if (!$object) {
return;
} // end if;
$status = $object->delete_note($note_id);
if (is_wp_error($status) || $status === false) {
wp_send_json_error(new \WP_Error('not-found', __('Note not found', 'wp-ultimo')));
} // end if;
wp_send_json_success(array(
'redirect_url' => wu_network_admin_url("wp-ultimo-edit-{$model}", array(
'id' => $object->get_id(),
'deleted' => 1,
'options' => 'notes',
)),
));
} // end handle_delete_note_modal;
} // end class Notes_Manager;

View File

@ -0,0 +1,146 @@
<?php
/**
* Notification Manager
*
* Handles processes related to notifications.
*
* @package WP_Ultimo
* @subpackage Managers/Notification_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to notifications.
*
* @since 2.0.0
*/
class Notification_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* List of callbacks to keep, for backwards compatibility purposes.
*
* @since 2.2.0
* @var array
*/
protected $backwards_compatibility_list;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
add_action('wp_ultimo_load', array($this, 'add_settings'));
if (is_admin() && !is_network_admin()) {
add_action('admin_init', array($this, 'hide_notifications_subsites'));
} // end if;
} // end init;
/**
* Hide notifications on subsites if settings was enabled.
*
* @since 2.0.0
* @return void
*/
public function hide_notifications_subsites() {
if (!wu_get_setting('hide_notifications_subsites')) {
return;
} // end if;
global $wp_filter;
/*
* List of callbacks to keep, for backwards compatibility purposes.
*/
$this->backwards_compatibility_list = apply_filters('wu_hide_notifications_exclude_list', array(
'inject_admin_head_ads',
));
$cleaner = array($this, 'clear_callback_list');
if (wu_get_isset($wp_filter, 'admin_notices')) {
$wp_filter['admin_notices']->callbacks = array_filter($wp_filter['admin_notices']->callbacks, $cleaner === null ? fn($v, $k): bool => !empty($v) : $cleaner, $cleaner === null ? ARRAY_FILTER_USE_BOTH : 0);
} // end if;
if (wu_get_isset($wp_filter, 'all_admin_notices')) {
$wp_filter['all_admin_notices']->callbacks = array_filter($wp_filter['all_admin_notices']->callbacks, $cleaner === null ? fn($v, $k): bool => !empty($v) : $cleaner, $cleaner === null ? ARRAY_FILTER_USE_BOTH : 0);
} // end if;
} // end hide_notifications_subsites;
/**
* Keeps the allowed callbacks.
*
* @since 2.0.0
*
* @param array $callbacks The callbacks attached.
* @return array
*/
public function clear_callback_list($callbacks): bool {
if (empty($this->backwards_compatibility_list)) {
return false;
} // end if;
$keys = array_keys($callbacks);
foreach ($keys as $key) {
foreach ($this->backwards_compatibility_list as $key_to_keep) {
if (strpos($key, (string) $key_to_keep) !== false) {
return true;
} // end if;
} // end foreach;
} // end foreach;
return false;
} // end clear_callback_list;
/**
* Filter the WP Ultimo settings to add Notifications Options
*
* @since 2.0.0
*
* @return void
*/
public function add_settings() {
wu_register_settings_field('sites', 'hide_notifications_subsites', array(
'title' => __('Hide Admin Notices on Sites', 'wp-ultimo'),
'desc' => __('Hide all admin notices on network sites, except for WP Ultimo broadcasts.', 'wp-ultimo'),
'type' => 'toggle',
'default' => 0,
'order' => 25,
));
} // end add_settings;
} // end class Notification_Manager;

View File

@ -0,0 +1,483 @@
<?php
/**
* Payment Manager
*
* Handles processes related to payments.
*
* @package WP_Ultimo
* @subpackage Managers/Payment_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Models\Payment;
use \WP_Ultimo\Logger;
use \WP_Ultimo\Invoices\Invoice;
use \WP_Ultimo\Checkout\Cart;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to payments.
*
* @since 2.0.0
*/
class Payment_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'payment';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Payment';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
$this->register_forms();
Event_Manager::register_model_events('payment', __('Payment', 'wp-ultimo'), array('created', 'updated'));
add_action('wp_login', array($this, 'check_pending_payments'), 10);
add_action('wp_enqueue_scripts', array($this, 'show_pending_payments'), 10);
add_action('admin_enqueue_scripts', array($this, 'show_pending_payments'), 10);
add_action('init', array($this, 'invoice_viewer'));
add_action('wu_async_transfer_payment', array($this, 'async_transfer_payment'), 10, 2);
add_action('wu_async_delete_payment', array($this, 'async_delete_payment'), 10);
add_action('wu_gateway_payment_processed', array($this, 'handle_payment_success'), 10, 3);
add_action('wu_transition_payment_status', array($this, 'transition_payment_status'), 10, 3);
} // end init;
/**
* Triggers the do_event of the payment successful.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Models\Payment $payment The payment.
* @param \WP_Ultimo\Models\Membership $membership The membership.
* @param \WP_Ultimo\Gateways\Base_Gateway $gateway The gateway.
* @return void
*/
public function handle_payment_success($payment, $membership, $gateway) {
$payload = array_merge(
wu_generate_event_payload('payment', $payment),
wu_generate_event_payload('membership', $membership),
wu_generate_event_payload('customer', $membership->get_customer())
);
wu_do_event('payment_received', $payload);
} // end handle_payment_success;
/**
* Check if current customer haves pending payments
*
* @param \WP_User|string $user The WordPress user instance or user login.
* @return void
*/
public function check_pending_payments($user) {
if (!is_main_site()) {
return;
} // end if;
if (!is_a($user, '\WP_User')) {
$user = get_user_by('login', $user);
} // end if;
if (!$user) {
return;
} // end if;
$customer = wu_get_customer_by_user_id($user->ID);
if (!$customer) {
return;
} // end if;
foreach ($customer->get_memberships() as $membership) {
$pending_payment = $membership->get_last_pending_payment();
if ($pending_payment) {
add_user_meta($user->ID, 'wu_show_pending_payment_popup', true, true);
break;
} // end if;
} // end foreach;
} // end check_pending_payments;
/**
* Add and trigger a popup in screen with the pending payments
*
* @return void
*/
public function show_pending_payments() {
if (!is_user_logged_in()) {
return;
} // end if;
$user_id = get_current_user_id();
$show_pending_payment = get_user_meta($user_id, 'wu_show_pending_payment_popup', true);
if (!$show_pending_payment) {
return;
} // end if;
wp_enqueue_style('dashicons');
wp_enqueue_style('wu-admin');
add_wubox();
$form_title = __('Pending Payments', 'wp-ultimo');
$form_url = wu_get_form_url('pending_payments');
wp_add_inline_script( 'wubox', "document.addEventListener('DOMContentLoaded', function(){wubox.show('$form_title', '$form_url');});" );
// Show only after user login
delete_user_meta($user_id, 'wu_show_pending_payment_popup');
} // end show_pending_payments;
/**
* Register the form showing the pending payments of current customer
*
* @return void
*/
public function register_forms() {
if (function_exists('wu_register_form')) {
wu_register_form('pending_payments', array(
'render' => array($this, 'render_pending_payments'),
'capability' => 'exist',
));
} // end if;
} // end register_forms;
/**
* Add customerr pending payments
*
* @return void
*/
public function render_pending_payments() {
if (!is_user_logged_in()) {
return;
} // end if;
$user_id = get_current_user_id();
$customer = wu_get_customer_by_user_id($user_id);
if (!$customer) {
return;
} // end if;
$pending_payments = array();
foreach ($customer->get_memberships() as $membership) {
$pending_payment = $membership->get_last_pending_payment();
if ($pending_payment) {
$pending_payments[] = $pending_payment;
} // end if;
} // end foreach;
$message = !empty($pending_payments) ? __('You have pending payments on your account!', 'wp-ultimo') : __('You do not have pending payments on your account!', 'wp-ultimo');
/**
* Allow user to change the message about the pending payments.
*
* @since 2.0.19
*
* @param string $message The message to print.
* @param \WP_Ultimo\Models\Customer $customer The current customer.
* @param array $pending_payments A list with pending payments.
*/
$message = apply_filters('wu_pending_payment_message', $message, $customer, $pending_payments);
$fields = array(
'alert_text' => array(
'type' => 'note',
'desc' => $message,
'classes' => '',
'wrapper_classes' => '',
),
);
foreach ($pending_payments as $payment) {
$slug = $payment->get_hash();
$url = $payment->get_payment_url();
$html = sprintf('<a href="%s" class="button-primary">%s</a>', $url, __('Pay Now', 'wp-ultimo'));
$title = $slug;
$fields[] = array(
'type' => 'note',
'title' => $title,
'desc' => $html,
);
} // end foreach;
$form = new \WP_Ultimo\UI\Form('pending-payments', $fields, array(
'views' => 'admin-pages/fields',
'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
));
$form->render();
} // end render_pending_payments;
/**
* Adds an init endpoint to render the invoices.
*
* @todo rewrite this to use rewrite rules.
* @since 2.0.0
* @return void
*/
public function invoice_viewer() {
if (wu_request('action') === 'invoice' && wu_request('reference') && wu_request('key')) {
/*
* Validates nonce.
*/
if (!wp_verify_nonce(wu_request('key'), 'see_invoice')) {
// wp_die(__('You do not have permissions to access this file.', 'wp-ultimo'));
} // end if;
$payment = wu_get_payment_by_hash(wu_request('reference'));
if (!$payment) {
wp_die(__('This invoice does not exist.', 'wp-ultimo'));
} // end if;
$invoice = new Invoice($payment);
/*
* Displays the PDF on the screen.
*/
$invoice->print_file();
exit;
} // end if;
} // end invoice_viewer;
/**
* Transfer a payment from a user to another.
*
* @since 2.0.0
*
* @param int $payment_id The ID of the payment being transferred.
* @param int $target_customer_id The new owner.
* @return mixed
*/
public function async_transfer_payment($payment_id, $target_customer_id) {
global $wpdb;
$payment = wu_get_payment($payment_id);
$target_customer = wu_get_customer($target_customer_id);
if (!$payment || !$target_customer || $payment->get_customer_id() === $target_customer->get_id()) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$wpdb->query('START TRANSACTION');
try {
/**
* Change the payment
*/
$payment->set_customer_id($target_customer_id);
$saved = $payment->save();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return new \WP_Error('exception', $e->getMessage());
} // end try;
$wpdb->query('COMMIT');
return true;
} // end async_transfer_payment;
/**
* Delete a payment.
*
* @since 2.0.0
*
* @param int $payment_id The ID of the payment being deleted.
* @return mixed
*/
public function async_delete_payment($payment_id) {
global $wpdb;
$payment = wu_get_payment($payment_id);
if (!$payment) {
return new \WP_Error('error', __('An unexpected error happened.', 'wp-ultimo'));
} // end if;
$wpdb->query('START TRANSACTION');
try {
/**
* Change the payment
*/
$saved = $payment->delete();
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK');
return $saved;
} // end if;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return new \WP_Error('exception', $e->getMessage());
} // end try;
$wpdb->query('COMMIT');
return true;
} // end async_delete_payment;
/**
* Watches the change in payment status to take action when needed.
*
* @since 2.0.0
*
* @param string $old_status The old status of the payment.
* @param string $new_status The new status of the payment.
* @param integer $payment_id Payment ID.
* @return void
*/
public function transition_payment_status($old_status, $new_status, $payment_id) {
$completable_statuses = array(
'completed',
);
if (!in_array($new_status, $completable_statuses, true)) {
return;
} // end if;
$payment = wu_get_payment($payment_id);
if (!$payment || $payment->get_saved_invoice_number()) {
return;
} // end if;
$current_invoice_number = absint(wu_get_setting('next_invoice_number', 1));
$payment->set_invoice_number($current_invoice_number);
$payment->save();
return wu_save_setting('next_invoice_number', $current_invoice_number + 1);
} // end transition_payment_status;
} // end class Payment_Manager;

View File

@ -0,0 +1,59 @@
<?php
/**
* Product Manager
*
* Handles processes related to products.
*
* @package WP_Ultimo
* @subpackage Managers/Product_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Logger;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to products.
*
* @since 2.0.0
*/
class Product_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'product';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Product';
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
} // end init;
} // end class Product_Manager;

View File

@ -0,0 +1,198 @@
<?php
/**
* Signup Fields Manager
*
* Keeps track of the registered signup field types.
*
* @package WP_Ultimo
* @subpackage Managers/Signup_Fields
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Logger;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Keeps track of the registered signup field types.
*
* @since 2.0.0
*/
class Signup_Fields_Manager extends Base_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Keeps the instantiated fields.
*
* @since 2.0.0
* @var array
*/
protected $instantiated_field_types;
/**
* Returns the list of registered signup field types.
*
* Developers looking for add new types of fields to the signup
* should use the filter wu_checkout_forms_field_types to do so.
*
* @see wu_checkout_forms_field_types
*
* @since 2.0.0
* @return array
*/
public function get_field_types() {
$field_types = array(
'pricing_table' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Pricing_Table',
'period_selection' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Period_Selection',
'products' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Products',
'template_selection' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Template_Selection',
'username' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Username',
'email' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Email',
'password' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Password',
'site_title' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Site_Title',
'site_url' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Site_Url',
'discount_code' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Discount_Code',
'order_summary' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Order_Summary',
'payment' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Payment',
'order_bump' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Order_Bump',
'billing_address' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Billing_Address',
'steps' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Steps',
'text' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Text',
'checkbox' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Checkbox',
'color_picker' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Color',
'select' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Select',
'hidden' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Hidden',
'shortcode' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Shortcode',
'terms_of_use' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Terms_Of_Use',
'submit_button' => '\\WP_Ultimo\\Checkout\\Signup_Fields\\Signup_Field_Submit_Button',
);
/*
* Allow developers to add new field types
*/
do_action('wu_register_field_types');
/**
* Our APIs to add new field types hook into here.
* Do not use this filter directly. Use the wu_register_field_type()
* function instead.
*
* @see wu_register_field_type()
*
* @since 2.0.0
* @param array $field_types
* @return array
*/
return apply_filters('wu_checkout_field_types', $field_types);
} // end get_field_types;
/**
* Instantiate a field type.
*
* @since 2.0.0
*
* @param string $class_name The class name.
* @return \WP_Ultimo\Checkout\Signup_Fields\Base_Signup_Field
*/
public function instantiate_field_type($class_name) {
return new $class_name();
} // end instantiate_field_type;
/**
* Returns an array with all fields, instantiated.
*
* @since 2.0.0
* @return array
*/
public function get_instantiated_field_types() {
if ($this->instantiated_field_types === null) {
$this->instantiated_field_types = array_map(array($this, 'instantiate_field_type'), $this->get_field_types());
} // end if;
return $this->instantiated_field_types;
} // end get_instantiated_field_types;
/**
* Returns a list of all the required fields that must be present on a CF.
*
* @since 2.0.0
* @return array
*/
public function get_required_fields() {
$fields = $this->get_instantiated_field_types();
$fields = array_filter($fields, fn($item) => $item->is_required());
return $fields;
} // end get_required_fields;
/**
* Returns a list of all the user fields.
*
* @since 2.0.0
* @return array
*/
public function get_user_fields() {
$fields = $this->get_instantiated_field_types();
$fields = array_filter($fields, fn($item) => $item->is_user_field());
return $fields;
} // end get_user_fields;
/**
* Returns a list of all the site fields.
*
* @since 2.0.0
* @return array
*/
public function get_site_fields() {
$fields = $this->get_instantiated_field_types();
$fields = array_filter($fields, fn($item) => $item->is_site_field());
return $fields;
} // end get_site_fields;
/**
* Returns a list of all editor fields registered.
*
* @since 2.0.0
* @return array
*/
public function get_all_editor_fields() {
$all_editor_fields = array();
$field_types = $this->get_instantiated_field_types();
foreach ($field_types as $field_type) {
$all_editor_fields = array_merge($all_editor_fields, $field_class->get_fields());
} // end foreach;
return $all_editor_fields;
} // end get_all_editor_fields;
} // end class Signup_Fields_Manager;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
<?php
/**
* Visits Manager
*
* Handles processes related to site visits control.
*
* @package WP_Ultimo
* @subpackage Managers/Visits_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to limitations.
*
* @since 2.0.0
*/
class Visits_Manager {
use \WP_Ultimo\Traits\Singleton;
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
if ((bool) wu_get_setting('enable_visits_limiting', true) === false || is_main_site()) {
return; // Feature not active, bail.
} // end if;
/*
* Due to how caching plugins work, we need to count visits via ajax.
* This adds the ajax endpoint that performs the counting.
*/
add_action('wp_ajax_nopriv_wu_count_visits', array($this, 'count_visits'), 10, 2);
add_action('wp_enqueue_scripts', array($this, 'enqueue_visit_counter_script'));
add_action('template_redirect', array($this, 'maybe_lock_site'));
} // end init;
/**
* Check if the limits for visits was set. If that's the case, lock the site.
*
* @since 2.0.0
* @return void
*/
public function maybe_lock_site() {
$site = wu_get_current_site();
if (!$site) {
return;
} // end if;
/*
* Case unlimited visits
*/
if (empty($site->get_limitations()->visits->get_limit())) {
return;
} // end if;
if ($site->has_limitations() && $site->get_visits_count() > $site->get_limitations()->visits->get_limit()) {
wp_die(__('This site is not available at this time.', 'wp-ultimo'), __('Not available', 'wp-ultimo'), 404);
} // end if;
} // end maybe_lock_site;
/**
* Counts visits to network sites.
*
* This needs to be extremely light-weight.
* The flow happens more or less like this:
* 1. Gets the site current total;
* 2. Adds one and re-save;
* 3. Checks limits and see if we need to flush caches and such;
* 4. Delegate these heavy tasks to action_scheduler.
*
* @since 2.0.0
* @return void
*/
public function count_visits() {
if (is_main_site() && is_admin()) {
return; // bail on main site.
} // end if;
$site = wu_get_current_site();
if ($site->get_type() !== 'customer_owned') {
return;
} // end if;
$visits_manager = new \WP_Ultimo\Objects\Visits($site->get_id());
/*
* Add a new visit.
*/
$visits_manager->add_visit();
/*
* Checks against the limitations.
*/
if (false) {
Cache_Manager::get_instance()->flush_known_caches();
echo 'flushing caches';
die('2');
} // end if;
die('1');
} // end count_visits;
/**
* Enqueues the visits count script when necessary.
*
* @since 2.0.0
* @return void
*/
public function enqueue_visit_counter_script() {
if (is_user_logged_in()) {
return; // bail if user is logged in.
} // end if;
wp_register_script('wu-visits-counter', wu_get_asset('visits-counter.js', 'js'), array(), wu_get_version());
wp_localize_script('wu-visits-counter', 'wu_visits_counter', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'code' => wp_create_nonce('wu-visit-counter'),
));
wp_enqueue_script('wu-visits-counter');
} // end enqueue_visit_counter_script;
} // end class Visits_Manager;

View File

@ -0,0 +1,334 @@
<?php
/**
* Webhook Manager
*
* Handles processes related to Webhooks.
*
* @package WP_Ultimo
* @subpackage Managers/Webhook_Manager
* @since 2.0.0
*/
namespace WP_Ultimo\Managers;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Models\Webhook;
use WP_Ultimo\Logger;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Handles processes related to webhooks.
*
* @since 2.0.0
*/
class Webhook_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api, \WP_Ultimo\Apis\WP_CLI, \WP_Ultimo\Traits\Singleton;
/**
* The manager slug.
*
* @since 2.0.0
* @var string
*/
protected $slug = 'webhook';
/**
* The model class associated to this manager.
*
* @since 2.0.0
* @var string
*/
protected $model_class = '\\WP_Ultimo\\Models\\Webhook';
/**
* Holds the list of available events for webhooks.
*
* @since 2.0.0
* @var array
*/
protected $events = array();
/**
* Holds the list of all webhooks.
*
* @since 2.0.0
* @var array
*/
protected $webhooks = array();
/**
* Instantiate the necessary hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
$this->enable_rest_api();
$this->enable_wp_cli();
add_action('init', array($this, 'register_webhook_listeners'));
add_action('wp_ajax_wu_send_test_event', array($this, 'send_test_event'));
} // end init;
/**
* Adds the listeners to the webhook callers, extend this by adding actions to wu_register_webhook_listeners
*
* @todo This needs to have a switch, allowing us to turn it on and off.
* @return void.
*/
public function register_webhook_listeners() {
foreach (wu_get_event_types() as $key => $event) {
add_action('wu_event_' . $key, array($this, 'send_webhooks'));
} // end foreach;
} // end register_webhook_listeners;
/**
* Sends all the webhooks that are triggered by a specific event.
*
* @since 2.0.0
*
* @param array $args with events slug and payload.
* @return void
*/
public function send_webhooks($args) {
$webhooks = Webhook::get_all();
foreach ($webhooks as $webhook) {
if ('wu_event_' . $webhook->get_event() === current_filter()) {
$blocking = wu_get_setting('webhook_calls_blocking', false);
$this->send_webhook($webhook, $args, $blocking);
} // end if;
} // end foreach;
} // end send_webhooks;
/**
* Sends a specific webhook.
*
* @since 2.0.0
*
* @param Webhook $webhook The webhook to send.
* @param array $data Key-value array of data to send.
* @param boolean $blocking Decides if we want to wait for a response to keep a log.
* @param boolean $count If we should update the webhook event count.
* @return string|null.
*/
public function send_webhook($webhook, $data, $blocking = true, $count = true) {
if (!$data) {
return;
} // end if;
$request = wp_remote_post($webhook->get_webhook_url(), array(
'method' => 'POST',
'timeout' => 45,
'redirection' => 5,
'headers' => array(
'Content-Type' => 'application/json',
),
'cookies' => array(),
'body' => wp_json_encode($data),
'blocking' => $blocking,
), current_filter(), $webhook);
if (is_wp_error($request)) {
$error_message = $request->get_error_message();
if ($count) {
$this->create_event(
$webhook->get_event(),
$webhook->get_id(),
$webhook->get_webhook_url(),
$data,
$error_message,
true
);
} // end if;
return $error_message;
} // end if;
$response = '';
// if blocking, we have a response
if ($blocking) {
$response = wp_remote_retrieve_body($request);
} // end if;
if ($count) {
$this->create_event(
$webhook->get_event(),
$webhook->get_id(),
$webhook->get_webhook_url(),
$data,
$response
);
$new_count = $webhook->get_event_count() + 1;
$webhook->set_event_count($new_count);
$webhook->save();
} // end if;
return $response;
} // end send_webhook;
/**
* Send a test event of the webhook
*
* @return void
*/
public function send_test_event() {
if (!current_user_can('manage_network')) {
wp_send_json(array(
'response' => __('You do not have enough permissions to send a test event.', 'wp-ultimo'),
'webhooks' => Webhook::get_items_as_array(),
));
} // end if;
$event = wu_get_event_type($_POST['webhook_event']);
$webhook_data = array(
'webhook_url' => $_POST['webhook_url'],
'event' => $_POST['webhook_event'],
'active' => true,
);
$webhook = new Webhook($webhook_data);
$response = $this->send_webhook($webhook, wu_maybe_lazy_load_payload($event['payload']), true, false);
wp_send_json(array(
'response' => htmlentities2($response),
'id' => wu_request('webhook_id')
));
} // end send_test_event;
/**
* Reads the log file and displays the content.
*
* @return void.
*/
public function serve_logs() {
echo '<style>
body {
font-family: monospace;
line-height: 20px;
}
pre {
background: #ececec;
border: solid 1px #ccc;
padding: 10px;
border-radius: 3px;
}
hr {
margin: 25px 0;
border-top: 1px solid #cecece;
border-bottom: transparent;
}
</style>
';
if (!current_user_can('manage_network')) {
echo __('You do not have enough permissions to read the logs of this webhook.', 'wp-ultimo');
exit;
} // end if;
$id = absint($_REQUEST['id']);
$logs = array_map(function($line): string {
$line = str_replace(' - ', ' </strong> - ', $line);
$matches = array();
$line = str_replace('\'', '\\\'', $line);
$line = preg_replace('~(\{(?:[^{}]|(?R))*\})~', '<pre><script>document.write(JSON.stringify(JSON.parse(\'${1}\'), null, 2));</script></pre>', $line);
return '<strong>' . $line . '<hr>';
}, Logger::read_lines("webhook-$id", 5));
echo implode('', $logs);
exit;
} // end serve_logs;
/**
* Log a webhook sent for later reference.
*
* @since 2.0.0
*
* @param string $event_name The name of the event.
* @param int $id The id of the webhook sent.
* @param string $url The URL called by the webhook.
* @param array $data The array with data to be sent.
* @param string $response The response got on webhook call.
* @param bool $is_error If the response is a WP_Error message.
* @return void
*/
protected function create_event($event_name, $id, $url, $data, $response, $is_error = false) {
$message = sprintf('Sent a %s event to the URL %s with data: %s ', $event_name, $url, json_encode($data));
if (!$is_error) {
$message .= empty($response) ? sprintf('Got response: %s', $response) : 'To debug the remote server response, turn the "Wait for Response" option on the WP Ultimo Settings > API & Webhooks Tab';
} else {
$message .= sprintf('Got error: %s', $response);
} // end if;
$event_data = array(
'object_id' => $id,
'object_type' => $this->slug,
'slug' => $event_name,
'payload' => array(
'message' => $message,
),
);
wu_create_event($event_data);
} // end create_event;
} // end class Webhook_Manager;