Files
2026-04-28 15:13:50 +02:00

454 lines
16 KiB
PHP

<?php
/**
* Recommended Plugins Widget.
*
* Renders a 2x2 grid of recommended free plugins on the dashboard, always
* showing 4 items. Installed plugins render as a disabled "Installed" button.
* The visible set is persisted and only rotates 14 days after the user's last
* widget-driven install, at which point installed slots are swapped for
* non-installed reserves that haven't been shown yet.
*
* @package WPConsent
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WPConsent_Recommended_Plugins
*/
class WPConsent_Recommended_Plugins {
/**
* All plugin definitions, keyed by short slug (= wordpress.org directory slug).
*
* @var array
*/
private $all_plugins = array();
/**
* Constructor — registers AJAX hook.
*/
public function __construct() {
add_action( 'wp_ajax_wpconsent_install_plugin', array( $this, 'ajax_install_plugin' ) );
}
/**
* Get all plugin definitions, lazy-loading on first call.
*
* Deferred so that __() is not called before the text domain loads.
*
* @return array
*/
private function get_all_plugins() {
if ( empty( $this->all_plugins ) ) {
$this->all_plugins = $this->define_plugins();
}
return $this->all_plugins;
}
/**
* Define the recommended plugins in display order.
*
* The first 4 are shown initially. The rest serve as a reserve
* to fill gaps as the user installs plugins from the list.
*
* @return array
*/
private function define_plugins() {
return array(
// Initial 4.
'all-in-one-seo-pack' => array(
'name' => __( 'AIOSEO', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Powerful SEO plugin to optimize your site, boost your rankings, and increase organic traffic.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
'pro_slug' => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
'icon' => 'icon-all-in-one-seo-pack.png',
),
'duplicator' => array(
'name' => __( 'Duplicator', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Simple way to move WordPress sites, create reliable backups, or clone a site for staging.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'duplicator/duplicator.php',
'pro_slug' => 'duplicator-pro/duplicator-pro.php',
'icon' => 'icon-duplicator.png',
),
'search-replace-wpcode' => array(
'name' => __( 'Search & Replace Everything', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Search and replace text, media, and URLs across your entire WordPress database with ease.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'search-replace-wpcode/wsrw.php',
'pro_slug' => 'search-replace-wpcode-pro/wsrw-premium.php',
'icon' => 'icon-search-replace-wpcode.png',
),
'insert-headers-and-footers' => array(
'name' => __( 'WPCode', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'The most popular code snippets plugin for WordPress, used by over 3 million websites.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'insert-headers-and-footers/ihaf.php',
'pro_slug' => 'wpcode-premium/wpcode.php',
'icon' => 'icon-wpcode.svg',
),
// Reserve.
'uncanny-automator' => array(
'name' => __( 'Uncanny Automator', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'The easiest and most powerful way to automate your WordPress site with no code.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'uncanny-automator/uncanny-automator.php',
'pro_slug' => 'uncanny-automator-pro/uncanny-automator-pro.php',
'icon' => 'icon-uncanny-automator.png',
),
'wp-mail-smtp' => array(
'name' => __( 'WP Mail SMTP', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Fix email deliverability issues by reconfiguring WordPress to use a proper mailer.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'wp-mail-smtp/wp_mail_smtp.php',
'pro_slug' => 'wp-mail-smtp-pro/wp_mail_smtp.php',
'icon' => 'icon-wp-mail-smtp.png',
),
'coming-soon' => array(
'name' => __( 'SeedProd', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Drag-and-drop WordPress builder for landing pages, coming soon, and maintenance mode pages.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'coming-soon/coming-soon.php',
'pro_slug' => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
'icon' => 'icon-seedprod.png',
),
'reviews-feed' => array(
'name' => __( 'Reviews Feed', 'wpconsent-cookies-banner-privacy-suite' ),
'description' => __( 'Display Google, Yelp, and Facebook reviews on your site to build trust and drive more sales.', 'wpconsent-cookies-banner-privacy-suite' ),
'slug' => 'reviews-feed/sb-reviews.php',
'pro_slug' => 'reviews-feed-pro/sb-reviews-pro.php',
'icon' => 'icon-reviews-feed.png',
),
);
}
/**
* Get the list of recommended plugins to display, each tagged with `is_installed`.
*
* @return array
*/
public function get_recommended() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installed_map = array_flip( array_keys( get_plugins() ) );
$all_plugins = $this->get_all_plugins();
foreach ( $all_plugins as $key => $plugin ) {
$all_plugins[ $key ]['is_installed'] = isset( $installed_map[ $plugin['slug'] ] ) || isset( $installed_map[ $plugin['pro_slug'] ] );
}
$result = array();
foreach ( $this->get_or_rotate_visible_set( $all_plugins ) as $key ) {
$result[ $key ] = $all_plugins[ $key ];
}
return $result;
}
/**
* Return the stored visible set, computing the initial pick or rotating when due.
*
* @param array $all_plugins Plugin definitions with `is_installed` flags.
*
* @return string[] Ordered list of plugin keys.
*/
private function get_or_rotate_visible_set( $all_plugins ) {
$activated = get_option( 'wpconsent_activated', array() );
$visible = isset( $activated['recommended_visible'] ) ? $activated['recommended_visible'] : array();
if ( empty( $visible ) ) {
$visible = $this->compute_initial_set( $all_plugins );
$activated['recommended_visible'] = $visible;
update_option( 'wpconsent_activated', $activated );
return $visible;
}
$last_install = isset( $activated['recommended_last_widget_install'] ) ? (int) $activated['recommended_last_widget_install'] : 0;
if ( ! $last_install || ( time() - $last_install ) < 14 * DAY_IN_SECONDS ) {
return $visible;
}
$rotated = $this->rotate_visible_set( $visible, $all_plugins );
$activated['recommended_visible'] = $rotated;
unset( $activated['recommended_last_widget_install'] );
update_option( 'wpconsent_activated', $activated );
return $rotated;
}
/**
* Compute the initial visible set: 4 non-installed first, installed fill the rest.
*
* @param array $all_plugins Plugin definitions with `is_installed` flags.
*
* @return string[] Ordered list of plugin keys.
*/
private function compute_initial_set( $all_plugins ) {
$display_count = 4;
$non_installed = array();
$already_installed = array();
foreach ( $all_plugins as $key => $plugin ) {
if ( ! empty( $plugin['is_installed'] ) ) {
$already_installed[] = $key;
} else {
$non_installed[] = $key;
}
}
$keys = array_slice( $non_installed, 0, $display_count );
if ( count( $keys ) < $display_count ) {
$keys = array_merge( $keys, array_slice( $already_installed, 0, $display_count - count( $keys ) ) );
}
return $keys;
}
/**
* Swap installed slots in the visible set for non-installed reserves not yet shown.
*
* @param string[] $visible Current visible plugin keys.
* @param array $all_plugins Plugin definitions with `is_installed` flags.
*
* @return string[] Rotated visible set (unchanged if no reserves exist).
*/
private function rotate_visible_set( $visible, $all_plugins ) {
$reserve = array();
foreach ( $all_plugins as $key => $plugin ) {
if ( empty( $plugin['is_installed'] ) && ! in_array( $key, $visible, true ) ) {
$reserve[] = $key;
}
}
if ( empty( $reserve ) ) {
return $visible;
}
$rotated = array();
foreach ( $visible as $key ) {
if ( ! empty( $all_plugins[ $key ]['is_installed'] ) && ! empty( $reserve ) ) {
$rotated[] = array_shift( $reserve );
} else {
$rotated[] = $key;
}
}
return $rotated;
}
/**
* Stamp the current time to (re)start the 14-day rotation timer.
*
* @return void
*/
private function track_widget_install() {
$activated = get_option( 'wpconsent_activated', array() );
$activated['recommended_last_widget_install'] = time();
update_option( 'wpconsent_activated', $activated );
}
/**
* Get the icon URL for a plugin image.
*
* @param string $icon_filename The icon filename.
*
* @return string
*/
private function get_icon_url( $icon_filename ) {
return WPCONSENT_PLUGIN_URL . 'admin/images/' . $icon_filename;
}
/**
* Render the recommended plugins dashboard widget.
*
* Outputs nothing if there are no plugins to recommend.
*
* @return void
*/
public function recommended_plugins_widget() {
$plugins = $this->get_recommended();
if ( empty( $plugins ) ) {
return;
}
?>
<div class="wpconsent-dashboard-box wpconsent-recommended-plugins-widget">
<div class="wpconsent-dashboard-box-title">
<h2><?php esc_html_e( 'Recommended Plugins', 'wpconsent-cookies-banner-privacy-suite' ); ?></h2>
</div>
<div class="wpconsent-dashboard-box-content">
<div class="wpconsent-recommended-plugins-grid">
<?php foreach ( $plugins as $key => $plugin ) : ?>
<div class="wpconsent-recommended-plugin-item">
<div class="wpconsent-recommended-plugin-header">
<img
src="<?php echo esc_url( $this->get_icon_url( $plugin['icon'] ) ); ?>"
alt="<?php echo esc_attr( $plugin['name'] ); ?>"
class="wpconsent-recommended-plugin-icon"
width="24"
height="24"
/>
<span class="wpconsent-recommended-plugin-name"><?php echo esc_html( $plugin['name'] ); ?></span>
</div>
<span class="wpconsent-recommended-plugin-description"><?php echo esc_html( $plugin['description'] ); ?></span>
<?php if ( ! empty( $plugin['is_installed'] ) ) : ?>
<button
type="button"
class="wpconsent-button wpconsent-button-secondary wpconsent-button-small"
disabled
>
<?php esc_html_e( 'Installed', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</button>
<?php else : ?>
<button
type="button"
class="wpconsent-button wpconsent-button-secondary wpconsent-button-small wpconsent-button-install-plugin"
data-slug="<?php echo esc_attr( $key ); ?>"
>
<?php esc_html_e( 'Install', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</button>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php
}
/**
* Activate a plugin and send a JSON success/error response.
*
* @param string $plugin_basename The plugin basename to activate.
*
* @return void Sends JSON and terminates.
*/
private function activate_and_respond( $plugin_basename ) {
$activated = activate_plugin( $plugin_basename );
if ( is_wp_error( $activated ) ) {
wp_send_json_error( array( 'message' => $activated->get_error_message() ) );
}
$this->track_widget_install();
wp_send_json_success(
array(
'message' => esc_html__( 'Plugin activated.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
/**
* AJAX handler: install and activate a recommended plugin.
*
* Validates the slug against the allowlist, then tries (in order):
* 1. Already active — return success immediately.
* 2. Pro version installed but inactive — activate it.
* 3. Lite version installed but inactive — activate it.
* 4. Not installed — download from wordpress.org and activate.
*
* @return void
*/
public function ajax_install_plugin() {
check_ajax_referer( 'wpconsent_admin' );
if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'You do not have permission to install plugins. Please ask your site administrator to install it for you.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
if ( ! wp_is_file_mod_allowed( 'install_plugins' ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Plugin installation is not allowed on this site. Please ask your site administrator to install it for you.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
$slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
if ( ! array_key_exists( $slug, $this->get_all_plugins() ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Invalid plugin.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
$plugin_info = $this->get_all_plugins()[ $slug ];
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/template.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
require_once ABSPATH . 'wp-admin/includes/screen.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
// Set the current screen to avoid undefined notices.
set_current_screen( 'toplevel_page_wpconsent' );
// Do not allow WordPress to search/download translations, as this will break JS output.
remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
// Already active (either version).
if ( is_plugin_active( $plugin_info['slug'] ) || is_plugin_active( $plugin_info['pro_slug'] ) ) {
$this->track_widget_install();
wp_send_json_success(
array(
'message' => esc_html__( 'Plugin already active.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
$installed_plugins = array_keys( get_plugins() );
// Pro version installed but not active — activate it.
if ( in_array( $plugin_info['pro_slug'], $installed_plugins, true ) ) {
$this->activate_and_respond( $plugin_info['pro_slug'] );
}
// Lite version installed but not active — activate it.
if ( in_array( $plugin_info['slug'], $installed_plugins, true ) ) {
$this->activate_and_respond( $plugin_info['slug'] );
}
// Not installed — download from wordpress.org and activate.
wpconsent_require_upgrader();
$installer = new Plugin_Upgrader( new WPConsent_Skin() );
$installer->install( 'https://downloads.wordpress.org/plugin/' . $slug . '.zip' );
wp_cache_delete( 'plugins', 'plugins' );
$plugin_basename = $installer->plugin_info();
if ( ! $plugin_basename ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Plugin installation failed. Please try again.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
$activated = activate_plugin( $plugin_basename );
if ( is_wp_error( $activated ) ) {
wp_send_json_error(
array(
'message' => esc_html__( 'Plugin installed but could not be activated.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
$this->track_widget_install();
wp_send_json_success(
array(
'message' => esc_html__( 'Plugin installed and activated.', 'wpconsent-cookies-banner-privacy-suite' ),
)
);
}
}