diff --git a/inc/admin-pages/class-dashboard-admin-page.php b/inc/admin-pages/class-dashboard-admin-page.php
index bc7e38746..6679b6190 100644
--- a/inc/admin-pages/class-dashboard-admin-page.php
+++ b/inc/admin-pages/class-dashboard-admin-page.php
@@ -125,7 +125,7 @@ public function hooks(): void {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Admin_Pages\Base_Admin_Page $page The page object.
+ * @param \WP_Ultimo\Admin_Pages\Base_Admin_Page $page The page object.
* @return void
*/
public function render_filter($page): void {
diff --git a/inc/admin-pages/class-list-admin-page.php b/inc/admin-pages/class-list-admin-page.php
index e8ade8527..16d4e116b 100644
--- a/inc/admin-pages/class-list-admin-page.php
+++ b/inc/admin-pages/class-list-admin-page.php
@@ -47,7 +47,7 @@ abstract class List_Admin_Page extends Base_Admin_Page {
* Holds the WP_List_Table instance to be used on the list
*
* @since 1.8.2
- * @var WP_List_Table
+ * @var \WP_List_Table
*/
protected $table;
@@ -217,7 +217,7 @@ public function save_screen_option($value, $option, $other_value) {
* Dumb function. Child classes need to implement this to set the table that Ultimate Multisite will use
*
* @since 1.8.2
- * @return WP_List_Table
+ * @return \WP_List_Table
*/
public function get_table() {
diff --git a/inc/admin-pages/class-site-edit-admin-page.php b/inc/admin-pages/class-site-edit-admin-page.php
index 0d88afd8c..fae2697c0 100644
--- a/inc/admin-pages/class-site-edit-admin-page.php
+++ b/inc/admin-pages/class-site-edit-admin-page.php
@@ -646,12 +646,12 @@ public function action_links() {
'icon' => 'wu-cog',
],
[
- 'url' => get_site_url($this->get_object()->get_id()),
+ 'url' => $this->get_object()->get_active_site_url(),
'label' => __('Visit Site', 'ultimate-multisite'),
'icon' => 'wu-link',
],
[
- 'url' => get_admin_url($this->get_object()->get_id()),
+ 'url' => wu_get_admin_url($this->get_object()->get_id()),
'label' => __('Dashboard', 'ultimate-multisite'),
'icon' => 'dashboard',
],
diff --git a/inc/admin-pages/class-site-list-admin-page.php b/inc/admin-pages/class-site-list-admin-page.php
index 4069315e6..2eb7d60a4 100644
--- a/inc/admin-pages/class-site-list-admin-page.php
+++ b/inc/admin-pages/class-site-list-admin-page.php
@@ -231,12 +231,10 @@ public function handle_publish_pending_site_modal(): void {
* Handles the add/edit of line items.
*
* @since 2.0.0
- * @return mixed
+ * @return void
*/
public function handle_add_new_site_modal() {
- global $current_site;
-
$domain_type = wu_request('tab', is_subdomain_install() ? 'sub-domain' : 'sub-directory');
if ('domain' === $domain_type) {
@@ -263,13 +261,13 @@ public function handle_add_new_site_modal() {
$site = wu_create_site($atts);
if (is_wp_error($site)) {
- return wp_send_json_error($site);
+ wp_send_json_error($site);
}
- if ($site->get_blog_id() === false) {
+ if (! $site->get_blog_id()) {
$error = new \WP_Error('error', __('Something wrong happened.', 'ultimate-multisite'));
- return wp_send_json_error($error);
+ wp_send_json_error($error);
}
$redirect = current_user_can('wu_edit_sites') ? 'wp-ultimo-edit-site' : 'wp-ultimo-sites';
diff --git a/inc/api/class-register-endpoint.php b/inc/apis/class-register-endpoint.php
similarity index 99%
rename from inc/api/class-register-endpoint.php
rename to inc/apis/class-register-endpoint.php
index 085169142..fbd342eec 100644
--- a/inc/api/class-register-endpoint.php
+++ b/inc/apis/class-register-endpoint.php
@@ -566,7 +566,7 @@ public function maybe_create_customer($p) {
*
* @param array $p The request parameters.
* @param \WP_Ultimo\Models\Membership $membership The membership created.
- * @return array|\WP_Ultimo\Models\Site\|\WP_Error
+ * @return array|\WP_Error
*/
public function maybe_create_site($p, $membership) {
diff --git a/inc/api/schemas/broadcast-create.php b/inc/apis/schemas/broadcast-create.php
similarity index 100%
rename from inc/api/schemas/broadcast-create.php
rename to inc/apis/schemas/broadcast-create.php
diff --git a/inc/api/schemas/broadcast-update.php b/inc/apis/schemas/broadcast-update.php
similarity index 100%
rename from inc/api/schemas/broadcast-update.php
rename to inc/apis/schemas/broadcast-update.php
diff --git a/inc/api/schemas/checkout-form-create.php b/inc/apis/schemas/checkout-form-create.php
similarity index 100%
rename from inc/api/schemas/checkout-form-create.php
rename to inc/apis/schemas/checkout-form-create.php
diff --git a/inc/api/schemas/checkout-form-update.php b/inc/apis/schemas/checkout-form-update.php
similarity index 100%
rename from inc/api/schemas/checkout-form-update.php
rename to inc/apis/schemas/checkout-form-update.php
diff --git a/inc/api/schemas/customer-create.php b/inc/apis/schemas/customer-create.php
similarity index 100%
rename from inc/api/schemas/customer-create.php
rename to inc/apis/schemas/customer-create.php
diff --git a/inc/api/schemas/customer-update.php b/inc/apis/schemas/customer-update.php
similarity index 100%
rename from inc/api/schemas/customer-update.php
rename to inc/apis/schemas/customer-update.php
diff --git a/inc/api/schemas/discount-code-create.php b/inc/apis/schemas/discount-code-create.php
similarity index 100%
rename from inc/api/schemas/discount-code-create.php
rename to inc/apis/schemas/discount-code-create.php
diff --git a/inc/api/schemas/discount-code-update.php b/inc/apis/schemas/discount-code-update.php
similarity index 100%
rename from inc/api/schemas/discount-code-update.php
rename to inc/apis/schemas/discount-code-update.php
diff --git a/inc/api/schemas/domain-create.php b/inc/apis/schemas/domain-create.php
similarity index 100%
rename from inc/api/schemas/domain-create.php
rename to inc/apis/schemas/domain-create.php
diff --git a/inc/api/schemas/domain-update.php b/inc/apis/schemas/domain-update.php
similarity index 100%
rename from inc/api/schemas/domain-update.php
rename to inc/apis/schemas/domain-update.php
diff --git a/inc/api/schemas/email-create.php b/inc/apis/schemas/email-create.php
similarity index 100%
rename from inc/api/schemas/email-create.php
rename to inc/apis/schemas/email-create.php
diff --git a/inc/api/schemas/email-update.php b/inc/apis/schemas/email-update.php
similarity index 100%
rename from inc/api/schemas/email-update.php
rename to inc/apis/schemas/email-update.php
diff --git a/inc/api/schemas/event-create.php b/inc/apis/schemas/event-create.php
similarity index 100%
rename from inc/api/schemas/event-create.php
rename to inc/apis/schemas/event-create.php
diff --git a/inc/api/schemas/event-update.php b/inc/apis/schemas/event-update.php
similarity index 100%
rename from inc/api/schemas/event-update.php
rename to inc/apis/schemas/event-update.php
diff --git a/inc/api/schemas/membership-create.php b/inc/apis/schemas/membership-create.php
similarity index 100%
rename from inc/api/schemas/membership-create.php
rename to inc/apis/schemas/membership-create.php
diff --git a/inc/api/schemas/membership-update.php b/inc/apis/schemas/membership-update.php
similarity index 100%
rename from inc/api/schemas/membership-update.php
rename to inc/apis/schemas/membership-update.php
diff --git a/inc/api/schemas/payment-create.php b/inc/apis/schemas/payment-create.php
similarity index 100%
rename from inc/api/schemas/payment-create.php
rename to inc/apis/schemas/payment-create.php
diff --git a/inc/api/schemas/payment-update.php b/inc/apis/schemas/payment-update.php
similarity index 100%
rename from inc/api/schemas/payment-update.php
rename to inc/apis/schemas/payment-update.php
diff --git a/inc/api/schemas/product-create.php b/inc/apis/schemas/product-create.php
similarity index 100%
rename from inc/api/schemas/product-create.php
rename to inc/apis/schemas/product-create.php
diff --git a/inc/api/schemas/product-update.php b/inc/apis/schemas/product-update.php
similarity index 100%
rename from inc/api/schemas/product-update.php
rename to inc/apis/schemas/product-update.php
diff --git a/inc/api/schemas/site-create.php b/inc/apis/schemas/site-create.php
similarity index 100%
rename from inc/api/schemas/site-create.php
rename to inc/apis/schemas/site-create.php
diff --git a/inc/api/schemas/site-update.php b/inc/apis/schemas/site-update.php
similarity index 100%
rename from inc/api/schemas/site-update.php
rename to inc/apis/schemas/site-update.php
diff --git a/inc/api/schemas/webhook-create.php b/inc/apis/schemas/webhook-create.php
similarity index 100%
rename from inc/api/schemas/webhook-create.php
rename to inc/apis/schemas/webhook-create.php
diff --git a/inc/api/schemas/webhook-update.php b/inc/apis/schemas/webhook-update.php
similarity index 100%
rename from inc/api/schemas/webhook-update.php
rename to inc/apis/schemas/webhook-update.php
diff --git a/inc/apis/trait-mcp-abilities.php b/inc/apis/trait-mcp-abilities.php
new file mode 100644
index 000000000..2dfba3b2b
--- /dev/null
+++ b/inc/apis/trait-mcp-abilities.php
@@ -0,0 +1,645 @@
+slug);
+ }
+
+ /**
+ * Enable MCP abilities for this entity.
+ * Should be called by the manager to register abilities.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function enable_mcp_abilities(): void {
+
+ $is_enabled = \WP_Ultimo\MCP_Adapter::get_instance()->is_mcp_enabled();
+
+ if (! $is_enabled) {
+ return;
+ }
+
+ if (! function_exists('wp_register_ability')) {
+ return;
+ }
+
+ add_action('wp_abilities_api_categories_init', [$this, 'register_ability_category'], 10, 0);
+ add_action('wp_abilities_api_init', [$this, 'register_abilities'], 10, 0);
+ }
+
+ /**
+ * Register the ability category for this entity.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function register_ability_category(): void {
+
+ if (! function_exists('wp_register_ability_category')) {
+ return;
+ }
+
+ if (wp_has_ability_category('multisite-ultimate')) {
+ return;
+ }
+ wp_register_ability_category(
+ 'multisite-ultimate',
+ [
+ 'label' => __('Multisite Ultimate', 'ultimate-multisite'),
+ 'description' => __('CRUD operations for Multisite Ultimate entities including customers, sites, products, memberships, and more.', 'ultimate-multisite'),
+ ]
+ );
+ }
+
+ /**
+ * Permission callback for MCP abilities.
+ * Checks if the current user has the required capabilities.
+ *
+ * @since 2.5.0
+ * @param array $input_data The input data passed to the ability.
+ * @return bool|\WP_Error True if user has permission, WP_Error otherwise.
+ */
+ public function mcp_permission_callback(array $input_data) {
+ unset($input_data);
+
+ $capability = "wu_read_{$this->slug}";
+
+ if (! current_user_can($capability) && ! current_user_can('manage_network')) {
+ return new \WP_Error(
+ 'rest_forbidden',
+ __('You do not have permission to access this resource.', 'ultimate-multisite'),
+ ['status' => 403]
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Register abilities with the Abilities API.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function register_abilities(): void {
+
+ $ability_prefix = $this->get_mcp_ability_prefix();
+ $model_name = $this->slug;
+ $display_name = ucwords(str_replace(['_', '-'], ' ', $model_name));
+
+ if (in_array('get_item', $this->enabled_mcp_abilities, true)) {
+ $this->register_get_item_ability($ability_prefix, $display_name);
+ }
+
+ if (in_array('get_items', $this->enabled_mcp_abilities, true)) {
+ $this->register_get_items_ability($ability_prefix, $display_name);
+ }
+
+ if (in_array('create_item', $this->enabled_mcp_abilities, true)) {
+ $this->register_create_item_ability($ability_prefix, $display_name);
+ }
+
+ if (in_array('update_item', $this->enabled_mcp_abilities, true)) {
+ $this->register_update_item_ability($ability_prefix, $display_name);
+ }
+
+ if (in_array('delete_item', $this->enabled_mcp_abilities, true)) {
+ $this->register_delete_item_ability($ability_prefix, $display_name);
+ }
+
+ /**
+ * Fires after MCP abilities are registered for an entity.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $model_name The model name.
+ * @param object $this The manager instance.
+ */
+ do_action('wu_mcp_abilities_registered', $ability_prefix, $model_name, $this);
+ }
+
+ /**
+ * Register the get item ability.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $display_name The display name.
+ * @return void
+ */
+ protected function register_get_item_ability(string $ability_prefix, string $display_name): void {
+
+ wp_register_ability(
+ "$ability_prefix-get-item",
+ [
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ 'label' => sprintf(__('Get %s by ID', 'ultimate-multisite'), $display_name),
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Retrieve a single %s by its ID', 'ultimate-multisite'), strtolower($display_name)),
+ 'category' => 'multisite-ultimate',
+ 'execute_callback' => [$this, 'mcp_get_item'],
+ 'permission_callback' => [$this, 'mcp_permission_callback'],
+ 'input_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'id' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The ID of the %s to retrieve', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'integer',
+ ],
+ ],
+ 'required' => ['id'],
+ ],
+ 'output_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'item' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The %s object', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'object',
+ ],
+ ],
+ ],
+ 'meta' => [
+ 'mcp' => [
+ 'public' => true, // Expose via MCP (required for MCP access)
+ 'type' => 'tool',
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Register the get items ability.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $display_name The display name.
+ * @return void
+ */
+ protected function register_get_items_ability(string $ability_prefix, string $display_name): void {
+
+ wp_register_ability(
+ "$ability_prefix-get-items",
+ [
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ 'label' => sprintf(__('List %s', 'ultimate-multisite'), $display_name),
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Retrieve a list of %s with optional filters', 'ultimate-multisite'), strtolower($display_name)),
+ 'category' => 'multisite-ultimate',
+ 'execute_callback' => [$this, 'mcp_get_items'],
+ 'permission_callback' => [$this, 'mcp_permission_callback'],
+ 'input_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'per_page' => [
+ 'description' => __('Number of items to retrieve per page', 'ultimate-multisite'),
+ 'type' => 'integer',
+ 'default' => 10,
+ ],
+ 'page' => [
+ 'description' => __('Page number to retrieve', 'ultimate-multisite'),
+ 'type' => 'integer',
+ 'default' => 1,
+ ],
+ ],
+ ],
+ 'output_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'items' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Array of %s objects', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ ],
+ ],
+ 'total' => [
+ 'description' => __('Total number of items', 'ultimate-multisite'),
+ 'type' => 'integer',
+ ],
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Register the create item ability.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $display_name The display name.
+ * @return void
+ */
+ protected function register_create_item_ability(string $ability_prefix, string $display_name): void {
+
+ $input_schema = $this->get_mcp_schema_for_ability('create');
+
+ wp_register_ability(
+ "$ability_prefix-create-item",
+ [
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ 'label' => sprintf(__('Create %s', 'ultimate-multisite'), $display_name),
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Create a new %s', 'ultimate-multisite'), strtolower($display_name)),
+ 'category' => 'multisite-ultimate',
+ 'execute_callback' => [$this, 'mcp_create_item'],
+ 'permission_callback' => [$this, 'mcp_permission_callback'],
+ 'input_schema' => $input_schema,
+ 'output_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'item' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The created %s object', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'object',
+ ],
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Register the update item ability.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $display_name The display name.
+ * @return void
+ */
+ protected function register_update_item_ability(string $ability_prefix, string $display_name): void {
+
+ $input_schema = $this->get_mcp_schema_for_ability('update');
+
+ // Add ID to the properties
+ $input_schema['properties']['id'] = [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The ID of the %s to update', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'integer',
+ ];
+
+ // Add ID to required fields
+ if (! isset($input_schema['required'])) {
+ $input_schema['required'] = [];
+ }
+ $input_schema['required'][] = 'id';
+
+ wp_register_ability(
+ "$ability_prefix-update-item",
+ [
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ 'label' => sprintf(__('Update %s', 'ultimate-multisite'), $display_name),
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Update an existing %s', 'ultimate-multisite'), strtolower($display_name)),
+ 'category' => 'multisite-ultimate',
+ 'execute_callback' => [$this, 'mcp_update_item'],
+ 'permission_callback' => [$this, 'mcp_permission_callback'],
+ 'input_schema' => $input_schema,
+ 'output_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'item' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The updated %s object', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'object',
+ ],
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Register the delete item ability.
+ *
+ * @since 2.5.0
+ * @param string $ability_prefix The ability prefix.
+ * @param string $display_name The display name.
+ * @return void
+ */
+ protected function register_delete_item_ability(string $ability_prefix, string $display_name): void {
+
+ wp_register_ability(
+ "$ability_prefix-delete-item",
+ [
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ 'label' => sprintf(__('Delete %s', 'ultimate-multisite'), $display_name),
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('Delete a %s by its ID', 'ultimate-multisite'), strtolower($display_name)),
+ 'category' => 'multisite-ultimate',
+ 'execute_callback' => [$this, 'mcp_delete_item'],
+ 'permission_callback' => [$this, 'mcp_permission_callback'],
+ 'input_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'id' => [
+ // translators: %s: entity name (e.g., customer, site, product)
+ 'description' => sprintf(__('The ID of the %s to delete', 'ultimate-multisite'), strtolower($display_name)),
+ 'type' => 'integer',
+ ],
+ ],
+ 'required' => ['id'],
+ ],
+ 'output_schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'success' => [
+ 'description' => __('Whether the deletion was successful', 'ultimate-multisite'),
+ 'type' => 'boolean',
+ ],
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get MCP schema for an ability (create or update).
+ * Returns JSON Schema format for input_schema.
+ *
+ * @since 2.5.0
+ * @param string $context The context (create or update).
+ * @return array
+ */
+ protected function get_mcp_schema_for_ability(string $context = 'create'): array {
+
+ if (! method_exists($this, 'get_arguments_schema')) {
+ return [];
+ }
+
+ $rest_schema = $this->get_arguments_schema('update' === $context);
+
+ $properties = [];
+ $required = [];
+
+ foreach ($rest_schema as $key => $args) {
+ $properties[ $key ] = [
+ 'description' => $args['description'] ?? ucfirst(str_replace('_', ' ', $key)),
+ 'type' => $args['type'] ?? 'string',
+ ];
+
+ if (isset($args['default'])) {
+ $properties[ $key ]['default'] = $args['default'];
+ }
+
+ if (isset($args['enum'])) {
+ $properties[ $key ]['enum'] = $args['enum'];
+ }
+
+ if (isset($args['required']) && $args['required']) {
+ $required[] = $key;
+ }
+ }
+
+ $schema = [
+ 'type' => 'object',
+ 'properties' => $properties,
+ ];
+
+ if (! empty($required)) {
+ $schema['required'] = $required;
+ }
+
+ return $schema;
+ }
+
+ /**
+ * MCP callback to get a single item.
+ *
+ * @since 2.5.0
+ * @param array $args The arguments passed to the ability.
+ * @return array|\WP_Error
+ */
+ public function mcp_get_item(array $args) {
+
+ if (! isset($args['id'])) {
+ return new \WP_Error('missing_id', __('ID is required', 'ultimate-multisite'));
+ }
+
+ $item = $this->model_class::get_by_id($args['id']);
+
+ if (empty($item)) {
+ return new \WP_Error(
+ "wu_{$this->slug}_not_found",
+ sprintf(
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ __('%s not found', 'ultimate-multisite'),
+ ucfirst(str_replace('_', ' ', $this->slug))
+ )
+ );
+ }
+
+ return [
+ 'item' => $item->to_array(),
+ ];
+ }
+
+ /**
+ * MCP callback to get a list of items.
+ *
+ * @since 2.5.0
+ * @param array $args The arguments passed to the ability.
+ * @return array
+ */
+ public function mcp_get_items(array $args): array {
+
+ $query_args = array_merge(
+ [
+ 'per_page' => 10,
+ 'page' => 1,
+ ],
+ $args
+ );
+
+ $items = $this->model_class::query($query_args);
+
+ $total = $this->model_class::query(array_merge($query_args, ['count' => true]));
+
+ return [
+ 'items' => array_map(
+ function ($item) {
+ return $item->to_array();
+ },
+ $items
+ ),
+ 'total' => $total,
+ ];
+ }
+
+ /**
+ * MCP callback to create an item.
+ *
+ * @since 2.5.0
+ * @param array $args The arguments passed to the ability.
+ * @return array|\WP_Error
+ */
+ public function mcp_create_item(array $args) {
+
+ $model_name = (new $this->model_class([]))->model;
+
+ $saver_function = "wu_create_{$model_name}";
+
+ if (function_exists($saver_function)) {
+ $item = call_user_func($saver_function, $args);
+
+ $saved = is_wp_error($item) ? $item : true;
+ } else {
+ $item = new $this->model_class($args);
+
+ $saved = $item->save();
+ }
+
+ if (is_wp_error($saved)) {
+ return $saved;
+ }
+
+ if (! $saved) {
+ return new \WP_Error(
+ "wu_{$this->slug}_save_failed",
+ __('Failed to save item', 'ultimate-multisite')
+ );
+ }
+
+ return [
+ 'item' => $item->to_array(),
+ ];
+ }
+
+ /**
+ * MCP callback to update an item.
+ *
+ * @since 2.5.0
+ * @param array $args The arguments passed to the ability.
+ * @return array|\WP_Error
+ */
+ public function mcp_update_item(array $args) {
+
+ if (! isset($args['id'])) {
+ return new \WP_Error('missing_id', __('ID is required', 'ultimate-multisite'));
+ }
+
+ $id = $args['id'];
+ unset($args['id']);
+
+ $item = $this->model_class::get_by_id($id);
+
+ if (empty($item)) {
+ return new \WP_Error(
+ "wu_{$this->slug}_not_found",
+ sprintf(
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ __('%s not found', 'ultimate-multisite'),
+ ucfirst(str_replace('_', ' ', $this->slug))
+ )
+ );
+ }
+
+ foreach ($args as $param => $value) {
+ $set_method = "set_{$param}";
+
+ if ('meta' === $param) {
+ $item->update_meta_batch($value);
+ } elseif (method_exists($item, $set_method)) {
+ call_user_func([$item, $set_method], $value);
+ }
+ }
+
+ $saved = $item->save();
+
+ if (is_wp_error($saved)) {
+ return $saved;
+ }
+
+ if (! $saved) {
+ return new \WP_Error(
+ "wu_{$this->slug}_save_failed",
+ __('Failed to update item', 'ultimate-multisite')
+ );
+ }
+
+ return [
+ 'item' => $item->to_array(),
+ ];
+ }
+
+ /**
+ * MCP callback to delete an item.
+ *
+ * @since 2.5.0
+ * @param array $args The arguments passed to the ability.
+ * @return array|\WP_Error
+ */
+ public function mcp_delete_item(array $args) {
+
+ if (! isset($args['id'])) {
+ return new \WP_Error('missing_id', __('ID is required', 'ultimate-multisite'));
+ }
+
+ $item = $this->model_class::get_by_id($args['id']);
+
+ if (empty($item)) {
+ return new \WP_Error(
+ "wu_{$this->slug}_not_found",
+ sprintf(
+ // translators: %s: entity name (e.g., Customer, Site, Product)
+ __('%s not found', 'ultimate-multisite'),
+ ucfirst(str_replace('_', ' ', $this->slug))
+ )
+ );
+ }
+
+ $result = $item->delete();
+
+ return [
+ 'success' => ! is_wp_error($result) && $result,
+ ];
+ }
+}
diff --git a/inc/api/trait-rest-api.php b/inc/apis/trait-rest-api.php
similarity index 100%
rename from inc/api/trait-rest-api.php
rename to inc/apis/trait-rest-api.php
diff --git a/inc/api/trait-wp-cli.php b/inc/apis/trait-wp-cli.php
similarity index 100%
rename from inc/api/trait-wp-cli.php
rename to inc/apis/trait-wp-cli.php
diff --git a/inc/class-api.php b/inc/class-api.php
index 953623236..b651d84a8 100644
--- a/inc/class-api.php
+++ b/inc/class-api.php
@@ -368,7 +368,7 @@ public function maybe_log_api_call($request): void {
* @param string|array $handler The callback.
* @param \WP_REST_Request $request The request object.
*
- * @return mixed
+ * @return void
*/
public function log_api_errors($result, $handler, $request) {
@@ -389,7 +389,6 @@ public function log_api_errors($result, $handler, $request) {
wu_log_add('api-errors', $result);
}
- return $result;
}
/**
diff --git a/inc/class-autoloader.php b/inc/class-autoloader.php
index 233d88a61..c8a1f442c 100644
--- a/inc/class-autoloader.php
+++ b/inc/class-autoloader.php
@@ -9,8 +9,6 @@
namespace WP_Ultimo;
-use Pablo_Pacheco\WP_Namespace_Autoloader\WP_Namespace_Autoloader;
-
// Exit if accessed directly
defined('ABSPATH') || exit;
diff --git a/inc/class-mcp-adapter.php b/inc/class-mcp-adapter.php
new file mode 100644
index 000000000..59fca6108
--- /dev/null
+++ b/inc/class-mcp-adapter.php
@@ -0,0 +1,331 @@
+is_mcp_enabled()) {
+ return;
+ }
+
+ try {
+ add_action('mcp_adapter_init', array($this, 'initialize_mcp_server'));
+ $this->adapter = McpAdapterCore::instance();
+
+ /**
+ * Fires after the MCP adapter is initialized.
+ *
+ * Allows other plugins and themes to register their own abilities.
+ *
+ * @since 2.5.0
+ * @param MCP_Adapter $mcp_adapter The MCP adapter instance.
+ */
+ do_action('wu_mcp_adapter_initialized', $this);
+ } catch (\Exception $e) {
+ wu_log_add(
+ 'mcp-adapter',
+ sprintf(
+ // translators: %s: error message from the exception
+ __('Failed to initialize MCP adapter: %s', 'ultimate-multisite'),
+ $e->getMessage()
+ )
+ );
+ }
+ }
+
+ /**
+ * Init MCP.
+ *
+ * @return void
+ */
+ public function initialize_mcp_server(): void {
+ $abilities_ids = $this->get_mcp_abilities();
+
+ // Bail if no abilities are available.
+ if ( empty($abilities_ids) ) {
+ return;
+ }
+
+ add_filter('rest_pre_dispatch', [$this, 'rest_pre_dispatch_save_request'], 10, 3);
+
+ /*
+ * Temporarily disable MCP validation during server creation.
+ * Workaround for validator bug with union types (e.g., ["integer", "null"]).
+ * This will be removed once the mcp-adapter validator bug is fixed.
+ *
+ * @see https://github.com/WordPress/mcp-adapter/issues/47
+ */
+ add_filter('mcp_validation_enabled', '__return_false', 999);
+
+ try {
+ // Create MCP server.
+ $this->adapter->create_server(
+ 'ultimate-multisite-server',
+ 'ultimate-multisite',
+ 'mcp-adapter',
+ __('Ultimate Multisite MCP Server', 'ultimate-multisite'),
+ __('AI-accessible Ultimate Multisite operations via MCP', 'ultimate-multisite'),
+ '1.0.0',
+ [HttpTransport::class],
+ \WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
+ \WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
+ $abilities_ids,
+ [],
+ [],
+ [$this, 'permission_callback']
+ );
+ } catch ( \Throwable $e ) {
+ wu_log_add(
+ 'mcp',
+ 'MCP server initialization failed: ' . $e->getMessage(),
+ LogLevel::ERROR
+ );
+ } finally {
+ // Re-enable MCP validation immediately after server creation.
+ remove_filter('mcp_validation_enabled', '__return_false', 999);
+ }
+ }
+
+ /**
+ * Add the admin interface to configure MCP adapter.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function add_settings(): void {
+
+ wu_register_settings_field(
+ 'api',
+ 'mcp_header',
+ [
+ 'title' => __('MCP Adapter Settings', 'ultimate-multisite'),
+ 'desc' => __('Options related to the Model Context Protocol (MCP) adapter. MCP allows AI assistants to interact with Ultimate Multisite programmatically.', 'ultimate-multisite'),
+ 'type' => 'header',
+ ]
+ );
+
+ wu_register_settings_field(
+ 'api',
+ 'enable_mcp',
+ [
+ 'title' => __('Enable MCP Adapter', 'ultimate-multisite'),
+ 'desc' => __('Tick this box to enable the Model Context Protocol (MCP) adapter. This allows AI assistants to interact with Ultimate Multisite through the Abilities API.', 'ultimate-multisite'),
+ 'type' => 'toggle',
+ 'default' => 0,
+ ]
+ );
+ wu_register_settings_field(
+ 'api',
+ 'mcp_serveer_urel',
+ [
+ 'title' => __('MCP Server URL', 'ultimate-multisite'),
+ 'desc' => '',
+ 'tooltip' => __('This is the URL where the MCP server is accessible via HTTP.', 'ultimate-multisite'),
+ 'copy' => true,
+ 'type' => 'text-display',
+ 'default' => rest_url('mcp/mcp-adapter-default-server'),
+ 'require' => [
+ 'enable_mcp' => 1,
+ ],
+ ]
+ );
+ wu_register_settings_field(
+ 'api',
+ 'mcp_stdio_commande',
+ [
+ 'title' => __('STDIO Command', 'ultimate-multisite'),
+ 'desc' => '',
+ 'tooltip' => __('This is the WP-CLI command to run the MCP server via STDIO transport.', 'ultimate-multisite'),
+ 'copy' => true,
+ 'type' => 'text-display',
+ 'default' => 'wp mcp-adapter serve --server=mcp-adapter-default-server --user=admin',
+ 'require' => [
+ 'enable_mcp' => 1,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the MCP adapter instance.
+ *
+ * @since 2.5.0
+ * @return McpAdapterCore|null
+ */
+ public function get_adapter(): ?McpAdapterCore {
+
+ return $this->adapter;
+ }
+
+ /**
+ * Checks if the MCP adapter is enabled.
+ *
+ * @since 2.5.0
+ * @return bool
+ */
+ public function is_mcp_enabled(): bool {
+
+ /**
+ * Allow plugin developers to force a given state for the MCP adapter.
+ *
+ * @since 2.5.0
+ * @param bool $enabled Whether the MCP adapter is enabled.
+ * @return bool
+ */
+ return apply_filters('wu_is_mcp_enabled', wu_get_setting('enable_mcp', false));
+ }
+
+
+ /**
+ * Get abilities for MCP server.
+ *
+ * Filters abilities to include only those with 'wu_' namespace by default,
+ * with a filter to allow inclusion of abilities from other namespaces.
+ *
+ * @return array Array of ability IDs for MCP server.
+ */
+ private function get_mcp_abilities(): array {
+
+ // Check if the abilities API is available.
+ if ( ! function_exists('wp_get_abilities') ) {
+ return array();
+ }
+
+ $all_abilities_ids = array_keys(wp_get_abilities());
+
+ // Filter abilities based on namespace and custom filter.
+ $mcp_abilities = array_filter(
+ $all_abilities_ids,
+ function ($ability_id) {
+ // Include Ultimate Multisite abilities by default.
+ $include = str_starts_with($ability_id, 'multisite-ultimate/');
+
+ // Allow filter to override inclusion decision.
+ /**
+ * Filter to override MCP ability inclusion decision.
+ *
+ * @since 2.4.8
+ * @param bool $include Whether to include the ability.
+ * @param string $ability_id The ability ID.
+ */
+ return apply_filters('wu_mcp_include_ability', $include, $ability_id);
+ }
+ );
+
+ // Re-index array.
+ return array_values($mcp_abilities);
+ }
+
+ /**
+ * Use API Key.
+ *
+ * @param \WP_REST_Request|null $request The Request.
+ *
+ * @return bool|\WP_Error
+ */
+ public function permission_callback(\WP_REST_Request $request = null) {
+ if ($request) {
+ $this->current_request = $request;
+ }
+ if (! $this->current_request) {
+ return new \WP_Error('no_request_object', 'Request Object lost', ['status' => 500]);
+ }
+
+ $result = API::get_instance()->check_authorization($this->current_request);
+
+ if ( ! $result) {
+ return new \WP_Error('invalid_api_key', 'Invalid API key', ['status' => 403]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * For backwards compatibility we use pre dispatch to save the request object.
+ *
+ * @param \WP_REST_Response $result The result.
+ * @param \WP_REST_Server $server The server.
+ * @param \WP_REST_Request $request The request.
+ *
+ * @return mixed
+ */
+ public function rest_pre_dispatch_save_request($result, $server, $request) {
+ $this->current_request = $request;
+ return $result;
+ }
+}
diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php
index 7cadbd4bb..ec2b1b9c3 100644
--- a/inc/class-wp-ultimo.php
+++ b/inc/class-wp-ultimo.php
@@ -438,6 +438,8 @@ protected function load_extra_components(): void {
*/
WP_Ultimo\SSO\SSO::get_instance();
+ WP_Ultimo\SSO\Magic_Link::get_instance();
+
/*
* Loads the debugger tools
*/
@@ -642,6 +644,8 @@ function () {
* Cron Schedules
*/
\WP_Ultimo\Cron::get_instance();
+
+ \WP_Ultimo\MCP_Adapter::get_instance();
}
/**
@@ -666,6 +670,11 @@ protected function load_admin_pages(): void {
*/
new WP_Ultimo\Admin_Pages\Top_Admin_Nav_Menu();
+ /*
+ * Initialize magic links for admin bar My Sites menu.
+ */
+ \WP_Ultimo\SSO\Admin_Bar_Magic_Links::get_instance();
+
/*
* Loads the Checkout Form admin page.
*/
diff --git a/inc/country/class-country.php b/inc/country/class-country.php
index aba3565d6..19db79d48 100644
--- a/inc/country/class-country.php
+++ b/inc/country/class-country.php
@@ -80,7 +80,7 @@ public function get_states() {
*
* @param array $states List of states in a XX => Name format.
* @param string $country_code Two-letter ISO code for the country.
- * @param WP_Ultimo\Country\Country $current_country Instance of the current class.
+ * @param \WP_Ultimo\Country\Country $current_country Instance of the current class.
* @return array The filtered list of states.
*/
return apply_filters('wu_country_get_states', $states, $this->country_code, $this);
@@ -140,7 +140,7 @@ public function get_cities($state_code = '') {
* @param array $cities List of state city names. No keys are present.
* @param string $country_code Two-letter ISO code for the country.
* @param string $state_code Two-letter ISO code for the state.
- * @param WP_Ultimo\Country\Country $current_country Instance of the current class.
+ * @param \WP_Ultimo\Country\Country $current_country Instance of the current class.
* @return array The filtered list of states.
*/
return apply_filters('wu_country_get_cities', $cities, $this->country_code, $state_code, $this);
@@ -238,14 +238,14 @@ public function get_administrative_division_name($state_code = null, $ucwords =
/**
* Returns nice name of the country administrative sub-divisions.
*
- * @since 2.0.11
- *
* @param string $name The division name. Usually something like state, province, region, etc.
* @param string $country_code Two-letter ISO code for the country.
* @param string $state_code Two-letter ISO code for the state.
- * @param WP_Ultimo\Country\Country $current_country Instance of the current class.
- * @param bool $current_country Instance of the current class.
+ * @param bool $ucwords if we uppercase the words.
+ * @param \WP_Ultimo\Country\Country $current_country Instance of the current class.
+ *
* @return string The modified division name.
+ *@since 2.0.11
*/
return apply_filters('wu_country_get_administrative_division_name', $name, $this->country_code, $state_code, $ucwords, $this);
}
diff --git a/inc/functions/domain.php b/inc/functions/domain.php
index 8cdf29dd7..7734326b3 100644
--- a/inc/functions/domain.php
+++ b/inc/functions/domain.php
@@ -30,7 +30,7 @@ function wu_get_domain($domain_id) {
* @since 2.0.0
*
* @param array $query Query arguments.
- * @return \WP_Ultimo\Models\Domain[]
+ * @return \WP_Ultimo\Models\Domain[]|string[]|int
*/
function wu_get_domains($query = []) {
diff --git a/inc/functions/settings.php b/inc/functions/settings.php
index 7dd0edcc5..fc5592859 100644
--- a/inc/functions/settings.php
+++ b/inc/functions/settings.php
@@ -6,8 +6,6 @@
* @since 2.0.0
*/
-use WP_Ultimo\Dependencies\Intervention\Image\ImageManagerStatic as Image;
-
// Exit if accessed directly
defined('ABSPATH') || exit;
@@ -33,12 +31,12 @@ function wu_get_all_settings() {
* @since 2.0.0
*
* @param string $setting Settings name to return.
- * @param mixed $default Default value for the setting if it doesn't exist.
+ * @param mixed $default_value Default value for the setting if it doesn't exist.
* @return mixed The value of that setting
*/
-function wu_get_setting($setting, $default = false) {
+function wu_get_setting($setting, $default_value = false) {
- return WP_Ultimo()->settings->get_setting($setting, $default);
+ return WP_Ultimo()->settings->get_setting($setting, $default_value);
}
/**
diff --git a/inc/functions/url.php b/inc/functions/url.php
index 96a7b0a0c..08de312b3 100644
--- a/inc/functions/url.php
+++ b/inc/functions/url.php
@@ -93,3 +93,70 @@ function wu_ajax_url($when = null, $query_args = [], $site_id = false, $scheme =
return apply_filters('wu_ajax_url', $url, $query_args, $when, $site_id);
}
+
+
+/**
+ * Adds a magic link if needed.
+ *
+ * @param ?int $blog_id Blog id.
+ * @param string $path Path.
+ * @param ?string $scheme Scheme.
+ *
+ * @return false|string
+ */
+function wu_get_admin_url($blog_id = null, $path = '', $scheme = 'admin') {
+ if (! $blog_id) {
+ return get_admin_url($blog_id, $path, $scheme);
+ }
+
+ $current_user_id = get_current_user_id();
+ $admin_url = get_admin_url($blog_id, $path, $scheme);
+
+ if ( ! $current_user_id ) {
+ return $admin_url;
+ }
+
+ $magic_link = \WP_Ultimo\SSO\Magic_Link::get_instance();
+
+ // Check if a magic link is needed.
+ if ( ! $magic_link->site_needs_magic_link($blog_id) ) {
+ return $admin_url;
+ }
+
+ $magic_link_url = $magic_link->generate_magic_link($current_user_id, $blog_id, $admin_url);
+
+ return $magic_link_url ?: get_admin_url($blog_id, $path, $scheme);
+}
+
+/**
+ * Adds a magic link if needed.
+ *
+ * @param ?int $blog_id Blog id.
+ *
+ * @return false|string
+ */
+function wu_get_home_url($blog_id = null) {
+ $site = wu_get_site($blog_id);
+
+ if (! $site) {
+ return get_home_url($blog_id);
+ }
+
+ $home_url = $site->get_active_site_url();
+ $current_user_id = get_current_user_id();
+
+ if ( ! $current_user_id) {
+ return $home_url;
+ }
+
+ $magic_link = \WP_Ultimo\SSO\Magic_Link::get_instance();
+
+ // Check if magic link is needed.
+ if ( ! $magic_link->site_needs_magic_link($blog_id) ) {
+ return $home_url;
+ }
+
+ $magic_link_url = $magic_link->generate_magic_link($current_user_id, $blog_id, $home_url);
+
+ return $magic_link_url ?: get_home_url($blog_id);
+}
diff --git a/inc/helpers/class-validator.php b/inc/helpers/class-validator.php
index 8f279eb57..48af911ba 100644
--- a/inc/helpers/class-validator.php
+++ b/inc/helpers/class-validator.php
@@ -36,7 +36,7 @@ class Validator {
* Holds an instance of the validator object.
*
* @since 2.0.0
- * @var Rakit\Validation\Validator
+ * @var \Rakit\Validation\Validator
*/
protected $validator;
@@ -44,7 +44,7 @@ class Validator {
* Holds an instance of the validation being performed.
*
* @since 2.0.0
- * @var Rakit\Validation\Validation
+ * @var \Rakit\Validation\Validation
*/
protected $validation;
@@ -181,7 +181,7 @@ protected function cast_to_wp_error($errors) {
* Get holds an instance of the validation being performed.
*
* @since 2.0.0
- * @return Rakit\Validation\Validation
+ * @return \Rakit\Validation\Validation
*/
public function get_validation() {
diff --git a/inc/integrations/host-providers/class-gridpane-host-provider.php b/inc/integrations/host-providers/class-gridpane-host-provider.php
index 9d5d8808b..8cb375dc6 100644
--- a/inc/integrations/host-providers/class-gridpane-host-provider.php
+++ b/inc/integrations/host-providers/class-gridpane-host-provider.php
@@ -9,8 +9,6 @@
namespace WP_Ultimo\Integrations\Host_Providers;
-use WP_Ultimo\Integrations\Host_Providers\Base_Host_Provider;
-
// Exit if accessed directly
defined('ABSPATH') || exit;
@@ -137,7 +135,7 @@ public function send_gridpane_api_request($endpoint, $data = [], $method = 'POST
* @since 2.0.0
* @param string $domain The domain name being mapped.
* @param int $site_id ID of the site that is receiving that mapping.
- * @return object\WP_Error
+ * @return object|\WP_Error
*/
public function on_add_domain($domain, $site_id) {
@@ -157,7 +155,7 @@ public function on_add_domain($domain, $site_id) {
* @since 2.0.0
* @param string $domain The domain name being removed.
* @param int $site_id ID of the site that is receiving that mapping.
- * @return object\WP_Error
+ * @return object|\WP_Error
*/
public function on_remove_domain($domain, $site_id) {
diff --git a/inc/list-tables/class-base-list-table.php b/inc/list-tables/class-base-list-table.php
index 29838ab69..86f86ee8f 100644
--- a/inc/list-tables/class-base-list-table.php
+++ b/inc/list-tables/class-base-list-table.php
@@ -223,7 +223,7 @@ public function get_label($label = 'singular') {
* @param integer $per_page Number of items to display per page.
* @param integer $page_number Current page.
* @param boolean $count If we should count records or return the actual records.
- * @return array
+ * @return array|int
*/
public function get_items($per_page = 5, $page_number = 1, $count = false) {
@@ -273,7 +273,7 @@ public function get_items($per_page = 5, $page_number = 1, $count = false) {
* @param array $query_args The query args.
* @return mixed
*/
- protected function _get_items($query_args) {
+ protected function _get_items($query_args) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
$query_class = new $this->query_class();
@@ -518,10 +518,22 @@ public function display(): void {
}
}
+ /**
+ * Returns the filters for this page.
+ *
+ * @since 2.0.0
+ */
+ public function get_filters(): array {
+
+ return [
+ 'filters' => [],
+ 'date_filters' => [],
+ ];
+ }
+
/**
* Display the filters if they exist.
*
- * @todo: refator
* @since 2.0.0
* @return void
*/
@@ -635,7 +647,7 @@ public function process_single_action() {}
* Handles the bulk processing.
*
* @since 2.0.0
- * @return bool
+ * @return bool|\WP_Error
*/
public static function process_bulk_action() {
@@ -824,7 +836,7 @@ public function column_default($item, $column_name) {
* @param string $date Valid date to be used inside a strtotime call.
* @return string
*/
- public function _column_datetime($date) {
+ public function _column_datetime($date) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
if ( ! wu_validate_date($date)) {
return __('--', 'ultimate-multisite');
@@ -1150,7 +1162,7 @@ public function column_cb($item) {
*
* @since 2.0.0
*/
- public function _get_js_var_name(): string {
+ public function _get_js_var_name(): string { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
return str_replace('-', '_', $this->id);
}
@@ -1161,7 +1173,7 @@ public function _get_js_var_name(): string {
* @since 2.0.0
* @return void
*/
- public function _js_vars(): void {
+ public function _js_vars(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
/**
* Call the parent method for backwards compat.
@@ -1305,16 +1317,6 @@ public function get_sortable_columns() {
public function get_extra_fields() {
return [];
-
- $_filter_fields = [];
-
- if (isset($filters['filters'])) {
- foreach ($filters['filters'] as $field_name => $field) {
- $_filter_fields[ $field_name ] = wu_request($field_name, '');
- }
- }
-
- return $_filter_fields;
}
/**
diff --git a/inc/list-tables/class-checkout-form-list-table.php b/inc/list-tables/class-checkout-form-list-table.php
index 2311ca563..d254d5aaa 100644
--- a/inc/list-tables/class-checkout-form-list-table.php
+++ b/inc/list-tables/class-checkout-form-list-table.php
@@ -52,7 +52,7 @@ public function __construct() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
+ * @param \WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
*/
public function column_name($item): string {
@@ -89,7 +89,7 @@ public function column_name($item): string {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
+ * @param \WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
* @return string
*/
public function column_slug($item) {
@@ -104,7 +104,7 @@ public function column_slug($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
+ * @param \WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
*/
public function column_steps($item): string {
// translators: %1$d: number of steps, %2$d: number of fields
@@ -116,7 +116,7 @@ public function column_steps($item): string {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
+ * @param \WP_Ultimo\Models\Checkout_Form $item Checkout Form object.
*/
public function column_shortcode($item): string {
@@ -128,7 +128,7 @@ public function column_shortcode($item): string {
__('Copy to the Clipboard', 'ultimate-multisite')
);
- return sprintf('', esc_attr($item->get_shortcode()), '');
+ return sprintf('', esc_attr($item->get_shortcode()));
}
/**
@@ -203,17 +203,4 @@ public function get_columns() {
return $columns;
}
-
- /**
- * Returns the filters for this page.
- *
- * @since 2.0.0
- */
- public function get_filters(): array {
-
- return [
- 'filters' => [],
- 'date_filters' => [],
- ];
- }
}
diff --git a/inc/list-tables/class-customers-site-list-table.php b/inc/list-tables/class-customers-site-list-table.php
index a151a11bd..827a80bc8 100644
--- a/inc/list-tables/class-customers-site-list-table.php
+++ b/inc/list-tables/class-customers-site-list-table.php
@@ -84,7 +84,7 @@ public function column_responsive($item): void {
'icon' => 'dashicons-wu-browser wu-align-middle wu-mr-1',
'label' => __('Go to the Dashboard', 'ultimate-multisite'),
'value' => __('Dashboard', 'ultimate-multisite'),
- 'url' => get_admin_url($item->get_id()),
+ 'url' => wu_get_admin_url($item->get_id()),
],
'membership' => [
'icon' => 'dashicons-wu-rotate-ccw wu-align-middle wu-mr-1',
diff --git a/inc/list-tables/class-discount-code-list-table.php b/inc/list-tables/class-discount-code-list-table.php
index a545e8967..5d5dee308 100644
--- a/inc/list-tables/class-discount-code-list-table.php
+++ b/inc/list-tables/class-discount-code-list-table.php
@@ -52,7 +52,7 @@ public function __construct() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Discount_Code $item Discount_Code object.
+ * @param \WP_Ultimo\Models\Discount_Code $item Discount_Code object.
*/
public function column_name($item): string {
@@ -84,7 +84,7 @@ public function column_name($item): string {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Discount_Code $item Discount_Code object.
+ * @param \WP_Ultimo\Models\Discount_Code $item Discount_Code object.
*
* @return string
*/
@@ -109,7 +109,7 @@ public function column_value($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Discount_Code $item Discount_Code object.
+ * @param \WP_Ultimo\Models\Discount_Code $item Discount_Code object.
*
* @return string
*/
@@ -134,7 +134,7 @@ public function column_setup_fee_value($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Discount_Code $item Discount_Code object.
+ * @param \WP_Ultimo\Models\Discount_Code $item Discount_Code object.
* @return string
*/
public function column_uses($item) {
@@ -158,7 +158,7 @@ public function column_uses($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Discount_Code $item Discount_Code object.
+ * @param \WP_Ultimo\Models\Discount_Code $item Discount_Code object.
* @return string
*/
public function column_coupon_code($item) {
@@ -196,17 +196,4 @@ public function get_columns() {
return $columns;
}
-
- /**
- * Returns the filters for this page.
- *
- * @since 2.0.0
- */
- public function get_filters(): array {
-
- return [
- 'filters' => [],
- 'date_filters' => [],
- ];
- }
}
diff --git a/inc/list-tables/class-domain-list-table.php b/inc/list-tables/class-domain-list-table.php
index f81bc3e73..199105ba5 100644
--- a/inc/list-tables/class-domain-list-table.php
+++ b/inc/list-tables/class-domain-list-table.php
@@ -9,7 +9,6 @@
namespace WP_Ultimo\List_Tables;
-use WP_Ultimo\Models\Domain;
use WP_Ultimo\Database\Domains\Domain_Stage;
// Exit if accessed directly
@@ -72,7 +71,7 @@ public function get_extra_query_fields() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Domain $item Domain object.
+ * @param \WP_Ultimo\Models\Domain $item Domain object.
*/
public function column_domain($item): string {
@@ -98,7 +97,7 @@ public function column_domain($item): string {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Domain $item Domain object.
+ * @param \WP_Ultimo\Models\Domain $item Domain object.
* @return string
*/
public function column_active($item) {
@@ -111,7 +110,7 @@ public function column_active($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Domain $item Domain object.
+ * @param \WP_Ultimo\Models\Domain $item Domain object.
* @return string
*/
public function column_primary_domain($item) {
@@ -124,7 +123,7 @@ public function column_primary_domain($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Domain $item Domain object.
+ * @param \WP_Ultimo\Models\Domain $item Domain object.
* @return string
*/
public function column_secure($item) {
diff --git a/inc/list-tables/class-membership-list-table-widget.php b/inc/list-tables/class-membership-list-table-widget.php
index 58fc7421a..829bac26b 100644
--- a/inc/list-tables/class-membership-list-table-widget.php
+++ b/inc/list-tables/class-membership-list-table-widget.php
@@ -115,7 +115,7 @@ public function get_extra_query_fields() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Membership $item Membership object.
+ * @param \WP_Ultimo\Models\Membership $item Membership object.
*/
public function column_hash($item): string {
@@ -199,7 +199,7 @@ public function column_amount($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Membership $item Membership object.
+ * @param \WP_Ultimo\Models\Membership $item Membership object.
* @return string
*/
public function column_customer($item) {
@@ -274,5 +274,5 @@ public function get_columns() {
* @since 2.0.0
* @return void
*/
- public function _js_vars(): void {}
+ public function _js_vars(): void {} // phpcs:ignore PSR2
}
diff --git a/inc/list-tables/class-membership-list-table.php b/inc/list-tables/class-membership-list-table.php
index 00815e364..f0f7cad36 100644
--- a/inc/list-tables/class-membership-list-table.php
+++ b/inc/list-tables/class-membership-list-table.php
@@ -66,7 +66,7 @@ public function get_extra_query_fields() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Membership $item Membership object.
+ * @param \WP_Ultimo\Models\Membership $item Membership object.
* @return string
*/
public function column_hash($item) {
@@ -101,7 +101,7 @@ public function column_hash($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Membership $item Membership object.
+ * @param \WP_Ultimo\Models\Membership $item Membership object.
* @return string
*/
public function column_status($item) {
@@ -120,7 +120,7 @@ public function column_status($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Membership $item Membership object.
+ * @param \WP_Ultimo\Models\Membership $item Membership object.
* @return string
*/
public function column_amount($item) {
diff --git a/inc/list-tables/class-payment-list-table-widget.php b/inc/list-tables/class-payment-list-table-widget.php
index b670fca95..cb9ccf71b 100644
--- a/inc/list-tables/class-payment-list-table-widget.php
+++ b/inc/list-tables/class-payment-list-table-widget.php
@@ -229,9 +229,11 @@ public function get_columns() {
* Returns the filters for this page.
*
* @since 2.0.0
- * @return void.
+ * @return array
*/
- public function get_filters() {}
+ public function get_filters(): array {
+ return [];
+ }
/**
* Overrides the parent method to include the custom ajax functionality for Ultimate Multisite.
@@ -239,5 +241,5 @@ public function get_filters() {}
* @since 2.0.0
* @return void
*/
- public function _js_vars(): void {}
+ public function _js_vars(): void {} // phpcs:ignore PSR2
}
diff --git a/inc/list-tables/class-payment-list-table.php b/inc/list-tables/class-payment-list-table.php
index f0ddd7d11..053109f7a 100644
--- a/inc/list-tables/class-payment-list-table.php
+++ b/inc/list-tables/class-payment-list-table.php
@@ -107,7 +107,7 @@ public function column_hash($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Payment $item Payment object.
+ * @param \WP_Ultimo\Models\Payment $item Payment object.
* @return string
*/
public function column_status($item) {
diff --git a/inc/list-tables/class-product-list-table.php b/inc/list-tables/class-product-list-table.php
index a29fa8c91..01201992c 100644
--- a/inc/list-tables/class-product-list-table.php
+++ b/inc/list-tables/class-product-list-table.php
@@ -52,7 +52,7 @@ public function __construct() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item Product object.
+ * @param \WP_Ultimo\Models\Product $item Product object.
* @return string
*/
public function column_name($item) {
@@ -91,7 +91,7 @@ public function column_name($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item Product object.
+ * @param \WP_Ultimo\Models\Product $item Product object.
* @return string
*/
public function column_type($item) {
@@ -108,7 +108,7 @@ public function column_type($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item Product object.
+ * @param \WP_Ultimo\Models\Product $item Product object.
* @return string
*/
public function column_slug($item) {
@@ -123,7 +123,7 @@ public function column_slug($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item Product object.
+ * @param \WP_Ultimo\Models\Product $item Product object.
* @return string
*/
public function column_amount($item) {
@@ -169,7 +169,7 @@ public function column_amount($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item Product object.
+ * @param \WP_Ultimo\Models\Product $item Product object.
* @return string
*/
public function column_setup_fee($item) {
@@ -266,7 +266,7 @@ public function get_columns() {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Product $item The line item being displayed.
+ * @param \WP_Ultimo\Models\Product $item The line item being displayed.
* @return void
*/
public function single_row_grid($item): void {
diff --git a/inc/list-tables/class-site-list-table.php b/inc/list-tables/class-site-list-table.php
index 55cb7ffb3..8ed6f68f7 100644
--- a/inc/list-tables/class-site-list-table.php
+++ b/inc/list-tables/class-site-list-table.php
@@ -112,7 +112,7 @@ public function column_cb($item): string {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Site $item Site object.
+ * @param \WP_Ultimo\Models\Site $item Site object.
*/
public function column_path($item): string {
@@ -121,8 +121,6 @@ public function column_path($item): string {
'model' => 'site',
];
- $title = $item->get_title();
-
$title = sprintf('%s', wu_network_admin_url('wp-ultimo-edit-site', $url_atts), $item->get_title());
// Concatenate the two blocks
@@ -220,7 +218,7 @@ public function column_blog_id($item) {
*
* @since 2.0.0
*
- * @param WP_Ultimo\Models\Site $item Site object.
+ * @param \WP_Ultimo\Models\Site $item Site object.
* @return string
*/
public function column_type($item) {
diff --git a/inc/list-tables/class-webhook-list-table.php b/inc/list-tables/class-webhook-list-table.php
index adb841520..258821d43 100644
--- a/inc/list-tables/class-webhook-list-table.php
+++ b/inc/list-tables/class-webhook-list-table.php
@@ -181,17 +181,4 @@ public function get_columns() {
return $columns;
}
-
- /**
- * Returns the filters for this page.
- *
- * @since 2.0.0
- */
- public function get_filters(): array {
-
- return [
- 'filters' => [],
- 'date_filters' => [],
- ];
- }
}
diff --git a/inc/list-tables/customer-panel/class-product-list-table.php b/inc/list-tables/customer-panel/class-product-list-table.php
index ad2eb73ef..a4b026a1c 100644
--- a/inc/list-tables/customer-panel/class-product-list-table.php
+++ b/inc/list-tables/customer-panel/class-product-list-table.php
@@ -48,19 +48,6 @@ public function get_columns() {
return [];
}
- /**
- * Resets the filters.
- *
- * @since 2.0.0
- */
- public function get_filters(): array {
-
- return [
- 'filters' => [],
- 'date_filters' => [],
- ];
- }
-
/**
* Resets bulk actions.
*
diff --git a/inc/list-tables/customer-panel/class-site-list-table.php b/inc/list-tables/customer-panel/class-site-list-table.php
index 2b88113d2..a9457ec17 100644
--- a/inc/list-tables/customer-panel/class-site-list-table.php
+++ b/inc/list-tables/customer-panel/class-site-list-table.php
@@ -48,19 +48,6 @@ public function get_columns() {
return [];
}
- /**
- * Clears filters.
- *
- * @since 2.0.0
- */
- public function get_filters(): array {
-
- return [
- 'filters' => [],
- 'date_filters' => [],
- ];
- }
-
/**
* Clears views.
*
diff --git a/inc/managers/class-broadcast-manager.php b/inc/managers/class-broadcast-manager.php
index 9fb4e1d3c..6c46fecc4 100644
--- a/inc/managers/class-broadcast-manager.php
+++ b/inc/managers/class-broadcast-manager.php
@@ -27,6 +27,7 @@ class Broadcast_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -57,6 +58,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
/**
* Add unseen broadcast notices to the panel.
*/
@@ -97,7 +100,7 @@ public function add_unseen_broadcast_notices(): void {
$targets = [$targets];
}
- $dismissed = get_user_meta(get_current_user_id(), 'wu_dismissed_admin_notices');
+ $dismissed = get_user_meta(get_current_user_id(), 'wu_dismissed_admin_notices', false);
if (in_array($current_customer->get_id(), $targets, true) && ! in_array($broadcast->get_id(), $dismissed, true)) {
$notice = '' . $broadcast->get_title() . ' ' . $broadcast->get_content() . '';
diff --git a/inc/managers/class-customer-manager.php b/inc/managers/class-customer-manager.php
index 7cfe793ac..c09ecb37e 100644
--- a/inc/managers/class-customer-manager.php
+++ b/inc/managers/class-customer-manager.php
@@ -27,6 +27,7 @@ class Customer_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -57,6 +58,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action(
'init',
function () {
@@ -118,8 +121,8 @@ public function handle_resend_verification_email(): void {
* @return array $response The Heartbeat response
*/
public function on_heartbeat_send($response) {
-
- $this->log_ip_and_last_login(wp_get_current_user());
+ $user = wp_get_current_user();
+ $this->log_ip_and_last_login($user->user_login, $user);
return $response;
}
@@ -127,15 +130,14 @@ public function on_heartbeat_send($response) {
/**
* Saves the IP address and last_login date onto the user.
*
- * @since 2.0.0
- *
+ * @param string $user_login The username of the user that logged in.
* @param \WP_User $user The WP User object of the user that logged in.
* @return void
*/
- public function log_ip_and_last_login($user): void {
+ public function log_ip_and_last_login($user_login, $user): void {
if ( ! is_a($user, '\WP_User')) {
- $user = get_user_by('login', $user);
+ $user = get_user_by('login', $user_login);
}
if ( ! $user) {
diff --git a/inc/managers/class-discount-code-manager.php b/inc/managers/class-discount-code-manager.php
index f2ca6f32b..ec0f2e96f 100644
--- a/inc/managers/class-discount-code-manager.php
+++ b/inc/managers/class-discount-code-manager.php
@@ -26,6 +26,7 @@ class Discount_Code_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -56,6 +57,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action('wu_gateway_payment_processed', [$this, 'maybe_add_use_on_payment_received']);
}
diff --git a/inc/managers/class-domain-manager.php b/inc/managers/class-domain-manager.php
index 4093977fb..6cc19a346 100644
--- a/inc/managers/class-domain-manager.php
+++ b/inc/managers/class-domain-manager.php
@@ -28,6 +28,7 @@ class Domain_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -132,6 +133,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
$this->set_cookie_domain();
add_action('plugins_loaded', [$this, 'load_integrations']);
@@ -536,6 +539,17 @@ public function add_sso_settings(): void {
],
]
);
+
+ wu_register_settings_field(
+ 'sso',
+ 'enable_magic_links',
+ [
+ 'title' => __('Enable Magic Links', 'ultimate-multisite'),
+ 'desc' => __('Enables magic link authentication for custom domains. Magic links provide a fallback authentication method for browsers that don\'t support third-party cookies. When enabled, dashboard and site links will automatically log users in when accessing sites with custom domains. Tokens are cryptographically secure, one-time use, and expire after 10 minutes.', 'ultimate-multisite'),
+ 'type' => 'toggle',
+ 'default' => 1,
+ ]
+ );
}
/**
diff --git a/inc/managers/class-event-manager.php b/inc/managers/class-event-manager.php
index 050d555ef..3d6d25dd0 100644
--- a/inc/managers/class-event-manager.php
+++ b/inc/managers/class-event-manager.php
@@ -11,6 +11,7 @@
namespace WP_Ultimo\Managers;
+use Psr\Log\LogLevel;
use WP_Ultimo\Models\Base_Model;
use WP_Ultimo\Models\Event;
@@ -26,8 +27,10 @@ class Event_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
+ const LOG_FILE_NAME = 'events';
/**
* The manager slug.
*
@@ -72,6 +75,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action('init', [$this, 'register_all_events']);
add_action('wp_ajax_wu_get_event_payload_preview', [$this, 'event_payload_preview']);
@@ -248,22 +253,24 @@ public function get_event_type_as_options() {
* @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.
+ * @return bool
*/
public function do_event($slug, $payload) {
$registered_event = $this->get_event($slug);
if ( ! $registered_event) {
- return ['error' => 'Event not found'];
+ wu_log_add(self::LOG_FILE_NAME, 'Event not found', LogLevel::ERROR);
+ return false;
}
$payload_diff = array_diff_key(wu_maybe_lazy_load_payload($registered_event['payload']), $payload);
- if (isset($payload_diff[0])) {
+ if (! empty($payload_diff[0])) {
foreach ($payload_diff[0] as $diff_key => $diff_value) {
- return ['error' => 'Param required:' . $diff_key];
+ wu_log_add(self::LOG_FILE_NAME, 'Param required:' . $diff_key, LogLevel::ERROR);
}
+ return false;
}
$payload['wu_version'] = wu_get_version();
@@ -275,7 +282,7 @@ public function do_event($slug, $payload) {
/**
* Saves in the database
*/
- $this->save_event($slug, $payload);
+ return $this->save_event($slug, $payload);
}
/**
@@ -334,9 +341,9 @@ public function get_event($slug) {
*
* @param string $slug of the event.
* @param array $payload with event params.
- * @return void.
+ * @return bool.
*/
- public function save_event($slug, $payload): void {
+ public function save_event($slug, $payload): bool {
$event = new Event(
[
@@ -349,7 +356,11 @@ public function save_event($slug, $payload): void {
]
);
- $event->save();
+ $return = $event->save();
+ if ( is_wp_error($return)) {
+ return false;
+ }
+ return (bool) $return;
}
/**
@@ -590,7 +601,7 @@ public function get_model_payload(string $model, ?object $model_object = null) {
*
* @since 2.0.0
*/
- public function clean_old_events(): bool {
+ public function clean_old_events(): void {
/*
* Add a filter setting this to 0 or false
* to prevent old events from being ever deleted.
@@ -598,7 +609,7 @@ public function clean_old_events(): bool {
$threshold_days = apply_filters('wu_events_threshold_days', 1);
if (empty($threshold_days)) {
- return false;
+ return;
}
$events_to_remove = wu_get_events(
@@ -624,8 +635,6 @@ public function clean_old_events(): bool {
// Translators: 1: Number of successfully removed events. 2: Number of failed events to remove.
wu_log_add('wu-cron', sprintf(__('Removed %1$d events successfully. Failed to remove %2$d events.', 'ultimate-multisite'), $success_count, count($events_to_remove) - $success_count));
-
- return true;
}
/**
diff --git a/inc/managers/class-membership-manager.php b/inc/managers/class-membership-manager.php
index ea4c4d922..fb5c416e6 100644
--- a/inc/managers/class-membership-manager.php
+++ b/inc/managers/class-membership-manager.php
@@ -26,8 +26,10 @@ class Membership_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
+ const LOG_FILE_NAME = 'memberships';
/**
* The manager slug.
*
@@ -56,6 +58,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action(
'init',
function () {
@@ -120,14 +124,15 @@ public function publish_pending_site(): void {
* @since 2.0.0
*
* @param int $membership_id The membership id.
- * @return bool|\WP_Error
+ * @return void
*/
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.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$status = $membership->publish_pending_site();
@@ -135,8 +140,6 @@ public function async_publish_pending_site($membership_id) {
if (is_wp_error($status)) {
wu_log_add('site-errors', $status, LogLevel::ERROR);
}
-
- return $status;
}
/**
@@ -176,7 +179,7 @@ public function check_pending_site_created() {
* @since 2.0.0
*
* @param int $membership_id The membership id.
- * @return bool|\WP_Error
+ * @return void
*/
public function async_membership_swap($membership_id) {
@@ -185,13 +188,15 @@ public function async_membership_swap($membership_id) {
$membership = wu_get_membership($membership_id);
if ( ! $membership) {
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$scheduled_swap = $membership->get_scheduled_swap();
if (empty($scheduled_swap)) {
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$order = $scheduled_swap->order;
@@ -205,13 +210,14 @@ public function async_membership_swap($membership_id) {
if (is_wp_error($status)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, $status->get_error_message(), LogLevel::ERROR);
+ return;
}
} catch (\Throwable $exception) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, $exception->getMessage(), LogLevel::ERROR);
+ return;
}
/*
@@ -220,8 +226,6 @@ public function async_membership_swap($membership_id) {
$membership->delete_scheduled_swap();
$wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return true;
}
/**
@@ -292,7 +296,7 @@ public function mark_cancelled_date($old_value, $new_value, $item_id): void {
*
* @param int $membership_id The ID of the membership being transferred.
* @param int $target_customer_id The new owner.
- * @return mixed
+ * @return void
*/
public function async_transfer_membership($membership_id, $target_customer_id) {
@@ -303,7 +307,8 @@ public function async_transfer_membership($membership_id, $target_customer_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.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$wpdb->query('START TRANSACTION'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@@ -330,8 +335,8 @@ public function async_transfer_membership($membership_id, $target_customer_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
}
@@ -345,19 +350,18 @@ public function async_transfer_membership($membership_id, $target_customer_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- return new \WP_Error('exception', $e->getMessage());
+ wu_log_add(self::LOG_FILE_NAME, $e->getMessage(), LogLevel::ERROR);
}
$wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$membership->unlock();
-
- return true;
}
/**
@@ -366,7 +370,7 @@ public function async_transfer_membership($membership_id, $target_customer_id) {
* @since 2.0.0
*
* @param int $membership_id The ID of the membership being deleted.
- * @return mixed
+ * @return void
*/
public function async_delete_membership($membership_id) {
@@ -375,7 +379,8 @@ public function async_delete_membership($membership_id) {
$membership = wu_get_membership($membership_id);
if ( ! $membership) {
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$wpdb->query('START TRANSACTION'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@@ -400,8 +405,8 @@ public function async_delete_membership($membership_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
}
@@ -412,17 +417,15 @@ public function async_delete_membership($membership_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return new \WP_Error('exception', $e->getMessage());
+ wu_log_add(self::LOG_FILE_NAME, $e->getMessage(), LogLevel::ERROR);
+ return;
}
$wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return true;
}
}
diff --git a/inc/managers/class-payment-manager.php b/inc/managers/class-payment-manager.php
index b2c30077a..ac2861a2a 100644
--- a/inc/managers/class-payment-manager.php
+++ b/inc/managers/class-payment-manager.php
@@ -11,6 +11,7 @@
namespace WP_Ultimo\Managers;
+use Psr\Log\LogLevel;
use WP_Ultimo\Managers\Base_Manager;
use WP_Ultimo\Models\Payment;
use WP_Ultimo\Logger;
@@ -29,8 +30,10 @@ class Payment_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
+ const LOG_FILE_NAME = 'payments';
/**
* The manager slug.
*
@@ -59,6 +62,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
$this->register_forms();
add_action(
@@ -317,7 +322,7 @@ public function invoice_viewer(): void {
*
* @param int $payment_id The ID of the payment being transferred.
* @param int $target_customer_id The new owner.
- * @return mixed
+ * @return void
*/
public function async_transfer_payment($payment_id, $target_customer_id) {
@@ -328,7 +333,8 @@ public function async_transfer_payment($payment_id, $target_customer_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.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$wpdb->query('START TRANSACTION'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@@ -344,18 +350,17 @@ public function async_transfer_payment($payment_id, $target_customer_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ wu_log_add(self::LOG_FILE_NAME, $e->getMessage(), LogLevel::ERROR);
- return new \WP_Error('exception', $e->getMessage());
+ return;
}
$wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return true;
}
/**
@@ -364,7 +369,7 @@ public function async_transfer_payment($payment_id, $target_customer_id) {
* @since 2.0.0
*
* @param int $payment_id The ID of the payment being deleted.
- * @return mixed
+ * @return void
*/
public function async_delete_payment($payment_id) {
@@ -373,7 +378,8 @@ public function async_delete_payment($payment_id) {
$payment = wu_get_payment($payment_id);
if ( ! $payment) {
- return new \WP_Error('error', __('An unexpected error happened.', 'ultimate-multisite'));
+ wu_log_add(self::LOG_FILE_NAME, __('An unexpected error happened.', 'ultimate-multisite'), LogLevel::ERROR);
+ return;
}
$wpdb->query('START TRANSACTION'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@@ -387,18 +393,17 @@ public function async_delete_payment($payment_id) {
if (is_wp_error($saved)) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return $saved;
+ wu_log_add(self::LOG_FILE_NAME, $saved->get_error_message(), LogLevel::ERROR);
+ return;
}
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- return new \WP_Error('exception', $e->getMessage());
+ wu_log_add(self::LOG_FILE_NAME, $e->getMessage(), LogLevel::ERROR);
+ return;
}
$wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
-
- return true;
}
/**
diff --git a/inc/managers/class-product-manager.php b/inc/managers/class-product-manager.php
index 1a2a4389c..89151c45d 100644
--- a/inc/managers/class-product-manager.php
+++ b/inc/managers/class-product-manager.php
@@ -26,6 +26,7 @@ class Product_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -55,5 +56,7 @@ public function init(): void {
$this->enable_rest_api();
$this->enable_wp_cli();
+
+ $this->enable_mcp_abilities();
}
}
diff --git a/inc/managers/class-site-manager.php b/inc/managers/class-site-manager.php
index 5daf07baf..63233b380 100644
--- a/inc/managers/class-site-manager.php
+++ b/inc/managers/class-site-manager.php
@@ -14,6 +14,7 @@
use WP_Ultimo\Helpers\Screenshot;
use WP_Ultimo\Database\Sites\Site_Type;
use WP_Ultimo\Database\Memberships\Membership_Status;
+use WP_Ultimo\Models\Site;
// Exit if accessed directly
defined('ABSPATH') || exit;
@@ -27,6 +28,7 @@ class Site_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -57,6 +59,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action('after_setup_theme', [$this, 'additional_thumbnail_sizes']);
add_action('wp_ajax_wu_get_screenshot', [$this, 'get_site_screenshot']);
@@ -83,11 +87,11 @@ public function init(): void {
add_filter('mucd_string_to_replace', [$this, 'search_and_replace_on_duplication'], 10, 3);
- add_filter('wu_site_created', [$this, 'search_and_replace_for_new_site'], 10, 2);
+ add_action('wu_site_created', [$this, 'search_and_replace_for_new_site'], 10, 2);
add_action('wu_handle_bulk_action_form_site_delete-pending', [$this, 'handle_delete_pending_sites'], 100, 3);
- add_action('users_list_table_query_args', [$this, 'hide_super_admin_from_list'], 10, 1);
+ add_filter('users_list_table_query_args', [$this, 'hide_super_admin_from_list'], 10, 1);
add_action('wu_before_handle_order_submission', [$this, 'maybe_validate_add_new_site'], 15);
@@ -325,14 +329,14 @@ public function lock_site(): void {
* @since 2.0.0
*
* @param int $site_id The site ID.
- * @return mixed
+ * @return void
*/
public function async_get_site_screenshot($site_id) {
$site = wu_get_site($site_id);
if ( ! $site) {
- return false;
+ return;
}
$domain = $site->get_active_site_url();
@@ -340,12 +344,12 @@ public function async_get_site_screenshot($site_id) {
$attachment_id = Screenshot::take_screenshot($domain);
if ( ! $attachment_id) {
- return false;
+ return;
}
$site->set_featured_image_id($attachment_id);
- return $site->save();
+ $site->save();
}
/**
@@ -598,8 +602,8 @@ public function get_search_and_replace_settings() {
* Handles search and replace for new blogs from WordPress.
*
* @since 1.7.0
- * @param array $data The date being saved.
- * @param object $site The site object.
+ * @param array $data The date being saved.
+ * @param Site $site The site object.
* @return void
*/
public static function search_and_replace_for_new_site($data, $site): void {
diff --git a/inc/managers/class-webhook-manager.php b/inc/managers/class-webhook-manager.php
index eff77f94d..492eff177 100644
--- a/inc/managers/class-webhook-manager.php
+++ b/inc/managers/class-webhook-manager.php
@@ -26,6 +26,7 @@ class Webhook_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\MCP_Abilities;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -72,6 +73,8 @@ public function init(): void {
$this->enable_wp_cli();
+ $this->enable_mcp_abilities();
+
add_action('init', [$this, 'register_webhook_listeners']);
add_action('wp_ajax_wu_send_test_event', [$this, 'send_test_event']);
diff --git a/inc/models/class-domain.php b/inc/models/class-domain.php
index f3a1bc15c..7465af4cf 100644
--- a/inc/models/class-domain.php
+++ b/inc/models/class-domain.php
@@ -236,14 +236,12 @@ public function get_site() {
*/
public function get_path() {
if (! isset($this->path)) {
- $site = $this->get_site();
+ // don't use $this->get_site() as it causes infinite loop and native is faster anyway.
+ $site = \WP_Site::get_instance($this->get_blog_id());
if ( ! $site) {
return null;
- } elseif ($site instanceof \WP_Site) {
- $this->path = $site->path;
- } elseif ($site instanceof Site) {
- $this->path = $site->get_path();
}
+ $this->path = $site->path;
}
return $this->path;
diff --git a/inc/models/class-event.php b/inc/models/class-event.php
index 3ef5ac302..01b938416 100644
--- a/inc/models/class-event.php
+++ b/inc/models/class-event.php
@@ -10,6 +10,7 @@
namespace WP_Ultimo\Models;
use WP_Ultimo\Models\Base_Model;
+use WP_User;
// Exit if accessed directly
defined('ABSPATH') || exit;
@@ -268,7 +269,7 @@ public function get_message() {
*/
public function interpolate_message($message, $payload): string {
- $payload = json_decode(json_encode($payload), true);
+ $payload = json_decode(json_encode($payload), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
$interpolation_keys = [];
@@ -351,7 +352,7 @@ public function get_author_id() {
* Returns the user associated with this author.
*
* @since 2.0.0
- * @return WP_User
+ * @return ?WP_User
*/
public function get_author_user() {
@@ -377,6 +378,7 @@ public function get_author_display_name() {
if ($user) {
return $user->display_name;
}
+ return '';
}
/**
@@ -392,6 +394,7 @@ public function get_author_email_address() {
if ($user) {
return $user->user_email;
}
+ return '';
}
/**
@@ -623,8 +626,8 @@ public function to_array() {
/**
* Override to clear event count.
*
+ * @return bool|\WP_Error
* @since 2.0.0
- * @return int|false
*/
public function save() {
diff --git a/inc/models/class-site.php b/inc/models/class-site.php
index e53d4467d..c424bd3d3 100644
--- a/inc/models/class-site.php
+++ b/inc/models/class-site.php
@@ -1606,10 +1606,9 @@ public function save() {
* @since 2.0.0
*
* @param array $data The object data that will be stored.
- * @param \WP_Ultimo\Models\Base_Model $this The object instance.
+ * @param \WP_Ultimo\Models\Base_Model $site The object instance.
*/
- do_action('wu_site_created', $data, $this); // @phpstan-ignore-line
-
+ do_action('wu_site_created', $data, $this);
}
if ( ! is_wp_error($saved) && wu_get_setting('enable_screenshot_generator', true)) {
diff --git a/inc/sso/class-admin-bar-magic-links.php b/inc/sso/class-admin-bar-magic-links.php
new file mode 100644
index 000000000..598c311d1
--- /dev/null
+++ b/inc/sso/class-admin-bar-magic-links.php
@@ -0,0 +1,149 @@
+get_nodes() as $node) {
+ $parts = explode('-', $node->id);
+ if (count($parts) >= 3 && 'blog' === $parts[0] && is_numeric($parts[1]) && 'd' === $parts[2]) {
+ $site_id = (int) $parts[1];
+ } else {
+ continue;
+ }
+
+ // Generate magic link.
+ $magic_link = wu_get_admin_url($site_id);
+
+ if ( ! $magic_link ) {
+ continue;
+ }
+
+ // Update the node with the magic link.
+ $node->href = $magic_link;
+
+ $wp_admin_bar->add_node($node);
+ }
+ }
+
+ /**
+ * Show access denied splash screen with magic links.
+ *
+ * This replaces the WordPress core access denied splash screen
+ * with our own version that uses magic links for sites with custom domains.
+ *
+ * @since 2.0.0
+ * @return void
+ */
+ public function show_access_denied_with_magic_links(): void {
+
+ // Only run in multisite and if user is logged in.
+ if ( ! is_multisite() || ! is_user_logged_in() || is_network_admin() ) {
+ return;
+ }
+
+ $blogs = get_blogs_of_user(get_current_user_id());
+
+ // If user has blogs and current blog is not in their list, show our custom message.
+ if ( wp_list_filter($blogs, array('userblog_id' => get_current_blog_id())) ) {
+ return;
+ }
+
+ $blog_name = get_bloginfo('name');
+
+ if ( empty($blogs) ) {
+ wp_die(
+ sprintf(
+ /* translators: 1: Site title. */
+ __('You attempted to access the "%1$s" dashboard, but you do not currently have privileges on this site. If you believe you should be able to access the "%1$s" dashboard, please contact your network administrator.'), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ $blog_name // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ ),
+ 403
+ );
+ }
+
+ $output = '
' . sprintf( + /* translators: 1: Site title. */ + __('You attempted to access the "%1$s" dashboard, but you do not currently have privileges on this site. If you believe you should be able to access the "%1$s" dashboard, please contact your network administrator.'), + $blog_name + ) . '
'; + $output .= '' . __('If you reached this screen by accident and meant to visit one of your own sites, here are some shortcuts to help you find your way.') . '
'; + + $output .= '| ' . esc_html($blog->blogname) . ' | '; + $output .= '' . __('Visit Dashboard') . ' | ' . + '' . __('View Site') . ' | '; + $output .= '