File: /home/ukqcurpj/www/wp-content/plugins/paid-memberships-pro/classes/class-pmpro-addons.php
<?php
defined( 'ABSPATH' ) || die( 'File cannot be accessed directly' );
/**
* Class for managing PMPro Addons.
*/
class PMPro_AddOns {
/**
* The single instance of the class.
*
* @var PMPro_AddOns
* @access protected
* @since 3.6
*/
protected static $instance = null;
/**
* Array of Add Ons.
*
* @since 1.8
* @var array
*/
public $addons = array();
/**
* Timestamp of last Add Ons check.
*
* @since 3.6
* @var int
*/
public $addons_timestamp = 0;
/**
* Cache of plugin information to reduce calls to get_plugins().
*
* @since 3.6
* @var array|null
*/
private $cached_plugins = null;
public function __construct() {
$this->addons = get_option( 'pmpro_addons', array() );
$this->addons_timestamp = get_option( 'pmpro_addons_timestamp', false );
add_action( 'admin_init', array( $this, 'admin_hooks' ), 0 ); // Priority 0 to run before other admin_init hooks.
}
/**
* Get the single instance of the class.
*
* @access public
* @since 3.6
* @return PMPro_AddOns
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prevent the instance from being cloned.
*
* @access public
* @since 3.6
* @return void
* @throws Exception If the instance is cloned.
*/
public function __clone() {
throw new Exception( esc_html__( 'PMPro_AddOns instance cannot be cloned', 'paid-memberships-pro' ) );
}
/**
* Prevent the instance from being unserialized.
*
* @access public
* @since 3.6
* @return void
* @throws Exception If the instance is unserialized.
*/
public function __wakeup() {
throw new Exception( esc_html__( 'PMPro_AddOns instance cannot be unserialized', 'paid-memberships-pro' ) );
}
/**
* Admin hooks for managing Add Ons.
*/
public function admin_hooks() {
$this->check_when_updating_plugins();
add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 );
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'update_plugins_filter' ) );
add_filter( 'http_request_args', array( $this, 'http_request_args_for_addons' ), 10, 2 );
add_action( 'update_option_pmpro_license_key', array( $this, 'reset_update_plugins_cache' ), 10, 2 );
// Register AJAX endpoints for add-on actions.
$this->register_ajax_endpoints();
}
/**
* Get the Add On slugs for each category we identify.
*
* @since 2.8.x
*
* @return array $addon_cats An array of plugin categories and plugin slugs within each.
*/
public function get_addon_categories() {
return array(
'popular' => array(
'pmpro-abandoned-cart-recovery',
'pmpro-add-paypal-express',
'pmpro-advanced-levels-shortcode',
'pmpro-approvals',
'pmpro-courses',
'pmpro-cpt',
'pmpro-group-accounts',
'pmpro-import-users-from-csv',
'pmpro-member-directory',
'pmpro-nav-menus',
'pmpro-roles',
'pmpro-set-expiration-dates',
'pmpro-signup-shortcode',
'pmpro-subscription-delays',
),
'association' => array(
'basic-user-avatars',
'pmpro-add-name-to-checkout',
'pmpro-approvals',
'pmpro-donations',
'pmpro-events',
'pmpro-group-accounts',
'pmpro-import-users-from-csv',
'pmpro-member-directory',
'pmpro-membership-card',
'pmpro-membership-manager-role',
'pmpro-pay-by-check',
'pmpro-set-expiration-dates',
'pmpro-shipping',
'pmpro-subscription-delays',
),
'premium_content' => array(
'pmpro-abandoned-cart-recovery',
'pmpro-cpt',
'pmpro-email-confirmation',
'pmpro-events',
'pmpro-google-analytics',
'pmpro-series',
'pmpro-user-pages',
),
'community' => array(
'pmpro-abandoned-cart-recovery',
'pmpro-approvals',
'pmpro-bbpress',
'pmpro-buddypress',
'pmpro-email-confirmation',
'pmpro-import-users-from-csv',
'pmpro-invite-only',
'pmpro-membership-card',
),
'courses' => array(
'lifterlms',
'pmpro-abandoned-cart-recovery',
'pmpro-approvals',
'pmpro-courses',
'pmpro-cpt',
'pmpro-google-analytics',
'pmpro-member-badges',
'pmpro-member-homepages',
'pmpro-testimonials',
'pmpro-user-pages',
),
'directory' => array(
'basic-user-avatars',
'pmpro-approvals',
'pmpro-member-badges',
'pmpro-member-directory',
'pmpro-shipping',
'pmpro-testimonials',
),
'newsletter' => array(
'mailpoet-paid-memberships-pro-add-on',
'convertkit-for-paid-memberships-pro',
'pmpro-add-name-to-checkout',
'pmpro-aweber',
'pmpro-google-analytics',
'pmpro-keap',
'pmpro-mailchimp',
'pmpro-testimonials',
),
'podcast' => array(
'pmpro-akismet',
'pmpro-email-confirmation',
'pmpro-events',
'pmpro-google-analytics',
'pmpro-invite-only',
'pmpro-testimonials',
'seriously-simple-podcasting',
),
'video' => array(
'pmpro-cpt',
'pmpro-email-confirmation',
'pmpro-events',
'pmpro-google-analytics',
'pmpro-invite-only',
'pmpro-testimonials',
),
);
}
/**
* Force update of plugin update data when the PMPro License key is updated
*
* @since 1.8
*
* @param array $args Array of request args.
* @param string $url The URL to be pinged.
* @return array $args Amended array of request args.
*/
public function reset_update_plugins_cache( $old_value, $value ) {
delete_option( 'pmpro_addons_timestamp' );
delete_site_transient( 'update_themes' );
delete_site_transient( 'update_plugins' );
}
/**
* Disables SSL verification to prevent download package failures.
*
* @since 1.8.5
*
* @param array $args Array of request args.
* @param string $url The URL to be pinged.
* @return array $args Amended array of request args.
*/
public function http_request_args_for_addons( $args, $url ) {
// If this is an SSL request and we are performing an upgrade routine, disable SSL verification.
if ( strpos( $url, 'https://' ) !== false && strpos( $url, PMPRO_LICENSE_SERVER ) !== false && strpos( $url, 'download' ) !== false ) {
$args['sslverify'] = false;
}
return $args;
}
/**
* Get a list of installed Add Ons with incorrect folder names.
*
* @since 3.1
*
* @return array $incorrect_folder_names An array of Add Ons with incorrect folder names. The key is the installed folder name, the value is the Add On data.
*/
public function get_add_ons_with_incorrect_folder_names() {
// Make an easily searchable array of installed plugins to reduce computational complexity.
// The key of the array is the plugin filename, the value is the folder name.
$installed_plugins = array();
// Get the cached list of installed plugins.
$cached_plugins = $this->get_installed_plugins();
foreach ( $cached_plugins as $plugin_name => $plugin_data ) {
// Skip plugins that are not in a folder.
if ( false === strpos( $plugin_name, '/' ) ) {
continue;
}
// Add the plugin to the $installed_plugins array.
list( $plugin_folder, $plugin_filename ) = explode( '/', $plugin_name, 2 );
$installed_plugins[ $plugin_filename ] = $plugin_folder;
}
// Set up an array to track Add Ons with wrong folder names.
// The key of the array is the equivalent of $plugin_name above, the value is the Add On data.
$incorrect_folder_names = array();
foreach ( $this->get_addons() as $addon ) {
// Get information about the Add On.
list( $addon_folder, $addon_filename ) = explode( '/', $addon['plugin'], 2 );
// Check if the Add On is installed with an incorrect folder name.
if ( array_key_exists( $addon_filename, $installed_plugins ) && $addon_folder !== $installed_plugins[ $addon_filename ] ) {
// The Add On is installed with the wrong folder name. Add it to the array.
$installed_name = $installed_plugins[ $addon_filename ] . '/' . $addon_filename;
$incorrect_folder_names[ $installed_name ] = $addon;
}
}
return $incorrect_folder_names;
}
/**
* Find a PMPro addon by slug.
*
* @since 1.8.5
*
* @param object $slug The identifying slug for the addon (typically the directory name)
* @return object $addon containing plugin information or false if not found
*/
public function get_addon_by_slug( $slug ) {
$addons = $this->get_addons();
if ( empty( $addons ) ) {
return false;
}
foreach ( $addons as $addon ) {
if ( $addon['Slug'] == $slug ) {
return $addon;
}
}
return false;
}
/**
* Infuse plugin update details when WordPress runs its update checker.
*
* @since 1.8.5
*
* @param object $value The WordPress update object.
* @return object $value Amended WordPress update object on success, default if object is empty.
*/
public function update_plugins_filter( $value ) {
// If no update object exists, return early.
if ( empty( $value ) ) {
return $value;
}
// Get Add On information
$addons = $this->get_addons();
// No Add Ons?
if ( empty( $addons ) ) {
return $value;
}
// Check Add Ons
foreach ( $addons as $addon ) {
// Skip for wordpress.org plugins
if ( empty( $addon['License'] ) || $addon['License'] == 'wordpress.org' ) {
continue;
}
// Get data for plugin
$plugin_file = $addon['Slug'] . '/' . $addon['Slug'] . '.php';
$plugin_file_abs = WP_PLUGIN_DIR . '/' . $plugin_file;
// Couldn't find plugin? Skip
if ( ! file_exists( $plugin_file_abs ) ) {
continue;
} else {
$plugin_data = get_plugin_data( $plugin_file_abs, false, true );
}
// Compare versions
if ( version_compare( $plugin_data['Version'], $addon['Version'], '<' ) ) {
$value->response[ $plugin_file ] = $this->get_plugin_API_object_from_addon( $addon );
$value->response[ $plugin_file ]->new_version = $addon['Version'];
// If we have an icon to show, add it to the response. Otherwise let it show the default icon.
$icon = $this->get_addon_icon( $addon['Slug'] );
if ( ! empty( $icon ) ) {
$value->response[ $plugin_file ]->icons = array( 'default' => esc_url( $icon ) );
}
} else {
$value->no_update[ $plugin_file ] = $this->get_plugin_API_object_from_addon( $addon );
}
}
// Return the update object.
return $value;
}
/**
* Get the list of available addons.
*
* @param bool $force_check Whether to force a check for new addons.
*
* @return array
* @since 3.6
*/
public function get_addons( $force_check = false ) {
$addons = $this->addons;
$addons_timestamp = $this->addons_timestamp;
// Check if forcing a pull from the server
$force_check = ! empty( $_REQUEST['force-check'] ) || $force_check;
// if no addons locally, we need to hit the server
if ( empty( $addons ) || $force_check || current_time( 'timestamp' ) > $addons_timestamp + 86400 ) {
$addons = $this->get_remote_addons();
}
return $addons;
}
/**
* Install an Add On by slug using WordPress core upgraders.
*
* @since 3.2.0
*
* @param string $slug The add on slug (directory name in the repo).
* @return array|WP_Error Result array on success or WP_Error on failure.
*/
public function install( $slug = '' ) {
$slug = sanitize_key( (string) $slug );
if ( empty( $slug ) ) {
return new WP_Error( 'pmpro_addon_install_invalid_slug', __( 'Invalid Add On slug.', 'paid-memberships-pro' ) );
}
if ( ! current_user_can( 'install_plugins' ) ) {
return new WP_Error( 'pmpro_addon_install_cap', __( 'You do not have permission to install plugins.', 'paid-memberships-pro' ) );
}
$addon = $this->get_addon_by_slug( $slug );
if ( empty( $addon ) ) {
return new WP_Error( 'pmpro_addon_not_found', __( 'Add On not found.', 'paid-memberships-pro' ) );
}
// License/access check for premium add ons.
if ( isset( $addon['License'] ) && pmpro_license_type_is_premium( $addon['License'] ) && ! $this->can_download_addon_with_license( $addon['License'] ) ) {
return new WP_Error( 'pmpro_addon_license_required', sprintf( __( 'A valid PMPro %s license is required to install this Add On.', 'paid-memberships-pro' ), ucwords( $addon['License'] ) ) );
}
$package = $this->get_download_package_url( $slug );
if ( is_wp_error( $package ) ) {
return $package;
}
// Prepare filesystem and upgrader.
$fs_ready = $this->ensure_filesystem();
if ( is_wp_error( $fs_ready ) ) {
return $fs_ready;
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$skin = $this->get_upgrader_skin();
$upgrader = new Plugin_Upgrader( $skin );
$result = $upgrader->install( $package );
if ( is_wp_error( $result ) ) {
return $result;
}
if ( ! $result ) {
return new WP_Error( 'pmpro_addon_install_failed', __( 'Installation failed.', 'paid-memberships-pro' ) );
}
// Best-effort resolve installed plugin file.
$plugin_file = $this->resolve_plugin_file( $slug );
return array(
'success' => true,
'action' => 'install',
'plugin_file' => $plugin_file,
'message' => __( 'Add On installed.', 'paid-memberships-pro' ),
);
}
/**
* Activate an Add On.
*
* @since 3.2.0
*
* @param string $slug_or_plugin Slug (folder) or plugin file (folder/file.php).
* @return array|WP_Error Result array or WP_Error.
*/
public function activate( $slug_or_plugin = '' ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( empty( $slug_or_plugin ) ) {
return new WP_Error( 'pmpro_addon_activate_invalid', __( 'Invalid Add On.', 'paid-memberships-pro' ) );
}
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error( 'pmpro_addon_activate_cap', __( 'You do not have permission to activate plugins.', 'paid-memberships-pro' ) );
}
$plugin_file = $this->resolve_plugin_file( $slug_or_plugin );
if ( is_wp_error( $plugin_file ) ) {
return $plugin_file;
}
if ( is_plugin_active( $plugin_file ) ) {
return array(
'success' => true,
'action' => 'activate',
'plugin_file' => $plugin_file,
'message' => __( 'Add On already active.', 'paid-memberships-pro' ),
);
}
$activate = activate_plugin( $plugin_file, '', false );
if ( is_wp_error( $activate ) ) {
return $activate;
}
return array(
'success' => true,
'action' => 'activate',
'plugin_file' => $plugin_file,
'message' => __( 'Add On activated.', 'paid-memberships-pro' ),
);
}
/**
* Deactivate an Add On.
*
* @since 3.2.0
*
* @param string $slug_or_plugin Slug (folder) or plugin file (folder/file.php).
* @return array|WP_Error Result array or WP_Error.
*/
public function deactivate( $slug_or_plugin = '' ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( empty( $slug_or_plugin ) ) {
return new WP_Error( 'pmpro_addon_deactivate_invalid', __( 'Invalid Add On.', 'paid-memberships-pro' ) );
}
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error( 'pmpro_addon_deactivate_cap', __( 'You do not have permission to deactivate plugins.', 'paid-memberships-pro' ) );
}
$plugin_file = $this->resolve_plugin_file( $slug_or_plugin );
if ( is_wp_error( $plugin_file ) ) {
return $plugin_file;
}
if ( ! is_plugin_active( $plugin_file ) ) {
return array(
'success' => true,
'action' => 'deactivate',
'plugin_file' => $plugin_file,
'message' => __( 'Add On already inactive.', 'paid-memberships-pro' ),
);
}
deactivate_plugins( array( $plugin_file ), false, false );
if ( is_plugin_active( $plugin_file ) ) {
return new WP_Error( 'pmpro_addon_deactivate_failed', __( 'Deactivation failed.', 'paid-memberships-pro' ) );
}
return array(
'success' => true,
'action' => 'deactivate',
'plugin_file' => $plugin_file,
'message' => __( 'Add On deactivated.', 'paid-memberships-pro' ),
);
}
/**
* Update an Add On.
*
* @since 3.6
*
* @param string $slug_or_plugin Slug (folder) or plugin file (folder/file.php).
* @return array|WP_Error Result array or WP_Error.
*/
public function update( $slug_or_plugin = '' ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( empty( $slug_or_plugin ) ) {
return new WP_Error( 'pmpro_addon_update_invalid', __( 'Invalid Add On.', 'paid-memberships-pro' ) );
}
if ( ! current_user_can( 'update_plugins' ) ) {
return new WP_Error( 'pmpro_addon_update_cap', __( 'You do not have permission to update plugins.', 'paid-memberships-pro' ) );
}
$plugin_file = $this->resolve_plugin_file( $slug_or_plugin );
if ( is_wp_error( $plugin_file ) ) {
return $plugin_file;
}
// Ensure WordPress has the latest update data before attempting an upgrade.
// When multiple updates are triggered sequentially over AJAX, the in-memory
// update transient can be stale after the first upgrade completes. Refreshing
// here prevents false negatives from Plugin_Upgrader::upgrade().
$this->refresh_update_data();
// License gating when applicable.
$slug = $this->maybe_extract_slug( $slug_or_plugin );
$addon = $slug ? $this->get_addon_by_slug( $slug ) : false;
if ( ! empty( $addon ) && isset( $addon['License'] ) && pmpro_license_type_is_premium( $addon['License'] ) && ! $this->can_download_addon_with_license( $addon['License'] ) ) {
return new WP_Error( 'pmpro_addon_license_required', sprintf( __( 'A valid PMPro %s license is required to update this Add On.', 'paid-memberships-pro' ), ucwords( $addon['License'] ) ) );
}
$fs_ready = $this->ensure_filesystem();
if ( is_wp_error( $fs_ready ) ) {
return $fs_ready;
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$skin = $this->get_upgrader_skin();
$upgrader = new Plugin_Upgrader( $skin );
$result = $upgrader->upgrade( $plugin_file );
if ( is_wp_error( $result ) ) {
return $result;
}
if ( false === $result ) {
// Try one more time after a hard refresh of update data.
$this->refresh_update_data();
$result = $upgrader->upgrade( $plugin_file );
if ( is_wp_error( $result ) ) {
return $result;
}
if ( false === $result ) {
// As a last resort for PMPro-hosted add ons, attempt a direct reinstall
// using the package URL. This can occur if the transient briefly lacks
// the response entry even though an update is available.
if ( ! empty( $addon ) && ! empty( $slug ) ) {
$package = $this->get_download_package_url( $slug );
if ( ! is_wp_error( $package ) && ! empty( $package ) ) {
$install_result = $upgrader->install( $package );
if ( $install_result ) {
return array(
'success' => true,
'action' => 'update',
'plugin_file' => $plugin_file,
'message' => __( 'Add On updated.', 'paid-memberships-pro' ),
);
}
}
}
return new WP_Error( 'pmpro_addon_update_failed', __( 'Update failed.', 'paid-memberships-pro' ) );
}
}
return array(
'success' => true,
'action' => 'update',
'plugin_file' => $plugin_file,
'message' => __( 'Add On updated.', 'paid-memberships-pro' ),
);
}
/**
* Refresh the core plugin update data and PMPro add-on responses.
*
* @since 3.6
* @return void
*/
private function refresh_update_data() {
// Make sure helper functions are loaded in AJAX context.
if ( ! function_exists( 'wp_update_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/update.php';
}
// Force a fresh check from WordPress.org and our filters.
wp_version_check( array(), true );
wp_update_plugins();
}
/**
* Delete (uninstall) an Add On.
*
* @since 3.6
*
* @param string $slug_or_plugin Slug (folder) or plugin file (folder/file.php).
* @return array|WP_Error Result array or WP_Error.
*/
public function delete( $slug_or_plugin = '' ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( empty( $slug_or_plugin ) ) {
return new WP_Error( 'pmpro_addon_delete_invalid', __( 'Invalid Add On.', 'paid-memberships-pro' ) );
}
if ( ! current_user_can( 'delete_plugins' ) ) {
return new WP_Error( 'pmpro_addon_delete_cap', __( 'You do not have permission to delete plugins.', 'paid-memberships-pro' ) );
}
$plugin_file = $this->resolve_plugin_file( $slug_or_plugin );
if ( is_wp_error( $plugin_file ) ) {
return $plugin_file;
}
// Deactivate before deleting.
if ( is_plugin_active( $plugin_file ) ) {
deactivate_plugins( array( $plugin_file ), false, false );
}
$fs_ready = $this->ensure_filesystem();
if ( is_wp_error( $fs_ready ) ) {
return $fs_ready;
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$deleted = delete_plugins( array( $plugin_file ) );
if ( is_wp_error( $deleted ) ) {
return $deleted;
}
if ( ! $deleted ) {
return new WP_Error( 'pmpro_addon_delete_failed', __( 'Delete failed.', 'paid-memberships-pro' ) );
}
return array(
'success' => true,
'action' => 'delete',
'plugin_file' => $plugin_file,
'message' => __( 'Add On deleted.', 'paid-memberships-pro' ),
);
}
/**
* Attempt to initialize the WordPress filesystem API for upgrader operations.
* Returns WP_Error when credentials are required and not provided.
*
* @since 3.6
*
* @return true|WP_Error
*/
private function ensure_filesystem() {
require_once ABSPATH . 'wp-admin/includes/file.php';
// Attempt to initialize using any available method (Direct, etc.).
if ( WP_Filesystem() ) {
return true;
}
// If we cannot initialize, credentials are likely required.
return new WP_Error( 'pmpro_fs_credentials', __( 'Filesystem credentials are required to continue.', 'paid-memberships-pro' ) );
}
/**
* Get a quiet upgrader skin to capture output nicely for programmatic use.
*
* @since 3.6
*
* @return Automatic_Upgrader_Skin
*/
private function get_upgrader_skin() {
// Use the core automatic skin to avoid direct output.
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skins.php';
$args = array(
'skip_header' => true,
'url' => admin_url( 'plugins.php' ),
'nonce' => wp_create_nonce( 'updates' ),
'title' => __( 'PMPro Add On Update', 'paid-memberships-pro' ),
);
return new Automatic_Upgrader_Skin( $args );
}
/**
* Resolve plugin file (folder/file.php) from a slug or plugin identifier.
*
* @since 3.6
*
* @param string $slug_or_plugin Slug or plugin file.
* @return string|WP_Error Plugin file or WP_Error if not found.
*/
private function resolve_plugin_file( $slug_or_plugin ) {
$slug_or_plugin = (string) $slug_or_plugin;
if ( false !== strpos( $slug_or_plugin, '/' ) && false !== strpos( $slug_or_plugin, '.php' ) ) {
return $slug_or_plugin; // Already a plugin file.
}
$slug = sanitize_key( $slug_or_plugin );
$default = $slug . '/' . $slug . '.php';
$abs = WP_PLUGIN_DIR . '/' . $default;
if ( file_exists( $abs ) ) {
return $default;
}
// Search installed plugins for a folder matching the slug.
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$installed = array_keys( $this->get_installed_plugins() );
foreach ( $installed as $plugin_file ) {
if ( 0 === strpos( $plugin_file, $slug . '/' ) ) {
return $plugin_file;
}
}
return new WP_Error( 'pmpro_plugin_file_not_found', __( 'Plugin file could not be resolved.', 'paid-memberships-pro' ) );
}
/**
* Maybe extract a slug from a slug or plugin file string.
*
* @since 3.6
*
* @param string $slug_or_plugin Input string.
* @return string Slug or empty string if not detected.
*/
private function maybe_extract_slug( $slug_or_plugin ) {
$slug_or_plugin = (string) $slug_or_plugin;
if ( false !== strpos( $slug_or_plugin, '/' ) ) {
list( $slug ) = explode( '/', $slug_or_plugin );
return sanitize_key( $slug );
}
return sanitize_key( $slug_or_plugin );
}
/**
* Get the download/package URL for an Add On by slug.
*
* @since 3.6
*
* @param string $slug The Add On slug.
* @return string|WP_Error Package URL or WP_Error.
*/
private function get_download_package_url( $slug ) {
$slug = sanitize_key( $slug );
// Ensure the plugins_api() function is available (not always loaded during admin-ajax requests).
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$api = plugins_api( 'plugin_information', array( 'slug' => $slug ) );
if ( is_wp_error( $api ) || empty( $api ) || empty( $api->download_link ) ) {
return new WP_Error( 'pmpro_addon_package_missing', __( 'Could not determine download URL for this Add On.', 'paid-memberships-pro' ) );
}
return esc_url_raw( $api->download_link );
}
/**
* Register AJAX endpoints for add-on operations.
*
* @since 3.6
*/
public function register_ajax_endpoints() {
add_action( 'wp_ajax_pmpro_addon_install', array( $this, 'ajax_install_addon' ) );
add_action( 'wp_ajax_pmpro_addon_update', array( $this, 'ajax_update_addon' ) );
add_action( 'wp_ajax_pmpro_addon_activate', array( $this, 'ajax_activate_addon' ) );
add_action( 'wp_ajax_pmpro_addon_deactivate', array( $this, 'ajax_deactivate_addon' ) );
add_action( 'wp_ajax_pmpro_addon_delete', array( $this, 'ajax_delete_addon' ) );
}
/**
* AJAX: Install.
*
* @since 3.6
* @return void
*/
public function ajax_install_addon() {
check_ajax_referer( 'pmpro_addons_actions', 'nonce' );
$slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
$result = $this->install( $slug );
$this->send_ajax_result( $result );
}
/**
* AJAX: Update Add On
*
* @since 3.6
*
* @return void
*/
public function ajax_update_addon() {
check_ajax_referer( 'pmpro_addons_actions', 'nonce' );
$target = isset( $_POST['target'] ) ? sanitize_text_field( wp_unslash( $_POST['target'] ) ) : '';
$result = $this->update( $target );
$this->send_ajax_result( $result );
}
/**
* AJAX: Activate Add On
*
* @since 3.6
*
* @return void
*/
public function ajax_activate_addon() {
check_ajax_referer( 'pmpro_addons_actions', 'nonce' );
$target = isset( $_POST['target'] ) ? sanitize_text_field( wp_unslash( $_POST['target'] ) ) : '';
$result = $this->activate( $target );
$this->send_ajax_result( $result );
}
/**
* AJAX: Deactivate Add On
*
* @since 3.6
*
* @return void
*/
public function ajax_deactivate_addon() {
check_ajax_referer( 'pmpro_addons_actions', 'nonce' );
$target = isset( $_POST['target'] ) ? sanitize_text_field( wp_unslash( $_POST['target'] ) ) : '';
$result = $this->deactivate( $target );
$this->send_ajax_result( $result );
}
/**
* AJAX: Delete Add On
*
* @since 3.6
*
* @return void
*/
public function ajax_delete_addon() {
check_ajax_referer( 'pmpro_addons_actions', 'nonce' );
$target = isset( $_POST['target'] ) ? sanitize_text_field( wp_unslash( $_POST['target'] ) ) : '';
$result = $this->delete( $target );
$this->send_ajax_result( $result );
}
/**
* Helper to send standardized AJAX responses.
*
* @since 3.6
*
* @param array|WP_Error $result Operation result.
*/
private function send_ajax_result( $result ) {
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
)
);
}
wp_send_json_success( $result );
}
/**
* Get the Add On icon from the plugin slug.
*
* @since 2.8.x
*
* @param string $slug The identifying slug for the addon (typically the directory name).
* @return string|false $plugin_icon_src The src URL for the plugin icon or false if PMPRO_DIR is not defined.
*/
public function get_addon_icon( $slug ) {
// If PMPRO_DIR is not defined, bail. This may happen in the Update Manager Add On.
if ( ! defined( 'PMPRO_DIR' ) ) {
return false;
}
if ( file_exists( PMPRO_DIR . '/images/add-ons/' . $slug . '.png' ) ) {
$plugin_icon_src = PMPRO_URL . '/images/add-ons/' . $slug . '.png';
} else {
$plugin_icon_src = PMPRO_URL . '/images/add-ons/default-icon.png';
}
return $plugin_icon_src;
}
/**
* Setup plugin updaters
*
* @since 1.8.5
*/
public function plugins_api( $api, $action = '', $args = null ) {
// Not even looking for plugin information? Or not given slug?
if ( 'plugin_information' != $action || empty( $args->slug ) ) {
return $api;
}
// get addon information
$addon = $this->get_addon_by_slug( $args->slug );
// no addons?
if ( empty( $addon ) ) {
return $api;
}
// handled by wordpress.org?
if ( empty( $addon['License'] ) || $addon['License'] == 'wordpress.org' ) {
return $api;
}
// Create a new stdClass object and populate it with our plugin information.
$api = $this->get_plugin_API_object_from_addon( $addon );
return $api;
}
/**
* Detect when trying to update a PMPro Plus plugin without a valid license key.
*
* @since 1.9
*/
public function check_when_updating_plugins() {
// if user can't edit plugins, then WP will catch this later
if ( ! current_user_can( 'update_plugins' ) ) {
return;
}
// updating one or more plugins via Dashboard -> Upgrade
if ( basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ) ) == 'update.php' && ! empty( $_REQUEST['action'] ) && $_REQUEST['action'] == 'update-selected' && ! empty( $_REQUEST['plugins'] ) ) {
// figure out which plugins we are updating
$plugins = explode( ',', stripslashes( sanitize_text_field( $_GET['plugins'] ) ) );
$plugins = array_map( 'urldecode', $plugins );
// look for addons
$premium_addons = array();
$premium_plugins = array();
foreach ( $plugins as $plugin ) {
$slug = str_replace( '.php', '', basename( $plugin ) );
$addon = $this->get_addon_by_slug( $slug );
if ( ! empty( $addon ) && pmpro_license_type_is_premium( $addon['License'] ) ) {
if ( ! isset( $premium_addons[ $addon['License'] ] ) ) {
$premium_addons[ $addon['License'] ] = array();
$premium_plugins[ $addon['License'] ] = array();
}
$premium_addons[ $addon['License'] ][] = $addon['Name'];
$premium_plugins[ $addon['License'] ][] = $plugin;
}
}
unset( $plugin );
// if Plus addons found, check license key
if ( ! empty( $premium_plugins ) ) {
foreach ( $premium_plugins as $license_type => $premium_plugin ) {
// if they have a good license, skip the error
if ( $this->can_download_addon_with_license( $license_type ) ) {
continue;
}
// show error
$msg = wp_kses(
sprintf( __( 'You must have a <a target="_blank" href="https://www.paidmembershipspro.com/pricing/?utm_source=wp-admin&utm_pluginlink=bulkupdate">valid PMPro %1$s License Key</a> to update PMPro %2$s add ons. The following plugins will not be updated:', 'paid-memberships-pro' ), ucwords( $license_type ), ucwords( $license_type ) ),
array(
'a' => array(
'href' => array(),
'target' => array(),
),
)
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="error"><p>' . $msg . ' <strong>' . esc_html( implode( ', ', $premium_addons[ $license_type ] ) ) . '</strong></p></div>';
}
}
// can exit out of this function now
return;
}
// upgrading just one or plugin via an update.php link
if ( basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ) ) == 'update.php' && ! empty( $_REQUEST['action'] ) && $_REQUEST['action'] == 'upgrade-plugin' && ! empty( $_REQUEST['plugin'] ) ) {
// figure out which plugin we are updating
$plugin = urldecode( trim( sanitize_text_field( $_REQUEST['plugin'] ) ) );
$slug = str_replace( '.php', '', basename( $plugin ) );
$addon = $this->get_addon_by_slug( $slug );
if ( ! empty( $addon ) && pmpro_license_type_is_premium( $addon['License'] ) && ! $this->can_download_addon_with_license( $addon['License'] ) ) {
require_once ABSPATH . 'wp-admin/admin-header.php';
$msg = sprintf(
__( 'You must have a <a href="https://www.paidmembershipspro.com/pricing/?utm_source=wp-admin&utm_pluginlink=addon_update">valid PMPro %1$s License Key</a> to update PMPro %2$s add ons.', 'paid-memberships-pro' ),
ucwords( $addon['License'] ),
ucwords( $addon['License'] )
);
$html = '<div class="wrap"><h2>' . esc_html__( 'Update Plugin', 'paid-memberships-pro' ) . '</h2>';
$html .= '<div class="error"><p>' . wp_kses_post( $msg ) . '</p></div>';
$html .= '<p><a href="' . esc_url( admin_url( 'admin.php?page=pmpro-addons' ) ) . '" target="_parent">' . esc_html__( 'Return to the PMPro Add Ons page', 'paid-memberships-pro' ) . '</a></p>';
$html .= '</div>';
echo wp_kses_post( $html );
include ABSPATH . 'wp-admin/admin-footer.php';
// can exit WP now
exit;
}
}
// updating via AJAX on the plugins page
if ( basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ) ) == 'admin-ajax.php' && ! empty( $_REQUEST['action'] ) && $_REQUEST['action'] == 'update-plugin' && ! empty( $_REQUEST['plugin'] ) ) {
// figure out which plugin we are updating
$plugin = urldecode( trim( sanitize_text_field( $_REQUEST['plugin'] ) ) );
$slug = str_replace( '.php', '', basename( $plugin ) );
$addon = $this->get_addon_by_slug( $slug );
if ( ! empty( $addon ) && pmpro_license_type_is_premium( $addon['License'] ) && ! $this->can_download_addon_with_license( $addon['License'] ) ) {
$msg = sprintf( __( 'You must enter a valid PMPro %s License Key in the PMPro Settings to update this Add On.', 'paid-memberships-pro' ), ucwords( $addon['License'] ) );
echo '<div class="error"><p>' . esc_html( $msg ) . '</p></div>';
// can exit WP now
exit;
}
}
}
/**
* Check if an add on can be downloaded based on it's license.
*
* @since 2.7.4
* @param string $addon_license The license type of the add on to check.
* @return bool True if the user's license key can download that add on,
* False if the user's license key cannot download it.
*/
public function can_download_addon_with_license( $addon_license ) {
// The wordpress.org and free types can always be downloaded.
if ( $addon_license === 'wordpress.org' || $addon_license === 'free' ) {
return true;
}
// Check premium license types.
if ( $addon_license === 'standard' ) {
$types_to_check = array( 'standard', 'plus', 'builder' );
}
if ( $addon_license === 'plus' ) {
$types_to_check = array( 'plus', 'builder' );
}
if ( $addon_license === 'builder' ) {
$types_to_check = array( 'builder' );
}
// Some unknown license?
if ( empty( $types_to_check ) ) {
return false;
}
return pmpro_license_isValid( null, $types_to_check );
}
/**
* Get remote addons from the License Server.
*
* @return array
* @since 3.6
*/
private function get_remote_addons() {
$addons = array();
/**
* Filter to change the timeout for this wp_remote_get() request.
*
* @since 1.8.5.1
*
* @param int $timeout The number of seconds before the request times out
*/
$timeout = apply_filters( 'pmpro_get_addons_timeout', 5 );
// Get Add Ons from the License Server
$remote_addons = wp_remote_get( PMPRO_LICENSE_SERVER . 'addons/', array( 'timeout' => (int) $timeout ) );
// Check for errors and if we're okay, save the addons formatted
if ( is_wp_error( $remote_addons ) ) {
pmpro_setMessage( 'Could not connect to the PMPro License Server to update addon information. Try again later.', 'error' );
// Return cached addons if available
return $this->addons ? : array();
} elseif ( ! empty( $remote_addons ) && $remote_addons['response']['code'] == 200 ) {
// Update the timestamp
update_option( 'pmpro_addons_timestamp', current_time( 'timestamp' ), 'no' );
$addons = json_decode( wp_remote_retrieve_body( $remote_addons ), true );
// If for some reason the addons are not formatted correctly
if ( empty( $addons ) ) {
pmpro_setMessage( 'No addons found.', 'error' );
return $addons;
}
// Create a short name for each Add On.
foreach ( $addons as $key => $value ) {
$addons[ $key ]['ShortName'] = trim( str_replace( array( 'Add On', 'Paid Memberships Pro - ' ), '', $addons[ $key ]['Title'] ) );
}
$short_names = array_column( $addons, 'ShortName' );
// Sort the addons by short name.
array_multisort( $short_names, SORT_ASC, SORT_STRING | SORT_FLAG_CASE, $addons );
// Update addons in cache.
update_option( 'pmpro_addons', $addons, 'no' );
}
return $addons;
}
/**
* Convert the format from get_addons() to that needed for plugins_api
*
* @param array $addon The addon information.
*
* @since 1.8.5
*/
private function get_plugin_API_object_from_addon( $addon ) {
$api = new stdClass();
if ( empty( $addon ) ) {
return $api;
}
// add info
$api->name = isset( $addon['Name'] ) ? $addon['Name'] : '';
$api->slug = isset( $addon['Slug'] ) ? $addon['Slug'] : '';
$api->plugin = isset( $addon['plugin'] ) ? $addon['plugin'] : '';
$api->version = isset( $addon['Version'] ) ? $addon['Version'] : '';
$api->author = isset( $addon['Author'] ) ? $addon['Author'] : '';
$api->author_profile = isset( $addon['AuthorURI'] ) ? $addon['AuthorURI'] : '';
$api->requires = isset( $addon['Requires'] ) ? $addon['Requires'] : '';
$api->tested = isset( $addon['Tested'] ) ? $addon['Tested'] : '';
$api->last_updated = isset( $addon['LastUpdated'] ) ? $addon['LastUpdated'] : '';
$api->homepage = isset( $addon['URI'] ) ? $addon['URI'] : '';
$api->download_link = isset( $addon['Download'] ) ? $addon['Download'] : '';
$api->package = isset( $addon['Download'] ) ? $addon['Download'] : '';
// add sections
if ( ! empty( $addon['Description'] ) ) {
$api->sections['description'] = $addon['Description'];
}
if ( ! empty( $addon['Installation'] ) ) {
$api->sections['installation'] = $addon['Installation'];
}
if ( ! empty( $addon['FAQ'] ) ) {
$api->sections['faq'] = $addon['FAQ'];
}
if ( ! empty( $addon['Changelog'] ) ) {
$api->sections['changelog'] = $addon['Changelog'];
}
// get license key if one is available
$key = get_option( 'pmpro_license_key', '' );
if ( ! empty( $key ) && ! empty( $api->download_link ) ) {
$api->download_link = add_query_arg( 'key', $key, $api->download_link );
}
if ( ! empty( $key ) && ! empty( $api->package ) ) {
$api->package = add_query_arg( 'key', $key, $api->package );
}
if ( empty( $api->upgrade_notice ) && pmpro_license_type_is_premium( $addon['License'] ) ) {
if ( ! pmpro_license_isValid( null, $addon['License'] ) ) {
$api->upgrade_notice = sprintf( __( 'Important: This plugin requires a valid PMPro %s license key to update.', 'paid-memberships-pro' ), ucwords( $addon['License'] ) );
}
}
return $api;
}
/**
* Get installed plugins with caching.
*
* @since 3.6
*
* @return array Installed plugins.
*/
private function get_installed_plugins() {
if ( null === $this->cached_plugins ) {
$this->cached_plugins = get_plugins();
}
return $this->cached_plugins;
}
}