Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php
+++ b/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php
@@ -5,12 +5,12 @@
* Description: A developer-first REST API to decouple WooCommerce on the frontend to help build modern and scalable storefronts.
* Author: CoCart Headless, LLC
* Author URI: https://cocartapi.com
- * Version: 4.8.4
+ * Version: 4.9.0
* Text Domain: cart-rest-api-for-woocommerce
* Domain Path: /languages/
- * Requires at least: 6.3
- * Tested up to: 6.9
- * Requires PHP: 7.4
+ * Requires at least: 6.7
+ * Tested up to: 7.0
+ * Requires PHP: 8.2
* Requires Plugins: woocommerce
*
* Copyright: CoCart Headless, LLC
--- a/cart-rest-api-for-woocommerce/includes/class-cocart.php
+++ b/cart-rest-api-for-woocommerce/includes/class-cocart.php
@@ -1,11 +1,12 @@
<?php
/**
- * CoCart core setup.
+ * CoCart Community setup.
*
* @author Sébastien Dumont
* @package CoCart
* @since 2.6.0
- * @version 4.6.2
+ * @version 4.9.0
+ * @license GPL-3.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -28,7 +29,7 @@
*
* @var string
*/
- public static $version = '4.8.4';
+ public static $version = '4.9.0';
/**
* CoCart Database Schema version.
@@ -44,7 +45,20 @@
public static $db_version = '4.3.23';
/**
- * Required WordPress Version
+ * Tested up to WordPress version.
+ *
+ * @access public
+ *
+ * @static
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @var string
+ */
+ public static $tested_up_to_wp = '6.9';
+
+ /**
+ * Required WordPress version.
*
* @access public
*
@@ -54,10 +68,10 @@
*
* @var string
*/
- public static $required_wp = '6.3';
+ public static $required_wp = '6.7';
/**
- * Required WooCommerce Version
+ * Required WooCommerce version.
*
* @access public
*
@@ -70,7 +84,7 @@
public static $required_woo = '9.0';
/**
- * Required PHP Version
+ * Required PHP version.
*
* @access public
*
@@ -78,7 +92,7 @@
*
* @var string
*/
- public static $required_php = '7.4';
+ public static $required_php = '8.2';
/**
* Cloning is forbidden.
@@ -114,8 +128,6 @@
public static function init() {
self::setup_constants();
self::includes();
- self::include_extension_compatibility();
- self::include_third_party();
// Install CoCart upon activation.
register_activation_hook( COCART_FILE, array( __CLASS__, 'install_cocart' ) );
@@ -129,6 +141,9 @@
add_action( 'woocommerce_loaded', array( __CLASS__, 'woocommerce' ) );
add_action( 'woocommerce_loaded', array( __CLASS__, 'background_updater' ) );
+ // Load integrations (compatibility modules + third-party plugin support).
+ add_action( 'init', array( __CLASS__, 'include_integrations' ), 5 );
+
// Load translation files.
add_action( 'init', array( __CLASS__, 'load_plugin_textdomain' ), 0 );
@@ -151,13 +166,17 @@
* @static
*
* @since 1.2.0 Introduced.
- * @version 4.5.0
+ * @version 4.9.0
*/
public static function setup_constants() {
self::define( 'COCART_ABSPATH', dirname( COCART_FILE ) . '/' );
self::define( 'COCART_PLUGIN_BASENAME', plugin_basename( COCART_FILE ) );
self::define( 'COCART_VERSION', self::$version );
self::define( 'COCART_DB_VERSION', self::$db_version );
+ self::define( 'COCART_TESTED_WP', self::$tested_up_to_wp );
+ self::define( 'COCART_REQUIRED_WP', self::$required_wp );
+ self::define( 'COCART_REQUIRED_PHP', self::$required_php );
+ self::define( 'COCART_REQUIRED_WOO', self::$required_woo );
self::define( 'COCART_SLUG', 'cart-rest-api-for-woocommerce' );
self::define( 'COCART_URL_PATH', untrailingslashit( plugins_url( '/', COCART_FILE ) ) );
self::define( 'COCART_FILE_PATH', untrailingslashit( plugin_dir_path( COCART_FILE ) ) );
@@ -168,10 +187,9 @@
self::define( 'COCART_REVIEW_URL', 'https://testimonial.to/cocart' );
self::define( 'COCART_SUGGEST_FEATURE', 'https://cocartapi.com/suggest-a-feature/' );
self::define( 'COCART_COMMUNITY_URL', 'https://cocartapi.com/community/' );
- self::define( 'COCART_DOCUMENTATION_URL', 'https://cocartapi.com/docs/' );
+ self::define( 'COCART_DOCUMENTATION_URL', 'https://docs.cocartapi.com/' );
self::define( 'COCART_TRANSLATION_URL', 'https://translate.cocartapi.com/projects/cart-rest-api-for-woocommerce/' );
self::define( 'COCART_REPO_URL', 'https://github.com/co-cart/co-cart' );
- self::define( 'COCART_NEXT_VERSION', '5.0.0' );
} // END setup_constants()
/**
@@ -287,6 +305,9 @@
include_once __DIR__ . '/cocart-deprecated-functions.php';
include_once __DIR__ . '/cocart-formatting-functions.php';
+ // Integration Registry — must load before compatibility and third-party modules.
+ include_once __DIR__ . '/classes/class-cocart-integrations.php';
+
// Core classes.
require_once __DIR__ . '/classes/class-cocart-helpers.php';
require_once __DIR__ . '/classes/class-cocart-install.php';
@@ -313,7 +334,7 @@
*/
if (
! defined( 'COCART_WHITE_LABEL' ) ||
- false === COCART_WHITE_LABEL && is_admin() ||
+ ( false === COCART_WHITE_LABEL && is_admin() ) ||
( defined( 'WP_CLI' ) && WP_CLI )
) {
require_once __DIR__ . '/classes/admin/class-cocart-admin.php';
@@ -341,30 +362,17 @@
} // END background_updater()
/**
- * Include extension compatibility.
- *
- * @access public
- *
- * @static
- *
- * @since 3.0.0 Introduced.
- */
- public static function include_extension_compatibility() {
- require_once __DIR__ . '/compatibility/class-cocart-compatibility.php';
- } // END include_extension_compatibility()
-
- /**
- * Include third party support.
+ * Include all integrations (compatibility modules + third-party plugin support).
*
* @access public
*
* @static
*
- * @since 2.8.1 Introduced.
+ * @since 4.9.0 Introduced.
*/
- public static function include_third_party() {
- require_once __DIR__ . '/third-party/class-cocart-third-party.php';
- } // END include_third_party()
+ public static function include_integrations(): void {
+ CoCart_Integrations::load();
+ } // END include_integrations()
/**
* Install CoCart upon activation.
@@ -413,7 +421,7 @@
self::deactivate_plugin();
wp_die(
sprintf(
- /* translators: %1$s: CoCart Core, %2$s: CoCart Plus */
+ /* translators: %1$s: CoCart, %2$s: CoCart Plus */
esc_html__( '%1$s is not required as it is already packaged within %2$s', 'cart-rest-api-for-woocommerce' ),
'CoCart',
'CoCart Plus'
@@ -425,7 +433,7 @@
self::deactivate_plugin();
wp_die(
sprintf(
- /* translators: %1$s: CoCart Core, %2$s: CoCart Pro */
+ /* translators: %1$s: CoCart, %2$s: CoCart Pro */
esc_html__( '%1$s is not required as it is already packaged within %2$s', 'cart-rest-api-for-woocommerce' ),
'CoCart',
'CoCart Pro'
@@ -463,7 +471,13 @@
* @since 4.1.0 Moved REST API classes to load ONLY when the REST API is used.
*/
public static function load_rest_api() {
+ // Prevent CoCart running in the backend should the REST API server be called by another plugin.
+ if ( is_admin() ) {
+ return;
+ }
+
require_once __DIR__ . '/classes/class-cocart-data-exception.php';
+ require_once __DIR__ . '/classes/rest-api/class-cocart-etag.php';
require_once __DIR__ . '/classes/rest-api/class-cocart-cart-callbacks.php';
require_once __DIR__ . '/classes/rest-api/class-cocart-cart-extension.php';
require_once __DIR__ . '/classes/rest-api/class-cocart-response.php';
@@ -477,13 +491,13 @@
/**
* Returns true if we are making a REST API request for CoCart.
*
- * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061.
- *
* @access public
*
* @static
*
* @since 2.1.0 Introduced.
+ * @since 4.2.0 Moved to main class.
+ * @since 4.9.0 Recognize requests made via the WordPress REST API batch endpoint.
*
* @return bool
*/
@@ -492,9 +506,16 @@
return false;
}
- $rest_prefix = trailingslashit( rest_get_url_prefix() );
- $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
- $is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'cocart/' ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $rest_prefix = trailingslashit( rest_get_url_prefix() );
+ $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+
+ $is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'cocart/' ) );
+
+ // Requests sent via the WP REST API batch endpoint share the cart, session and
+ // customer for every sub-request dispatched, so return true to accept them.
+ if ( ! $is_rest_api_request ) {
+ $is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'batch/v1' ) );
+ }
/**
* Filters the REST API requested.
@@ -586,30 +607,23 @@
/**
* Load the plugin translations if any ready.
*
- * Note: the first-loaded translation file overrides any following ones if the same translation is present.
+ * Note: the first-loaded translation file takes priority over any following ones if the same translation is present.
*
* Locales found in:
* - WP_LANG_DIR/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce-LOCALE.mo
- * - WP_LANG_DIR/plugins/cart-rest-api-for-woocommerce-LOCALE.mo
+ * - PLUGIN_DIR/languages/cart-rest-api-for-woocommerce-LOCALE.mo
*
* @access public
*
* @static
*
- * @since 1.0.0 Introduced.
- * @version 4.3.7
+ * @since 1.0.0 Introduced.
*/
public static function load_plugin_textdomain() {
- if ( function_exists( 'determine_locale' ) ) {
- $locale = determine_locale();
- } else {
- $locale = is_admin() ? get_user_locale() : get_locale();
- }
-
- $locale = apply_filters( 'plugin_locale', $locale, COCART_SLUG ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ $locale = determine_locale();
unload_textdomain( COCART_SLUG );
load_textdomain( COCART_SLUG, WP_LANG_DIR . '/' . COCART_SLUG . '/' . COCART_SLUG . '-' . $locale . '.mo' );
- load_plugin_textdomain( COCART_SLUG, false, plugin_basename( dirname( COCART_FILE ) ) . '/languages' ); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound
+ load_textdomain( COCART_SLUG, plugin_dir_path( COCART_FILE ) . 'languages/' . COCART_SLUG . '-' . $locale . '.mo' );
} // END load_plugin_textdomain()
} // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-action-links.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-action-links.php
@@ -57,6 +57,13 @@
$page = admin_url( 'admin.php' );
+ /**
+ * Filter to enable or disable the setup wizard.
+ *
+ * @since 2.6.0 Introduced.
+ *
+ * @param bool $enable_setup_wizard True to enable, false to disable.
+ */
if ( apply_filters( 'cocart_enable_setup_wizard', true ) && current_user_can( 'manage_options' ) ) {
$action_links['setup-wizard'] = '<a href="' . add_query_arg(
array(
@@ -77,21 +84,6 @@
'CoCart'
) . '">' . esc_attr__( 'Support', 'cart-rest-api-for-woocommerce' ) . '</a>';
- // Only show upgrade option if neither CoCart Plus, Pro or above is found.
- if ( apply_filters( 'cocart_show_upgrade_action_link', true ) ) {
- $store_url = CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, COCART_STORE_URL . 'why-upgrade/' ) );
-
- $action_links['upgrade'] = sprintf(
- '<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferrer" style="color: #6032b0; font-weight: 600;">%2$s</a>',
- esc_url( $store_url ),
- sprintf(
- /* translators: %s: CoCart */
- esc_attr__( 'Upgrade %s', 'cart-rest-api-for-woocommerce' ),
- 'CoCart'
- )
- );
- }
-
$links = array_merge( $links, $action_links );
return $links;
@@ -115,34 +107,57 @@
return $metadata;
}
- if ( plugin_basename( COCART_FILE ) === $file ) {
- $row_meta = array(
- 'community' => '<a href="' . esc_url( COCART_COMMUNITY_URL ) . '" title="' . sprintf(
- /* translators: %1$s: CoCart, %2$s :Discord */
- esc_attr__( 'Join %1$s Community on %2$s', 'cart-rest-api-for-woocommerce' ),
- 'CoCart',
- 'Discord'
- ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Join Community', 'cart-rest-api-for-woocommerce' ) . '</a>',
- 'docs' => '<a href="' . esc_url( COCART_DOCUMENTATION_URL ) . '" title="' . sprintf(
- /* translators: %s: CoCart */
- esc_attr__( 'View %s Documentation', 'cart-rest-api-for-woocommerce' ),
- 'CoCart'
- ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Documentation', 'cart-rest-api-for-woocommerce' ) . '</a>',
- 'translate' => '<a href="' . CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, esc_url( COCART_TRANSLATION_URL ) ) ) . '" title="' . sprintf(
- /* translators: %s: CoCart */
- esc_attr__( 'Translate %s', 'cart-rest-api-for-woocommerce' ),
- 'CoCart'
- ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Translate', 'cart-rest-api-for-woocommerce' ) . '</a>',
- 'review' => '<a href="' . esc_url( COCART_REVIEW_URL ) . '" title="' . sprintf(
+ if ( plugin_basename( COCART_FILE ) !== $file ) {
+ return $metadata;
+ }
+
+ $row_meta = array(
+ 'community' => '<a href="' . esc_url( COCART_COMMUNITY_URL ) . '" title="' . sprintf(
+ /* translators: %1$s: CoCart, %2$s :Discord */
+ esc_attr__( 'Join %1$s Community on %2$s', 'cart-rest-api-for-woocommerce' ),
+ 'CoCart',
+ 'Discord'
+ ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Join Community', 'cart-rest-api-for-woocommerce' ) . '</a>',
+ 'docs' => '<a href="' . esc_url( COCART_DOCUMENTATION_URL ) . '" title="' . sprintf(
+ /* translators: %s: CoCart */
+ esc_attr__( 'View %s Documentation', 'cart-rest-api-for-woocommerce' ),
+ 'CoCart'
+ ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Documentation', 'cart-rest-api-for-woocommerce' ) . '</a>',
+ 'translate' => '<a href="' . esc_url( COCART_TRANSLATION_URL ) . '" title="' . sprintf(
+ /* translators: %s: CoCart */
+ esc_attr__( 'Translate %s', 'cart-rest-api-for-woocommerce' ),
+ 'CoCart'
+ ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Translate', 'cart-rest-api-for-woocommerce' ) . '</a>',
+ 'review' => '<a href="' . esc_url( COCART_REVIEW_URL ) . '" title="' . sprintf(
+ /* translators: %s: CoCart */
+ esc_attr__( 'Submit a review for %s', 'cart-rest-api-for-woocommerce' ),
+ 'CoCart'
+ ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Leave a Review', 'cart-rest-api-for-woocommerce' ) . '</a>',
+ );
+
+ /**
+ * Filter to show or hide the upgrade action link.
+ *
+ * @since 2.1.0 Introduced.
+ *
+ * @param bool $show True to show the upgrade link, false to hide it.
+ */
+ if ( apply_filters( 'cocart_show_upgrade_action_link', true ) ) {
+ $store_url = CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, COCART_STORE_URL . 'why-upgrade/' ) );
+
+ $row_meta['upgrade-cocart'] = sprintf(
+ '<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferrer" style="color: #6032b0; font-weight: 600;">%2$s</a>',
+ esc_url( $store_url ),
+ sprintf(
/* translators: %s: CoCart */
- esc_attr__( 'Submit a review for %s', 'cart-rest-api-for-woocommerce' ),
+ esc_attr__( 'Upgrade %s', 'cart-rest-api-for-woocommerce' ),
'CoCart'
- ) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Leave a Review', 'cart-rest-api-for-woocommerce' ) . '</a>',
+ )
);
-
- $metadata = array_merge( $metadata, $row_meta );
}
+ $metadata = array_merge( $metadata, $row_meta );
+
return $metadata;
} // END plugin_row_meta()
} // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-assets.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-assets.php
@@ -27,11 +27,40 @@
// Registers and enqueue Stylesheets.
add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );
+ // Registers and enqueue Scripts.
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
+
// Adds admin body classes.
add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
} // END __construct()
/**
+ * Registers and enqueues scripts for CoCart admin pages.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ */
+ public function admin_scripts() {
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+ $screen = get_current_screen();
+ $screen_id = $screen ? $screen->id : '';
+
+ if ( strpos( $screen_id, 'cocart-settings' ) !== false ) {
+ $script_path = 'assets/js/admin/cocart-admin-settings' . $suffix . '.js';
+
+ wp_register_script(
+ 'cocart-admin-settings',
+ COCART_URL_PATH . '/' . $script_path,
+ array( 'jquery' ),
+ CoCart::get_file_version( COCART_ABSPATH . $script_path ),
+ true
+ );
+ wp_enqueue_script( 'cocart-admin-settings' );
+ }
+ } // END admin_scripts()
+
+ /**
* Registers and enqueue Stylesheets.
*
* @access public
@@ -46,15 +75,15 @@
if ( CoCart_Helpers::is_cocart_admin_page() ) {
$style_path = 'assets/css/admin/cocart.css';
- wp_register_style( COCART_SLUG . '-admin', COCART_URL_PATH . '/' . $style_path, array(), CoCart::get_file_version( COCART_ABSPATH . $style_path ) );
- wp_enqueue_style( COCART_SLUG . '-admin' );
- wp_style_add_data( COCART_SLUG . '-admin', 'rtl', 'replace' );
+ wp_register_style( 'cocart-admin', COCART_URL_PATH . '/' . $style_path, array(), CoCart::get_file_version( COCART_ABSPATH . $style_path ) );
+ wp_enqueue_style( 'cocart-admin' );
+ wp_style_add_data( 'cocart-admin', 'rtl', 'replace' );
if ( $suffix ) {
- wp_style_add_data( COCART_SLUG . '-admin', 'suffix', '.min' );
+ wp_style_add_data( 'cocart-admin', 'suffix', '.min' );
}
}
if ( $suffix ) {
- wp_style_add_data( COCART_SLUG . '-admin', 'suffix', '.min' );
+ wp_style_add_data( 'cocart-admin', 'suffix', '.min' );
}
} // END admin_styles()
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-menus.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-menus.php
@@ -4,7 +4,7 @@
*
* @author Sébastien Dumont
* @package CoCartAdminMenus
- * @since 2.0.0
+ * @since 2.0.0 Introduced.
* @version 3.1.0
* @license GPL-2.0+
*/
@@ -22,7 +22,8 @@
* A list with the objects that handle submenu pages
*
* @access public
- * @var array
+ *
+ * @var array
*/
public $submenu_pages = array();
@@ -43,15 +44,25 @@
/**
* Add CoCart to the menu.
*
- * @access public
- * @since 2.0.0
+ * @access public
+ *
+ * @since 2.0.0 Introduced.
* @version 3.10.5
*/
public function add_main_menu_page() {
+ /**
+ * Filter the capability required to access CoCart admin screens.
+ *
+ * @since 2.0.0 Introduced.
+ *
+ * @param string $capability Required capability.
+ */
+ $screen_capability = apply_filters( 'cocart_screen_capability', 'manage_options' );
+
add_menu_page(
'CoCart',
'CoCart',
- apply_filters( 'cocart_screen_capability', 'manage_options' ),
+ $screen_capability,
'cocart',
function () {
return '';
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-notices.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-notices.php
@@ -7,7 +7,7 @@
* @author Sébastien Dumont
* @package CoCartAdminNotices
* @since 1.2.0 Introduced.
- * @version 4.3.25
+ * @version 4.9.0
* @license GPL-2.0+
*/
@@ -62,7 +62,6 @@
'check_wc' => 'check_woocommerce_notice',
'plugin_review' => 'plugin_review_notice',
'check_beta' => 'check_beta_notice',
- 'upgrade_warning' => 'upgrade_warning_notice',
'base_tables_missing' => 'base_tables_missing_notice',
'setup_wizard' => 'setup_wizard_notice',
);
@@ -241,7 +240,11 @@
/**
* Hook: Hide a CoCart notice.
*
- * Example: `cocart_hide_plugin_review_notice'
+ * @since 3.0.0 Introduced.
+ *
+ * Example: `cocart_hide_plugin_review_notice`
+ *
+ * @param string $notice_name The name of the notice being hidden.
*/
do_action( "cocart_hide_{$notice_name}_notice" );
}
@@ -340,7 +343,11 @@
/**
* Hook: Hide a CoCart notice.
*
- * Example: `cocart_hide_plugin_review_notice'
+ * @since 3.0.0 Introduced.
+ *
+ * Example: `cocart_hide_plugin_review_notice`
+ *
+ * @param string $notice_name The name of the notice being hidden.
*/
do_action( "cocart_hide_{$notice_name}_notice" );
} // END hide_notice()
@@ -394,6 +401,14 @@
}
foreach ( $notices as $notice ) {
+ /**
+ * Filter to show or hide a specific admin notice.
+ *
+ * @since 3.0.0 Introduced.
+ *
+ * @param bool $show_notice True to show, false to hide.
+ * @param string $notice The notice name.
+ */
if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'cocart_show_admin_notice', true, $notice ) ) {
add_action( 'admin_notices', array( $this, self::$core_notices[ $notice ] ) );
} else {
@@ -454,6 +469,13 @@
* @return void
*/
public function base_tables_missing_notice() {
+ /**
+ * Filter to hide the base tables missing notice.
+ *
+ * @since 3.0.0 Introduced.
+ *
+ * @param mixed $dismissed Whether the notice has been dismissed.
+ */
$notice_dismissed = apply_filters(
'cocart_hide_base_tables_missing_nag',
get_user_meta( get_current_user_id(), 'dismissed_cocart_base_tables_missing_notice', true )
@@ -495,6 +517,8 @@
*
* @access public
*
+ * @deprecated 4.9.0 Deprecated as the next major release is now expected to be 5.0.0 and the notice is no longer relevant.
+ *
* @since 1.2.3 Introduced.
* @since 3.10.4 Check how long CoCart has been installed before showing.
*
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-settings-renderer.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-settings-renderer.php
@@ -0,0 +1,636 @@
+<?php
+/**
+ * Settings page renderer.
+ *
+ * Renders tabs, panels, and individual field rows from a schema returned by
+ * CoCart_Admin_Settings_Page::get_sections() and get_fields().
+ *
+ * @author Sébastien Dumont
+ * @package CoCartAdmin
+ * @since 4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class CoCart_Admin_Settings_Renderer {
+
+ /**
+ * Detect whether a developer has registered their own callback on a filter,
+ * ignoring CoCart's own settings-bridge callbacks (priority 5).
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param string $hook_name Filter hook to inspect.
+ *
+ * @return bool True if an external callback is registered.
+ */
+ public function has_external_filter( $hook_name ) {
+ global $wp_filter;
+
+ if ( empty( $wp_filter[ $hook_name ] ) ) {
+ return false;
+ }
+
+ foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $callbacks ) {
+ if ( 5 === $priority ) {
+ continue;
+ }
+ if ( ! empty( $callbacks ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ } // END has_external_filter()
+
+ /**
+ * Collect registered callbacks for a filter hook, skipping priority 5.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param string $hook_name Filter hook to inspect.
+ *
+ * @return array<int, array<string, mixed>> Each entry: priority, label, file, line.
+ */
+ public function get_filter_locations( $hook_name ) {
+ global $wp_filter;
+
+ $locations = array();
+
+ if ( empty( $wp_filter[ $hook_name ] ) ) {
+ return $locations;
+ }
+
+ foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $callbacks ) {
+ if ( 5 === $priority ) {
+ continue;
+ }
+ foreach ( $callbacks as $callback ) {
+ $fn = $callback['function'];
+ try {
+ if ( is_array( $fn ) ) {
+ $ref = new ReflectionMethod( $fn[0], $fn[1] );
+ } elseif ( $fn instanceof Closure ) {
+ $ref = new ReflectionFunction( $fn );
+ } elseif ( is_string( $fn ) ) {
+ $ref = new ReflectionFunction( $fn );
+ } else {
+ continue;
+ }
+ $file = $ref->getFileName();
+ $line = $ref->getStartLine();
+ $label = is_array( $fn )
+ ? ( is_object( $fn[0] ) ? get_class( $fn[0] ) : $fn[0] ) . '::' . $fn[1]
+ : ( is_string( $fn ) ? $fn : '{closure}' );
+
+ $locations[] = array(
+ 'priority' => $priority,
+ 'label' => $label,
+ 'file' => $file ? str_replace( ABSPATH, '', $file ) : '',
+ 'line' => $line ? $line : null,
+ );
+ } catch ( ReflectionException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // Skip callbacks we cannot introspect.
+ }
+ }
+ }
+
+ return $locations;
+ } // END get_filter_locations()
+
+ /**
+ * Render the tab navigation bar.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+ */
+ public function render_tabs( $sections ) {
+ $default_tab = $this->get_default_tab( $sections );
+
+ foreach ( $sections as $key => $section ) {
+ $id = esc_attr( $key );
+ $label = $section['label'];
+ $selected = ( $key === $default_tab ) ? 'true' : 'false';
+ $class = ( $key === $default_tab ) ? ' is-active' : '';
+ $badge = '';
+
+ if ( ! empty( $section['preview'] ) ) {
+ $badge = ' <span class="cocart-settings-tab-badge"><span class="dashicons dashicons-lock"></span></span>';
+ }
+
+ printf(
+ '<button type="button" class="cocart-settings-tab%s" role="tab" aria-selected="%s" aria-controls="cocart-tab-%s" data-tab="%s">%s%s</button>',
+ esc_attr( $class ),
+ esc_attr( $selected ),
+ esc_attr( $id ),
+ esc_attr( $id ),
+ wp_kses_post( $label ),
+ wp_kses_post( $badge )
+ );
+ }
+ } // END render_tabs()
+
+ /**
+ * Determine which section should be active by default.
+ *
+ * Skips sections flagged as a preview (e.g. "General") so they are only
+ * shown when explicitly selected, falling back to the first section.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+ *
+ * @return string Section ID to activate by default.
+ */
+ protected function get_default_tab( $sections ) {
+ foreach ( $sections as $key => $section ) {
+ if ( empty( $section['preview'] ) ) {
+ return $key;
+ }
+ }
+
+ return array_key_first( $sections );
+ } // END get_default_tab()
+
+ /**
+ * Render all tab panels.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+ * @param array<string, array<string, mixed>> $fields Fields from get_fields().
+ * @param array<string, mixed> $settings Saved cocart_settings option.
+ */
+ public function render_panels( $sections, $fields, $settings ) {
+ $default_tab = $this->get_default_tab( $sections );
+
+ foreach ( $sections as $key => $section ) {
+ $section_id = $key;
+ $section_fields = isset( $fields[ $section_id ] ) ? $fields[ $section_id ] : array();
+
+ $is_active = ( $section_id === $default_tab );
+ $active = $is_active ? ' is-active' : '';
+ printf(
+ '<div id="cocart-tab-%s" class="cocart-settings-panel%s" role="tabpanel"%s>',
+ esc_attr( $section_id ),
+ esc_attr( $active ),
+ ( $is_active ? '' : ' hidden' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ );
+ echo '<table class="cocart-settings-table"><tbody>';
+
+ foreach ( $section_fields as $id => $field ) {
+ $value = ! empty( $field['disabled'] )
+ ? ( $field['default'] ?? '' )
+ : $this->get_field_value( $id, $section_id, $settings, $field['default'] ?? '' );
+ $this->render_field_row( $field, $value, $section_id, $id );
+ }
+
+ echo '</tbody></table>';
+ echo '</div><!-- /cocart-tab-' . esc_html( $section_id ) . ' -->';
+ }
+ } // END render_panels()
+
+ /**
+ * Return the saved value for a field, falling back to default.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param string $id Field ID (array key from get_fields()).
+ * @param string $section Section ID.
+ * @param array<string, mixed> $settings Saved cocart_settings option.
+ * @param mixed $fallback Default value from the field definition.
+ *
+ * @return mixed
+ */
+ public function get_field_value( $id, $section, $settings, $fallback = '' ) {
+ if ( is_int( $fallback ) ) {
+ return isset( $settings[ $section ][ $id ] ) ? (int) $settings[ $section ][ $id ] : $fallback;
+ }
+
+ return ! empty( $settings[ $section ][ $id ] ) ? $settings[ $section ][ $id ] : $fallback;
+ } // END get_field_value()
+
+ /**
+ * Check whether a field's filter is currently active and compute the effective value.
+ *
+ * Returns an array: [ 'is_filtered' => bool, 'effective_value' => mixed ]
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition (may have 'filter' key).
+ * @param mixed $value Current saved value.
+ * @param string $id Field ID (array key from get_fields()).
+ *
+ * @return array<string, mixed>
+ */
+ public function get_filter_state( $field, $value, $id = '' ) {
+ if ( empty( $field['filter']['hook'] ) ) {
+ return array(
+ 'is_filtered' => false,
+ 'effective_value' => $value,
+ );
+ }
+
+ $hook = $field['filter']['hook'];
+ $is_filtered = $this->has_external_filter( $hook );
+
+ if ( ! $is_filtered ) {
+ return array(
+ 'is_filtered' => false,
+ 'effective_value' => $value,
+ );
+ }
+
+ // For inverted boolean filters (disable_* hooks): the filter returns true to disable.
+ if ( ! empty( $field['filter']['invert'] ) ) {
+ // Default passed to filter: the logical inverse of the saved "enabled" value.
+ $saved_enabled = ( 'yes' === $value );
+ /**
+ * Filters whether the feature controlled by this field is disabled.
+ *
+ * @since 4.9.0
+ */
+ $filter_result = apply_filters( $hook, ! $saved_enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ $effective = $filter_result ? 'no' : 'yes';
+ } elseif ( 'checkbox' === $field['type'] && 'cocart_does_product_allow_price_change' === $hook ) {
+ // Name Your Price: filter takes extra params for per-product overrides.
+ $saved_enabled = ( 'yes' === $value );
+ /**
+ * Filters whether a cart item is allowed to override the product price.
+ *
+ * @since 4.9.0
+ */
+ $filter_result = apply_filters( $hook, $saved_enabled, array(), null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ $effective = $filter_result ? 'yes' : 'no';
+ } elseif ( 'checkbox' === $field['type'] ) {
+ $saved_enabled = ( 'yes' === $value );
+ /**
+ * Filters whether the feature controlled by this field is enabled.
+ *
+ * @since 4.9.0
+ */
+ $filter_result = apply_filters( $hook, $saved_enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ $effective = $filter_result ? 'yes' : 'no';
+ } elseif ( 'url' === $field['type'] && 'allowed_origin' === $id ) {
+ /**
+ * Filters the allowed HTTP origin result.
+ *
+ * @since 4.9.0
+ */
+ $effective = apply_filters( $hook, $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ } elseif ( 'number' === $field['type'] && 'cocart_cart_expiration' === $hook ) {
+ $is_logged_in = ( 'loggedin_expiration_days' === $id );
+ /**
+ * Filters the cart expiration time in seconds.
+ *
+ * @since 4.9.0
+ */
+ $filter_result = (int) apply_filters( $hook, (int) $value * DAY_IN_SECONDS, $is_logged_in ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+ $effective = (int) round( $filter_result / DAY_IN_SECONDS );
+ } else {
+ $effective = $value;
+ }
+
+ return array(
+ 'is_filtered' => true,
+ 'effective_value' => $effective,
+ );
+ } // END get_filter_state()
+
+ /**
+ * Render a single `<tr>` for a field.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition.
+ * @param mixed $value Saved or default value.
+ * @param string $section Section ID.
+ * @param string $id Field ID (array key from get_fields()).
+ */
+ public function render_field_row( $field, $value, $section, $id ) {
+ $disabled = ! empty( $field['disabled'] );
+
+ $is_filtered = false;
+ $locked = $disabled;
+
+ if ( ! $disabled ) {
+ $filter_state = $this->get_filter_state( $field, $value, $id );
+ $is_filtered = $filter_state['is_filtered'];
+ $value = $filter_state['effective_value'];
+ $locked = $is_filtered && ! empty( $field['filter']['locked'] );
+ }
+
+ echo '<tr>';
+ echo '<th scope="row">';
+ $this->render_field_label( $field, $section, $id );
+ echo '</th>';
+ echo '<td>';
+ $this->render_field_input( $field, $value, $locked, $section, $id );
+ if ( 'custom' !== $field['type'] ) {
+ $this->render_field_description( $field );
+ $this->render_field_docs_link( $field );
+ }
+ if ( $is_filtered && false !== ( $field['filter']['notice'] ?? true ) ) {
+ $this->render_filter_notice( $field );
+ }
+ if ( $disabled ) {
+ $this->render_disabled_notice();
+ }
+ echo '</td>';
+ echo '</tr>';
+ } // END render_field_row()
+
+ /**
+ * Render the `<label>` element in the `<th>`.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition.
+ * @param string $section Section ID.
+ * @param string $id Field ID (array key from get_fields()).
+ */
+ protected function render_field_label( $field, $section, $id ) {
+ $has_id = ! in_array( $field['type'], array( 'checkbox', 'custom' ), true );
+ if ( $has_id ) {
+ $input_id = 'cocart-' . esc_attr( $section ) . '-' . esc_attr( str_replace( '_', '-', $id ) );
+ printf( '<label for="%s">%s</label>', esc_attr( $input_id ), wp_kses_post( $field['label'] ) );
+ } else {
+ printf( '<label>%s</label>', wp_kses_post( $field['label'] ) );
+ }
+ } // END render_field_label()
+
+ /**
+ * Render the input control(s) for a field.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition.
+ * @param mixed $value Current value.
+ * @param bool $locked Whether the field is locked by a filter or marked disabled.
+ * @param string $section Section ID.
+ * @param string $id Field ID (array key from get_fields()).
+ */
+ protected function render_field_input( $field, $value, $locked, $section, $id ) {
+ $name = $section . '_' . $id;
+ $input_id = 'cocart-' . $section . '-' . str_replace( '_', '-', $id );
+ $type = $field['type'];
+ $placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : '';
+ $disabled = ! empty( $field['disabled'] );
+
+ switch ( $type ) {
+ case 'text':
+ case 'url':
+ if ( 'url' === $type && 'allowed_origin' === $id ) {
+ $this->render_site_origin_notice();
+ }
+ printf(
+ '<input type="%s" id="%s" name="%s" class="regular-text" value="%s" placeholder="%s" style="width:25em;" %s>',
+ esc_attr( $type ),
+ esc_attr( $input_id ),
+ esc_attr( $name ),
+ esc_attr( (string) $value ),
+ esc_attr( $placeholder ),
+ disabled( $disabled, true, false )
+ );
+ break;
+
+ case 'textarea':
+ $readonly = $locked ? ' readonly' : '';
+ printf(
+ '<textarea id="%s" name="%s" rows="5" class="regular-text" placeholder="%s" style="width:25em;"%s %s>%s</textarea>',
+ esc_attr( $input_id ),
+ esc_attr( $name ),
+ esc_attr( $placeholder ),
+ $readonly, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ disabled( $disabled, true, false ),
+ esc_textarea( (string) $value )
+ );
+ break;
+
+ case 'number':
+ $min = isset( $field['min'] ) ? ' min="' . esc_attr( $field['min'] ) . '"' : '';
+ $max = isset( $field['max'] ) ? ' max="' . esc_attr( $field['max'] ) . '"' : '';
+ $step = isset( $field['step'] ) ? ' step="' . esc_attr( $field['step'] ) . '"' : '';
+ printf(
+ '<input type="number" id="%s" name="%s" class="small-text" value="%s"%s%s%s %s>',
+ esc_attr( $input_id ),
+ esc_attr( $name ),
+ esc_attr( (string) $value ),
+ $min, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ $max, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ $step, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ disabled( $disabled, true, false )
+ );
+ break;
+
+ case 'checkbox':
+ $enabled = ( 'yes' === $value );
+ $title = $enabled ? esc_attr__( 'Enabled', 'cart-rest-api-for-woocommerce' ) : esc_attr__( 'Disabled', 'cart-rest-api-for-woocommerce' );
+ $label = $enabled ? esc_html__( 'Enabled', 'cart-rest-api-for-woocommerce' ) : esc_html__( 'Disabled', 'cart-rest-api-for-woocommerce' );
+ $locked_class = $locked ? ' cocart-toggle--locked' : '';
+ printf(
+ '<label class="cocart-toggle%s" title="%s">',
+ esc_attr( $locked_class ),
+ esc_attr( $title ) // Already escaped by esc_attr__() above; esc_attr() is a no-op but satisfies PHPCS.
+ );
+ printf(
+ '<input type="checkbox" class="cocart-settings-toggle" name="%s" data-field="%s" value="yes" %s %s>',
+ esc_attr( $name ),
+ esc_attr( $name ),
+ checked( $value, 'yes', false ),
+ disabled( $locked, true, false )
+ );
+ echo '<span class="cocart-toggle-slider"></span>';
+ printf( '<span class="cocart-status-label">%s</span>', esc_html( $label ) );
+ echo '</label>';
+ break;
+
+ case 'select':
+ $options = $field['options'] ?? array();
+ printf( '<select id="%s" name="%s" %s>', esc_attr( $input_id ), esc_attr( $name ), disabled( $disabled, true, false ) );
+ foreach ( $options as $opt_value => $opt_label ) {
+ printf(
+ '<option value="%s" %s>%s</option>',
+ esc_attr( $opt_value ),
+ selected( $value, $opt_value, false ),
+ esc_html( $opt_label )
+ );
+ }
+ echo '</select>';
+ break;
+
+ case 'readonly':
+ printf(
+ '<input type="text" id="%s" class="regular-text" value="%s" style="width:25em;" readonly>',
+ esc_attr( $input_id ),
+ esc_attr( (string) $value )
+ );
+ break;
+
+ case 'custom':
+ if ( ! empty( $field['render_cb'] ) && is_callable( $field['render_cb'] ) ) {
+ call_user_func( $field['render_cb'], $field, $value );
+ }
+ break;
+ }
+ } // END render_field_input()
+
+ /**
+ * Render a notice stating the site's own origins are always allowed automatically.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ */
+ protected function render_site_origin_notice() {
+ $home_origin = wp_parse_url( home_url() );
+
+ if ( empty( $home_origin['host'] ) ) {
+ return;
+ }
+
+ $site_origin = ( ! empty( $home_origin['scheme'] ) ? $home_origin['scheme'] : 'https' ) . '://' . $home_origin['host'];
+
+ /* translators: %s: Site origin URL. */
+ $message = sprintf( __( 'Your site's own origin (%s) is already allowed automatically.', 'cart-rest-api-for-woocommerce' ), '<code>' . esc_html( $site_origin ) . '</code>' );
+
+ printf( '<p>%s</p>', wp_kses_post( $message ) );
+ } // END render_site_origin_notice()
+
+ /**
+ * Render the description `<p>` if the field has one.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition.
+ */
+ protected function render_field_description( $field ) {
+ if ( ! empty( $field['description'] ) ) {
+ // Description is already translated by the registering code; escape on output.
+ printf( '<p class="description">%s</p>', wp_kses_post( $field['description'] ) );
+ }
+ } // END render_field_description()
+
+ /**
+ * Render the documentation link badge if a URL is set.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition.
+ */
+ protected function render_field_docs_link( $field ) {
+ if ( empty( $field['docs_url'] ) ) {
+ return;
+ }
+ $label = isset( $field['docs_label'] ) ? esc_html( $field['docs_label'] ) : esc_html__( 'Docs', 'cart-rest-api-for-woocommerce' );
+ printf(
+ '<div class="cocart-setting-doc-wrap"><a href="%s" class="button cocart-button-alt" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-external"></span>%s</a></div>',
+ esc_url( $field['docs_url'] ),
+ $label // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ );
+ } // END render_field_docs_link()
+
+ /**
+ * Render the filter-override notice bar below a field.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, mixed> $field Field definition (must have 'filter' key).
+ */
+ public function render_filter_notice( $field ) {
+ if ( empty( $field['filter']['hook'] ) ) {
+ return;
+ }
+ $hook = $field['filter']['hook'];
+ printf(
+ '<p class="cocart-settings-filter-notice"><span class="dashicons dashicons-info-outline"></span>%s<button type="button" class="cocart-filter-info-btn button-link" data-filter="%s">%s</button></p>',
+ wp_kses(
+ /* translators: %s: filter hook name */
+ sprintf( __( 'Controlled by filter <code>%s</code>.', 'cart-rest-api-for-woocommerce' ), $hook ),
+ array( 'code' => array() )
+ ),
+ esc_attr( $hook ),
+ esc_html__( 'More info', 'cart-rest-api-for-woocommerce' )
+ );
+ } // END render_filter_notice()
+
+ /**
+ * Render a notice for fields disabled in Community, with a link
+ * that opens a modal to upgrade CoCart.
+ *
+ * @access protected
+ *
+ * @since 4.9.0 Introduced.
+ */
+ protected function render_disabled_notice() {
+ printf(
+ '<p class="cocart-settings-upgrade-notice"><span class="dashicons dashicons-lock"></span><button type="button" class="cocart-upgrade-info-btn button-link">%s</button></p>',
+ esc_html__( 'Upgrade to unlock', 'cart-rest-api-for-woocommerce' )
+ );
+ } // END render_disabled_notice()
+
+ /**
+ * Build the filter_info array for wp_localize_script — only for hooks that are currently active.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, mixed>> $fields Fields from get_fields().
+ *
+ * @return array<string, array<int, array<string, mixed>>>
+ */
+ public function build_filter_info( $fields ) {
+ $seen = array();
+ $filter_info = array();
+
+ foreach ( $fields as $section_fields ) {
+ foreach ( $section_fields as $field ) {
+ if ( ! empty( $field['disabled'] ) || empty( $field['filter']['hook'] ) ) {
+ continue;
+ }
+ $hook = $field['filter']['hook'];
+ if ( isset( $seen[ $hook ] ) ) {
+ continue;
+ }
+ $seen[ $hook ] = true;
+ if ( $this->has_external_filter( $hook ) ) {
+ $filter_info[ $hook ] = $this->get_filter_locations( $hook );
+ }
+ }
+ }
+
+ return $filter_info;
+ } // END build_filter_info()
+} // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-setup-wizard.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-setup-wizard.php
@@ -71,12 +71,26 @@
return $submenu_pages;
}
+ /**
+ * Filter to enable or disable the setup wizard.
+ *
+ * @since 2.6.0 Introduced.
+ *
+ * @param bool $enable_setup_wizard True to enable, false to disable.
+ */
if ( apply_filters( 'cocart_enable_setup_wizard', true ) ) {
$submenu_pages['setup-wizard'] = array(
'class_name' => 'CoCart_Admin_Setup_Wizard',
'data' => array(
'page_title' => __( 'Setup Wizard', 'cart-rest-api-for-woocommerce' ),
'menu_title' => __( 'Setup Wizard', 'cart-rest-api-for-woocommerce' ),
+ /**
+ * Filter the capability required to access CoCart admin screens.
+ *
+ * @since 2.0.0 Introduced.
+ *
+ * @param string $capability Required capability.
+ */
'capability' => apply_filters( 'cocart_screen_capability', 'manage_options' ),
'menu_slug' => 'cocart-setup',
),
@@ -122,12 +136,14 @@
'view' => array( $this, 'cocart_setup_wizard_store_setup' ),
'handler' => array( $this, 'cocart_setup_wizard_store_setup_save' ),
),
+
/*
'sessions' => array(
'name' => __( 'Sessions', 'cart-rest-api-for-woocommerce' ),
'view' => array( $this, 'cocart_setup_wizard_sessions' ),
'handler' => array( $this, 'cocart_setup_wizard_sessions_save' ),
- ),*/
+ ),
+ */
'ready' => array(
'name' => __( 'Ready!', 'cart-rest-api-for-woocommerce' ),
'view' => array( $this, 'cocart_setup_wizard_ready' ),
@@ -135,6 +151,13 @@
),
);
+ /**
+ * Filter the setup wizard steps.
+ *
+ * @since 2.6.0 Introduced.
+ *
+ * @param array $steps The setup wizard steps.
+ */
$this->steps = apply_filters( 'cocart_setup_wizard_steps', $default_steps );
$this->step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -218,7 +241,14 @@
<a class="cocart-setup-wizard-footer-links" href="<?php echo esc_url( $this->get_next_step_link() ); ?>"><?php esc_html_e( 'Skip this step', 'cart-rest-api-for-woocommerce' ); ?></a>
<?php endif; ?>
- <?php do_action( 'cocart_setup_wizard_footer' ); ?>
+ <?php
+ /**
+ * Hook: Fires in the setup wizard footer.
+ *
+ * @since 2.6.0 Introduced.
+ */
+ do_action( 'cocart_setup_wizard_footer' );
+ ?>
</div>
<?php
@@ -327,7 +357,7 @@
</select>
<p class="cocart-actions step">
- <button class="button button-primary button-large cocart-button" value="<?php esc_attr_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?></button>
+ <button class="button button-primary cocart-button" value="<?php esc_attr_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?></button>
</p>
</form>
<?php
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin.php
@@ -5,7 +5,7 @@
* @author Sébastien Dumont
* @package CoCartAdmin
* @since 1.2.0
- * @version 4.0.0
+ * @version 4.9.0
* @license GPL-2.0+
*/
@@ -44,6 +44,10 @@
* @since 1.2.0 Introduced.
*/
public function includes() {
+ if ( ! is_admin() ) {
+ return;
+ }
+
// Required files.
include_once __DIR__ . '/abstract/abstract-class-submenu-page.php'; // Admin Abstracts.
require_once __DIR__ . '/class-cocart-admin-assets.php'; // Admin Assets.
@@ -61,7 +65,10 @@
include_once __DIR__ . '/woocommerce/class-cocart-wc-admin-system-status.php'; // WooCommerce System Status.
// Pages.
+ require_once __DIR__ . '/class-cocart-admin-settings-renderer.php'; // Settings Renderer.
+ require_once __DIR__ . '/pages/class-cocart-admin-pages-settings.php'; // Settings page.
require_once __DIR__ . '/pages/class-cocart-admin-pages-support.php'; // Support.
+ require_once __DIR__ . '/pages/class-cocart-admin-pages-integrations.php'; // Integrations page.
require_once __DIR__ . '/class-cocart-admin-setup-wizard.php'; // Setup Wizard.
} // END includes()
@@ -112,6 +119,13 @@
}
// Setup wizard redirect.
+ /**
+ * Filter to enable or disable the setup wizard.
+ *
+ * @since 2.6.0 Introduced.
+ *
+ * @param bool $enable_setup_wizard True to enable, false to disable.
+ */
if ( get_transient( '_cocart_activation_redirect' ) && apply_filters( 'cocart_enable_setup_wizard', true ) ) {
$do_redirect = true;
$current_page = isset( $_GET['page'] ) ? wc_clean( sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -121,8 +135,17 @@
$do_redirect = false;
}
+ /**
+ * Filter to prevent the automatic setup wizard redirect.
+ *
+ * @since 2.6.0 Introduced.
+ *
+ * @param bool $prevent True to prevent redirect, false to allow.
+ */
+ $prevent_redirect = apply_filters( 'cocart_prevent_automatic_wizard_redirect', false );
+
// On these pages, or during these events, disable the redirect.
- if ( 'cocart-setup' === $current_page || ! CoCart_Admin_Notices::has_notice( 'setup_wizard' ) || apply_filters( 'cocart_prevent_automatic_wizard_redirect', false ) || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( 'cocart-setup' === $current_page || ! CoCart_Admin_Notices::has_notice( 'setup_wizard' ) || $prevent_redirect || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
delete_transient( '_cocart_activation_redirect' );
$do_redirect = false;
}
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-integrations.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-integrations.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * Admin Page: Integrations page for CoCart.
+ *
+ * @author Sébastien Dumont
+ * @package CoCartAdminPages
+ * @since 4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class CoCart_Admin_Integrations_Page extends CoCart_Submenu_Page {
+
+ /**
+ * Helper init method that runs on parent __construct
+ *
+ * @access protected
+ */
+ protected function init() {
+ add_filter( 'cocart_register_submenu_page', array( $this, 'register_submenu_page' ), 12 );
+ add_action( 'wp_ajax_cocart_toggle_integration', array( $this, 'ajax_toggle_integration' ) );
+ add_action( 'wp_ajax_cocart_search_integrations', array( $this, 'ajax_search_integrations' ) );
+ } // END init()
+
+ /**
+ * Callback for the HTML output for the integrations page.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ */
+ public function output() {
+ ?>
+ <div class="wrap cocart-wrapped cocart-wrapped--wide" role="main">
+ <div class="cocart-content">
+ <?php
+ include_once COCART_ABSPATH . 'includes/classes/admin/views/html-integrations.php';
+ ?>
+ </div>
+ </div>
+ <div class="clear"></div>
+ <?php
+ } // END output()
+
+ /**
+ * Register the admin submenu page.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array $submenu_pages Currently registered submenu pages.
+ *
+ * @return array $submenu_pages All registered submenu pages.
+ */
+ public function register_submenu_page( $submenu_pages ) {
+ if ( ! is_array( $submenu_pages ) ) {
+ return $submenu_pages;
+ }
+
+ /**
+ * Filter the capability required to access CoCart admin screens.
+ *
+ * @since 2.0.0 Introduced.
+ *
+ * @param string $capability Required capability.
+ */
+ $screen_capability = apply_filters( 'cocart_screen_capability', 'manage_options' );
+
+ $submenu_pages['integrations'] = array(
+ 'class_name' => 'CoCart_Admin_Integrations_Page',
+ 'data' => array(
+ 'page_title' => __( 'Integrations', 'cart-rest-api-for-woocommerce' ),
+ 'menu_title' => __( 'Integrations', 'cart-rest-api-for-woocommerce' ),
+ 'capability' => $screen_capability,
+ 'menu_slug' => 'cocart-integrations',
+ ),
+ );
+
+ return $submenu_pages;
+ } // END register_submenu_page()
+
+ /**
+ * AJAX handler: toggle a single integration on or off.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ */
+ public function ajax_toggle_integration() {
+ check_ajax_referer( 'cocart_integrations_nonce', 'nonce' );
+
+ /**
+ * Filter the capability required to access CoCart admin screens.
+ *
+ * @since 2.0.0 Introduced.
+ *
+ * @param string $capability Required capability.
+ */
+ if ( ! current_user_can( apply_filters( 'cocart_screen_capability', 'manage_options' ) ) ) {
+ wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cart-rest-api-for-woocommerce' ) ) );
+ }
+
+ $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
+ $enable = isset( $_POST['enabled'] ) && 'true' === $_POST['enabled'];
+
+ if ( empty( $slug ) || ! array_key_exists( $slug, CoCart_Integrations::get_all() ) ) {
+ wp_send_json_error( array( 'message' => __( 'Unknown integration.', 'cart-rest-api-for-woocommerce' ) ) );
+ }
+
+ if ( $enable ) {
+ CoCart_Integrations::enable( $slug );
+ } else {
+ CoCart_Integrations::disable( $slug );
+ }
+
+ wp_send_json_success( array( 'enabled' => CoCart_Integrations::is_enabled( $slug ) ) );
+ } // END ajax_toggle_integration()
+
+ /**
+ * AJAX handler: search integrations by name or description.
+ *
+ * Returns an HTML fragment of matching integration cards to replace the cards container.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ */
+ public function ajax_search_integrations() {
+ check_ajax_referer( 'cocart_integrations_nonce', 'nonce' );
+
+ /**
+ * Filter the capability required to access CoCart admin screens.
+ *
+ * @since 2.0.0 Introduced.
+ *
+ * @param string $capability Required capability.
+ */
+ if ( ! current_user_can( apply_filters( 'cocart_screen_capability', 'manage_options' ) ) ) {
+ wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cart-rest-api-for-woocommerce' ) ) );
+ }
+
+ $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
+ $integrations = CoCart_Integrations::get_all();
+
+ if ( '' !== $query ) {
+ $integrations = array_filter(
+ $integrations,
+ function ( $args ) use ( $query ) {
+ return false !== stripos( $args['name'], $query ) ||
+ false !== stripos( $args['description'], $query );
+ }
+ );
+ }
+
+ ob_start();
+
+ if ( empty( $integrations ) ) {
+ echo '<p class="cocart-integrations-no-results">' . esc_html__( 'No integrations found.', 'cart-rest-api-for-woocommerce' ) . '</p>';
+ } else {
+ foreach ( $integrations as $slug => $args ) {
+ $enabled = CoCart_Integrations::is_enabled( $slug );
+ $available = CoCart_Integrations::can_be_enabled( $slug );
+ include COCART_ABSPATH . 'includes/classes/admin/views/html-integrations-card.php';
+ }
+ }
+
+ $html = ob_get_clean();
+
+ wp_send_json_success( array( 'html' => $html ) );
+ } // END ajax_search_integrations()
+} // END class
+
+return new CoCart_Admin_Integrations_Page();
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-settings.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-settings.php
@@ -0,0 +1,601 @@
+<?php
+/**
+ * Admin Page: Settings page for CoCart.
+ *
+ * @author Sébastien Dumont
+ * @package CoCartAdminPages
+ * @since 4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+class CoCart_Admin_Settings_Page extends CoCart_Submenu_Page {
+
+ /**
+ * Helper init method that runs on parent __construct
+ *
+ * @access protected
+ */
+ protected function init() {
+ add_filter( 'cocart_register_submenu_page', array( $this, 'register_submenu_page' ), 10 );
+ add_action( 'wp_ajax_cocart_save_settings', array( $this, 'ajax_save_settings' ) );
+ add_action( 'wp_ajax_cocart_install_jwt', array( $this, 'ajax_install_jwt' ) );
+ add_action( 'wp_ajax_cocart_deactivate_jwt', array( $this, 'ajax_deactivate_jwt' ) );
+ } // END init()
+
+ /**
+ * Returns registered settings sections, merged with any registered by add-ons.
+ *
+ * Each section: [ 'id' => string, 'label' => string, 'priority' => int ]
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @return array<string, array<string, mixed>>
+ */
+ public function get_sections() {
+ $sections = array(
+ 'general' => array(
+ 'label' => __( 'General', 'cart-rest-api-for-woocommerce' ),
+ 'priority' => 10,
+ 'preview' => true,
+ ),
+ 'cors' => array(
+ 'label' => __( 'API & CORS', 'cart-rest-api-for-woocommerce' ),
+ 'priority' => 20,
+ ),
+ 'auth' => array(
+ 'label' => __( 'Auth', 'cart-rest-api-for-woocommerce' ),
+ 'priority' => 30,
+ ),
+ 'session' => array(
+ 'label' => __( 'Session', 'cart-rest-api-for-woocommerce' ),
+ 'priority' => 40,
+ ),
+ 'features' => array(
+ 'label' => __( 'Features', 'cart-rest-api-for-woocommerce' ),
+ 'priority' => 50,
+ ),
+ );
+
+ /**
+ * Filter: cocart_settings_sections
+ *
+ * Add-ons use this filter to register new settings tabs.
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, mixed>> $sections Keyed by section ID.
+ */
+ $sections = apply_filters( 'cocart_settings_sections', $sections );
+
+ uasort( $sections, static function ( $a, $b ) {
+ return ( $a['priority'] ?? 10 ) <=> ( $b['priority'] ?? 10 );
+ } );
+
+ return $sections;
+ } // END get_sections()
+
+ /**
+ * Returns registered settings fields, merged with any registered by add-ons.
+ *
+ * Each field must include at minimum: id, section, label, type.
+ * Supported types: text, url, password, textarea, number, checkbox, select, readonly, custom.
+ *
+ * @access public
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @return array<string, array<string, mixed>>
+ */
+ public function get_fields() {
+ $fields = array(
+ 'general' => $this->get_general_fields(),
+ 'cors' => $this->get_cors_fields(),
+ 'auth' => $this->get_auth_fields(),
+ 'session' => $this->get_session_fields(),
+ 'features' => $this->get_features_fields(),
+ );
+
+ /**
+ * Filter: cocart_settings_fields
+ *
+ * Add-ons use this filter to register fields in any section (including their own).
+ *
+ * @since 4.9.0 Introduced.
+ *
+ * @param array<string, array<string, array<string, mixed>>> $fields Keyed by section ID, then field ID.
+ */
+ return apply_filters( 'cocart_settings_fields', $fields );
+ } // END get_fields()
+
+ /**
+ * Returns the default values for all registered fields, keyed by section then field ID.
+ *
+ * Used to popu