Files
wp-multisite-waas/inc/api/class-register-endpoint.php
David Stone a815fdf179 Prep Plugin for release on WordPress.org
Escape everything that should be escaped.
Add nonce checks where needed.
Sanitize all inputs.
Apply Code style changes across the codebase.
Correct many deprecation notices.
Optimize load order of many filters.
2025-04-07 09:15:21 -06:00

717 lines
20 KiB
PHP

<?php
/**
* The Register API endpoint.
*
* @package WP_Ultimo
* @subpackage API
* @since 2.0.0
*/
namespace WP_Ultimo\API;
use WP_Ultimo\Checkout\Cart;
use WP_Ultimo\Database\Sites\Site_Type;
use WP_Ultimo\Database\Payments\Payment_Status;
use WP_Ultimo\Database\Memberships\Membership_Status;
use WP_Ultimo\Objects\Billing_Address;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* The Register API endpoint.
*
* @since 2.0.0
*/
class Register_Endpoint {
use \WP_Ultimo\Traits\Singleton;
/**
* Loads the initial register route hooks.
*
* @since 2.0.0
* @return void
*/
public function init(): void {
add_action('wu_register_rest_routes', [$this, 'register_route']);
}
/**
* Adds a new route to the wu namespace, for the register endpoint.
*
* @since 2.0.0
*
* @param \WP_Ultimo\API $api The API main singleton.
* @return void
*/
public function register_route($api): void {
$namespace = $api->get_namespace();
register_rest_route(
$namespace,
'/register',
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'handle_get'],
'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']),
]
);
register_rest_route(
$namespace,
'/register',
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [$this, 'handle_endpoint'],
'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']),
'args' => $this->get_rest_args(),
]
);
}
/**
* Handle the register endpoint get for zapier integration reasons.
*
* @since 2.0.0
*
* @param \WP_REST_Request $request WP Request Object.
* @return array
*/
public function handle_get($request) {
return [
'registration_status' => wu_get_setting('enable_registration', true) ? 'open' : 'closed',
];
}
/**
* Handle the register endpoint logic.
*
* @since 2.0.0
*
* @param \WP_REST_Request $request WP Request Object.
* @return array|\WP_Error
*/
public function handle_endpoint($request) {
global $wpdb;
$params = json_decode($request->get_body(), true);
if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
wu_log_add('api-calls', wp_json_encode($params, JSON_PRETTY_PRINT));
}
$validation_errors = $this->validate($params);
if (is_wp_error($validation_errors)) {
$validation_errors->add_data(
[
'status' => 400,
]
);
return $validation_errors;
}
$wpdb->query('START TRANSACTION');
try {
$customer = $this->maybe_create_customer($params);
if (is_wp_error($customer)) {
return $this->rollback_and_return($customer);
}
$customer->update_last_login(true, true);
$customer->add_note(
[
'text' => __('Created via REST API', 'wp-multisite-waas'),
'author_id' => $customer->get_user_id(),
]
);
/*
* Payment Method defaults
*/
$payment_method = wp_parse_args(
wu_get_isset($params, 'payment_method'),
[
'gateway' => '',
'gateway_customer_id' => '',
'gateway_subscription_id' => '',
'gateway_payment_id' => '',
]
);
/*
* Cart params and creation
*/
$cart_params = $params;
$cart_params = wp_parse_args(
$cart_params,
[
'type' => 'new',
]
);
$cart = new Cart($cart_params);
/*
* Validates if the cart is valid.
*/
if ($cart->is_valid() && count($cart->get_line_items()) === 0) {
return new \WP_Error(
'invalid_cart',
__('Products are required.', 'wp-multisite-waas'),
array_merge(
(array) $cart->done(),
[
'status' => 400,
]
)
);
}
/*
* Get Membership data
*/
$membership_data = $cart->to_membership_data();
$membership_data = array_merge(
$membership_data,
wu_get_isset(
$params,
'membership',
[
'status' => Membership_Status::PENDING,
]
)
);
$membership_data['customer_id'] = $customer->get_id();
$membership_data['gateway'] = wu_get_isset($payment_method, 'gateway');
$membership_data['gateway_subscription_id'] = wu_get_isset($payment_method, 'gateway_subscription_id');
$membership_data['gateway_customer_id'] = wu_get_isset($payment_method, 'gateway_customer_id');
$membership_data['auto_renew'] = wu_get_isset($params, 'auto_renew');
/*
* Unset the status because we are going to transition it later.
*/
$membership_status = $membership_data['status'];
unset($membership_data['status']);
$membership = wu_create_membership($membership_data);
if (is_wp_error($membership)) {
return $this->rollback_and_return($membership);
}
$membership->add_note(
[
'text' => __('Created via REST API', 'wp-multisite-waas'),
'author_id' => $customer->get_user_id(),
]
);
$payment_data = $cart->to_payment_data();
$payment_data = array_merge(
$payment_data,
wu_get_isset(
$params,
'payment',
[
'status' => Payment_Status::PENDING,
]
)
);
/*
* Unset the status because we are going to transition it later.
*/
$payment_status = $payment_data['status'];
unset($payment_data['status']);
$payment_data['customer_id'] = $customer->get_id();
$payment_data['membership_id'] = $membership->get_id();
$payment_data['gateway'] = wu_get_isset($payment_method, 'gateway');
$payment_data['gateway_payment_id'] = wu_get_isset($payment_method, 'gateway_payment_id');
$payment = wu_create_payment($payment_data);
if (is_wp_error($payment)) {
return $this->rollback_and_return($payment);
}
$payment->add_note(
[
'text' => __('Created via REST API', 'wp-multisite-waas'),
'author_id' => $customer->get_user_id(),
]
);
$site = false;
/*
* Site creation.
*/
if (wu_get_isset($params, 'site')) {
$site = $this->maybe_create_site($params, $membership);
if (is_wp_error($site)) {
return $this->rollback_and_return($site);
}
}
/*
* Deal with status changes.
*/
if ($membership->get_status() !== $membership_status) {
$membership->set_status($membership_status);
$membership->save();
/*
* The above change might trigger a site publication
* to take place, so we need to try to fetch the site
* again, this time as a WU Site object.
*/
if ($site) {
$wp_site = get_site_by_path($site['domain'], $site['path']);
if ($wp_site) {
$site['id'] = $wp_site->blog_id;
}
}
}
if ($payment->get_status() !== $payment_status) {
$payment->set_status($payment_status);
$payment->save();
}
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return new \WP_Error('registration_error', $e->getMessage(), ['status' => 500]);
}
$wpdb->query('COMMIT');
/*
* We have everything we need now.
*/
return [
'membership' => $membership->to_array(),
'customer' => $customer->to_array(),
'payment' => $payment->to_array(),
'site' => $site ?: ['id' => 0],
];
}
/**
* Returns the list of arguments allowed on to the endpoint.
*
* This is also used to build the documentation page for the endpoint.
*
* @since 2.0.0
* @return array
*/
public function get_rest_args() {
/*
* Billing Address Fields
*/
$billing_address_fields = Billing_Address::fields_for_rest(false);
$customer_args = [
'customer_id' => [
'description' => __('The customer ID, if the customer already exists. If you also need to create a customer/wp user, use the "customer" property.', 'wp-multisite-waas'),
'type' => 'integer',
],
'customer' => [
'description' => __('Customer data. Needs to be present when customer id is not.', 'wp-multisite-waas'),
'type' => 'object',
'properties' => [
'user_id' => [
'description' => __('Existing WordPress user id to attach this customer to. If you also need to create a WordPress user, pass the properties "username", "password", and "email".', 'wp-multisite-waas'),
'type' => 'integer',
],
'username' => [
'description' => __('The customer username. This is used to create the WordPress user.', 'wp-multisite-waas'),
'type' => 'string',
'minLength' => 4,
],
'password' => [
'description' => __('The customer password. This is used to create the WordPress user. Note that no validation is performed here to enforce strength.', 'wp-multisite-waas'),
'type' => 'string',
'minLength' => 6,
],
'email' => [
'description' => __('The customer email address. This is used to create the WordPress user.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'email',
],
'billing_address' => [
'type' => 'object',
'properties' => $billing_address_fields,
],
],
],
];
$membership_args = [
'membership' => [
'description' => __('The membership data is automatically generated based on the cart info passed (e.g. products) but can be overridden with this property.', 'wp-multisite-waas'),
'type' => 'object',
'properties' => [
'status' => [
'description' => __('The membership status.', 'wp-multisite-waas'),
'type' => 'string',
'enum' => array_values(Membership_Status::get_allowed_list()),
'default' => Membership_Status::PENDING,
],
'date_expiration' => [
'description' => __('The membership expiration date. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
'date_trial_end' => [
'description' => __('The membership trial end date. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
'date_activated' => [
'description' => __('The membership activation date. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
'date_renewed' => [
'description' => __('The membership last renewed date. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
'date_cancellation' => [
'description' => __('The membership cancellation date. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
'date_payment_plan_completed' => [
'description' => __('The membership completion date. Used when the membership is limited to a limited number of billing cycles. Must be a valid PHP date format.', 'wp-multisite-waas'),
'type' => 'string',
'format' => 'date-time',
],
],
],
];
$payment_args = [
'payment' => [
'description' => __('The payment data is automatically generated based on the cart info passed (e.g. products) but can be overridden with this property.', 'wp-multisite-waas'),
'type' => 'object',
'properties' => [
'status' => [
'description' => __('The payment status.', 'wp-multisite-waas'),
'type' => 'string',
'enum' => array_values(Payment_Status::get_allowed_list()),
'default' => Payment_Status::PENDING,
],
],
],
'payment_method' => [
'description' => __('Payment method information. Useful when using the REST API to integrate other payment methods.', 'wp-multisite-waas'),
'type' => 'object',
'properties' => [
'gateway' => [
'description' => __('The gateway name. E.g. stripe.', 'wp-multisite-waas'),
'type' => 'string',
],
'gateway_customer_id' => [
'description' => __('The customer ID on the gateway system.', 'wp-multisite-waas'),
'type' => 'string',
],
'gateway_subscription_id' => [
'description' => __('The subscription ID on the gateway system.', 'wp-multisite-waas'),
'type' => 'string',
],
'gateway_payment_id' => [
'description' => __('The payment ID on the gateway system.', 'wp-multisite-waas'),
'type' => 'string',
],
],
],
];
$site_args = [
'site' => [
'type' => 'object',
'properties' => [
'site_url' => [
'type' => 'string',
'description' => __('The site subdomain or subdirectory (depending on your Multisite install). This would be "test" in "test.your-network.com".', 'wp-multisite-waas'),
'minLength' => 4,
'required' => true,
],
'site_title' => [
'type' => 'string',
'description' => __('The site title. E.g. My Amazing Site', 'wp-multisite-waas'),
'minLength' => 4,
'required' => true,
],
'publish' => [
'description' => __('If we should publish this site regardless of membership/payment status. Sites are created as pending by default, and are only published when a payment is received or the status of the membership changes to "active". This flag allows you to bypass the pending state.', 'wp-multisite-waas'),
'type' => 'boolean',
'default' => false,
],
'template_id' => [
'description' => __('The template ID we should copy when creating this site. If left empty, the value dictated by the products will be used.', 'wp-multisite-waas'),
'type' => 'integer',
],
'site_meta' => [
'description' => __('An associative array of key values to be saved as site_meta.', 'wp-multisite-waas'),
'type' => 'object',
],
'site_option' => [
'description' => __('An associative array of key values to be saved as site_options. Useful for changing plugin settings and other site configurations.', 'wp-multisite-waas'),
'type' => 'object',
],
],
],
];
$cart_args = [
'products' => [
'description' => __('The products to be added to this membership. Takes an array of product ids or slugs.', 'wp-multisite-waas'),
'uniqueItems' => true,
'type' => 'array',
],
'duration' => [
'description' => __('The membership duration.', 'wp-multisite-waas'),
'type' => 'integer',
'required' => false,
],
'duration_unit' => [
'description' => __('The membership duration unit.', 'wp-multisite-waas'),
'type' => 'string',
'default' => 'month',
'enum' => [
'day',
'week',
'month',
'year',
],
],
'discount_code' => [
'description' => __('A discount code. E.g. PROMO10.', 'wp-multisite-waas'),
'type' => 'string',
],
'auto_renew' => [
'description' => __('The membership auto-renew status. Useful when integrating with other payment options via this REST API.', 'wp-multisite-waas'),
'type' => 'boolean',
'default' => false,
'required' => true,
],
'country' => [
'description' => __('The customer country. Used to calculate taxes and check if registration is allowed for that country.', 'wp-multisite-waas'),
'type' => 'string',
'default' => '',
],
'currency' => [
'description' => __('The currency to be used.', 'wp-multisite-waas'),
'type' => 'string',
],
];
$args = array_merge($customer_args, $membership_args, $cart_args, $payment_args, $site_args);
return apply_filters('wu_rest_register_endpoint_args', $args, $this);
}
/**
* Maybe create a customer, if needed.
*
* @since 2.0.0
*
* @param array $p The request parameters.
* @return \WP_Ultimo\Models\Customer|\WP_Error
*/
public function maybe_create_customer($p) {
$customer_id = wu_get_isset($p, 'customer_id');
if ($customer_id) {
$customer = wu_get_customer($customer_id);
if ( ! $customer) {
return new \WP_Error('customer_not_found', __('The customer id sent does not correspond to a valid customer.', 'wp-multisite-waas'));
}
} else {
$customer = wu_create_customer($p['customer']);
}
return $customer;
}
/**
* Undocumented function
*
* @since 2.0.0
*
* @param array $p The request parameters.
* @param \WP_Ultimo\Models\Membership $membership The membership created.
* @return array|\WP_Ultimo\Models\Site\|\WP_Error
*/
public function maybe_create_site($p, $membership) {
$site_data = $p['site'];
/*
* Let's get a list of membership sites.
* This list includes pending sites as well.
*/
$sites = $membership->get_sites();
/*
* Decide if we should create a new site or not.
*
* When should we create a new pending site?
* There are a couple of rules:
* - The membership must not have a pending site;
* - The membership must not have an existing site;
*
* The get_sites method already includes pending sites,
* so we can safely rely on it.
*/
if ( ! empty($sites)) {
/*
* Returns the first site on that list.
* This is not ideal, but since we'll usually only have
* one site here, it's ok. for now.
*/
return current($sites);
}
$site_url = wu_get_isset($site_data, 'site_url');
$d = wu_get_site_domain_and_path($site_url);
/*
* Validates the site url.
*/
$results = wpmu_validate_blog_signup($site_url, wu_get_isset($site_data, 'site_title'), $membership->get_customer()->get_user());
if ($results['errors']->has_errors()) {
return $results['errors'];
}
/*
* Get the transient data to save with the site
* that way we can use it when actually registering
* the site on WordPress.
*/
$transient = array_merge(
wu_get_isset($site_data, 'site_meta', []),
wu_get_isset($site_data, 'site_option', [])
);
$template_id = apply_filters('wu_checkout_template_id', (int) wu_get_isset($site_data, 'template_id'), $membership, $this);
$site_data = [
'domain' => $d->domain,
'path' => $d->path,
'title' => wu_get_isset($site_data, 'site_title'),
'template_id' => $template_id,
'customer_id' => $membership->get_customer()->get_id(),
'membership_id' => $membership->get_id(),
'transient' => $transient,
'signup_meta' => wu_get_isset($site_data, 'site_meta', []),
'signup_options' => wu_get_isset($site_data, 'site_option', []),
'type' => Site_Type::CUSTOMER_OWNED,
];
$membership->create_pending_site($site_data);
$site_data['id'] = 0;
if (wu_get_isset($site_data, 'publish')) {
$membership->publish_pending_site();
$wp_site = get_site_by_path($site_data['domain'], $site_data['path']);
if ($wp_site) {
$site_data['id'] = $wp_site->blog_id;
}
}
return $site_data;
}
/**
* Set the validation rules for this particular model.
*
* To see how to setup rules, check the documentation of the
* validation library we are using: https://github.com/rakit/validation
*
* @since 2.0.0
* @link https://github.com/rakit/validation
* @return array
*/
public function validation_rules() {
return [
'customer_id' => 'required_without:customer',
'customer' => 'required_without:customer_id',
'customer.username' => 'required_without_all:customer_id,customer.user_id',
'customer.password' => 'required_without_all:customer_id,customer.user_id',
'customer.email' => 'required_without_all:customer_id,customer.user_id',
'customer.user_id' => 'required_without_all:customer_id,customer.username,customer.password,customer.email',
'site.site_url' => 'required_with:site|alpha_num|min:4|lowercase|unique_site',
'site.site_title' => 'required_with:site|min:4',
];
}
/**
* Validates the rules and make sure we only save models when necessary.
*
* @since 2.0.0
* @param array $args The params to validate.
* @return mixed[]|\WP_Error
*/
public function validate($args) {
$validator = new \WP_Ultimo\Helpers\Validator();
$validator->validate($args, $this->validation_rules());
if ($validator->fails()) {
return $validator->get_errors();
}
return true;
}
/**
* Rolls back database changes and returns the error passed.
*
* @since 2.0.0
*
* @param \WP_Error $error The error to return.
* @return \WP_Error
*/
protected function rollback_and_return($error) {
global $wpdb;
$wpdb->query('ROLLBACK');
return $error;
}
}