From 2720f6828ecbbef43380bbc202f48ac21d8e0e34 Mon Sep 17 00:00:00 2001 From: Tigran Ghukasyan Date: Wed, 2 Oct 2024 18:04:04 +0400 Subject: [PATCH 1/7] initial implementation of cross promotion --- assets/img/duplicator-icon.svg | 54 ++ assets/js/extra-plugins.js | 88 ++ src/CrossPromotion.php | 356 +++++++ src/TransientsManager.php | 1667 ++++++++++++++++++++++++++++++++ transients-manager.php | 1638 +------------------------------ 5 files changed, 2172 insertions(+), 1631 deletions(-) create mode 100644 assets/img/duplicator-icon.svg create mode 100644 assets/js/extra-plugins.js create mode 100644 src/CrossPromotion.php create mode 100644 src/TransientsManager.php diff --git a/assets/img/duplicator-icon.svg b/assets/img/duplicator-icon.svg new file mode 100644 index 0000000..4f28a1b --- /dev/null +++ b/assets/img/duplicator-icon.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/js/extra-plugins.js b/assets/js/extra-plugins.js new file mode 100644 index 0000000..c6b1c35 --- /dev/null +++ b/assets/js/extra-plugins.js @@ -0,0 +1,88 @@ +/** + * Transients Manager Extra Plugins + */ + +'use strict'; + +var AmTmExtraPlugins = window.AmTmExtraPlugins || (function (document, window, $) { + + /** + * Public functions and properties. + */ + var app = { + + /** + * Start the engine. + */ + init: function () { + $(app.ready); + }, + + /** + * Document ready. + */ + ready: function () { + app.events(); + }, + + /** + * Dismissible notices events. + */ + events: function () { + $(document).on( + 'click', + 'button.am-tm-extra-plugin-item[data-plugin]', + function (e) { + e.preventDefault(); + + if ($(this).hasClass('disabled')) { + return; + } + + let button = $(this); + let buttonText = $(this).html(); + + $(this).addClass('disabled'); + $(this).html(l10nAmTmExtraPlugins.loading); + + $.post( + am_tm_extra_plugins.ajax_url, + { + action: 'transients_manager_extra_plugin', + nonce: am_tm_extra_plugins.extra_plugin_install_nonce, + plugin: $(this).data('plugin'), + } + ).done(function (response) { + console.log(response); + if (response.success !== true) { + console.log("Plugin installed failed with message: " + response.data.message); + button.fadeOut(300); + + setTimeout(function () { + button.html(buttonText); + button.removeClass('disabled'); + button.fadeIn(100); + }, 3000); + return; + } + + button.fadeOut(500); + status.fadeOut(500); + + button.html(l10nAmTmExtraPlugins.activated); + button.fadeIn(300); + status.fadeIn(300); + }); + } + ); + }, + + + }; + + return app; + +}(document, window, jQuery)); + +// Initialize. +AmTmExtraPlugins.init(); diff --git a/src/CrossPromotion.php b/src/CrossPromotion.php new file mode 100644 index 0000000..0b51069 --- /dev/null +++ b/src/CrossPromotion.php @@ -0,0 +1,356 @@ + esc_html__('Loading...', 'transients-manager'), + 'failure' => esc_html__('Failure', 'transients-manager'), + 'active' => esc_html__('Active', 'transients-manager'), + 'activated' => esc_html__('Activated', 'transients-manager'), + ) + ); + + wp_localize_script( + 'am-tm-extra-plugins', + 'am_tm_extra_plugins', + array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'extra_plugin_install_nonce' => wp_create_nonce('transients_manager_extra_plugin'), + ) + ); + } + + /** + * Install plugin via ajax + * + * @return void + */ + public static function installPluginAjax() + { + if (!self::shouldShowNotice()) { + return; + } + + try { + if (check_ajax_referer('transients_manager_extra_plugin', 'nonce', false) === false) { + throw new \Exception(__('Invalid nonce', 'transients-manager')); + } + + if (!current_user_can('install_plugins')) { + throw new \Exception(__('You do not have permission to install plugins', 'transients-manager')); + } + + $slug = filter_input(INPUT_POST, 'plugin', FILTER_SANITIZE_STRING); + if (empty($slug)) { + throw new \Exception(__('Invalid plugin slug', 'transients-manager')); + } + + if (self::installPlugin($slug)) { + wp_send_json_success([ + 'success' => true, + 'message' => __('Plugin installed successfully', 'transients-manager'), + ]); + } else { + throw new \Exception(__('Failed to install plugin', 'transients-manager')); + } + } catch (\Exception $e) { + wp_send_json_error([ + 'success' => false, + 'message' => $e->getMessage(), + ]); + } + } + + /** + * Display admin notices + * + * @return void + */ + public static function notices() + { + if (!self::shouldShowNotice()) { + return; + } + + self::renderStyles(); + self::render(); + } + + /** + * True fi notice should be shown + * + * @return bool + */ + private static function shouldShowNotice() + { + $tm = TransientsManager::getInstance(); + $screen = get_current_screen(); + if ( $screen->id !== $tm->screen_id ) { + return false; + } + + if (!current_user_can('install_plugins')) { + return false; + } + if ($tm->getInstallTime() + 2 * WEEK_IN_SECONDS < time()) { + return false; + } + + if (get_option(self::NOTICE_DISMISS_KEY, false)) { + return false; + } + + return true; + } + + /** + * Display admin notices + * + * @return void + */ + private static function render() + { + foreach (self::getAllPlugins() as $slug => $pluginInfo) { + if ($pluginInfo['isPro']) { + continue; + } + + $hasPro = isset($pluginInfo['pro']); + if ($hasPro && self::isPluginInstalled($pluginInfo['pro'])) { + continue; + } + + if (!$hasPro && self::isPluginInstalled($slug)) { + continue; + } + + $isPro = false; + if ($hasPro && self::isPluginInstalled($slug)) { + $isPro = true; + $pluginInfo = self::getPluginBySlug($pluginInfo['pro']); + } +?> +
+

+ +

+
+
+ <?php esc_attr($pluginInfo['name']); ?> +
+
+

+

+ + + + + +
+
+
+ + +install($pluginInfo['url'])) { + throw new \Exception('Failed to install plugin'); + } + + return true; + } + + /** + * Get the plugin info + * + * @return array Plugin info or false if not found + */ + protected static function getAllPlugins() + { + return [ + 'duplicator/duplicator.php' => [ + 'name' => __('Duplicator - WordPress Migration & Backup Plugin', 'transients-manager'), + 'url' => 'https://downloads.wordpress.org/plugin/duplicator.zip', + 'desc' => __('Leading WordPress backup & site migration plugin. Over 1,500,000+ smart website owners use Duplicator to make easy, reliable and secure WordPress backups to protect their websites.', 'transients-manager'), + 'pro' => 'duplicator-pro/duplicator-pro.php' , + 'isPro' => false, + ], + 'duplicator-pro/duplicator-pro.php' => [ + 'name' => __('Duplicator Pro - WordPress Migration & Backup Plugin', 'transients-manager'), + 'url' => 'http://duplicator.com/?utm_source=transientsmanager&utm_medium=link&utm_campaign=Cross%20Promotion', + 'desc' => __('Leading WordPress backup & site migration plugin. Smart website owners use Duplicator Pro to make easy, reliable and secure WordPress backups to protect their websites.', 'transients-manager'), + 'pro' => false, + 'isPro' => true, + ], + ]; + + } + + /** + * Get the plugin info + * + * @param string $slug Plugin slug + * + * @return array{name:string,slug:string,url:string,desc:string}|false Plugin info or false if not found + */ + protected static function getPluginBySlug($slug) + { + $allPlugins = self::getAllPlugins(); + if (isset($allPlugins[$slug])) { + return $allPlugins[$slug]; + } + + return false; + } +} diff --git a/src/TransientsManager.php b/src/TransientsManager.php new file mode 100644 index 0000000..ddf7ac9 --- /dev/null +++ b/src/TransientsManager.php @@ -0,0 +1,1667 @@ +page_id}", array( $this, 'print_styles' ) ); + + /** + * Allow third-party plugins a chance to modify the hooks above + * + * @since 2.0 + * @param object $this + */ + do_action( 'transients_manager_hooks', $this ); + } + + /** + * Set many of the class variables + * + * @since 2.0 + */ + public function set_vars() { + + // Times + $this->time_now = time(); + $this->next_cron_delete = wp_next_scheduled( 'delete_expired_transients' ); + + // Sanitize the transient ID + $this->transient_id = ! empty( $_GET['trans_id'] ) + ? absint( $_GET['trans_id'] ) + : 0; + + // Get the transient + $this->transient = ! empty( $this->transient_id ) + ? $this->get_transient_by_id( $this->transient_id ) + : false; + + // Sanitize the action + $this->action = ! empty( $_REQUEST['action'] ) + ? sanitize_key( $_REQUEST['action'] ) + : ''; + } + + /** + * Load text domain + * + * @since 1.0 + */ + public function text_domain() { + load_plugin_textdomain( 'transients-manager' ); + } + + /** + * Register menu link under Tools + * + * @since 1.0 + */ + public function tools_link() { + + // Set the screen ID + $this->screen_id = add_management_page( + esc_html__( 'Transients Manager', 'transients-manager' ), + esc_html__( 'Transients', 'transients-manager' ), + $this->capability, + $this->page_id, + array( $this, 'admin' ) + ); + } + + /** + * Editing or showing? + * + * @since 2.0 + * @return string + */ + protected function page_type() { + + // Edit or Show? + return ! empty( $this->action ) && ( 'edit_transient' === $this->action ) && ! empty( $this->transient ) + ? 'edit' + : 'show'; + } + + /** + * Render the admin UI + * + * @since 1.0 + */ + public function admin() { + + // Editing a single Transient + ( 'edit' === $this->page_type() ) + ? $this->page_edit_transient( $this->transient ) + : $this->page_show_transients(); + } + + /** + * Admin notices + * + * @since 2.0 + */ + public function notices() { + + // Get the current screen + $screen = get_current_screen(); + + // Bail if not the correct screen + if ( $screen->id !== $this->screen_id ) { + return; + } + + // Persistent Transients + if ( wp_using_ext_object_cache() ) : + +?> +
+

+
+ +
+

+
+ +
+

+
+get_total_transients( $search ); + $pages = ceil( $count / $per_page ); + $one_page = ( 1 === $pages ) ? 'one-page' : ''; + + // Pagination + $pagination = paginate_links( array( + 'base' => 'tools.php?%_%', + 'format' => '&paged=%#%', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page + ) ); + + // Transients + $transients = $this->get_transients( array( + 'search' => $search, + 'offset' => $offset, + 'number' => $per_page + ) ); + +?> + +
+

+
+ +
+ +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ +
+ + +
+
+ + + + + + + + + + + + get_transient_name( $transient ); + $value = $this->get_transient_value( $transient ); + $expiration = $this->get_transient_expiration( $transient ); + + // Delete + $delete_url = wp_nonce_url( + remove_query_arg( + array( 'deleted', 'updated' ), + add_query_arg( + array( + 'action' => 'delete_transient', + 'transient' => $name, + 'name' => $transient->option_name + ) + ) + ), + 'transients_manager' + ); + + // Edit + $edit_url = remove_query_arg( + array( 'updated', 'deleted' ), + add_query_arg( + array( + 'action' => 'edit_transient', + 'trans_id' => $transient->option_id + ) + ) + ); ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +
+                                    
+                                
+ +
+ + | + +
+ + +
+ + + +
+ + +
+ +
+
+ + + +
+ +
+ + +
+
+
+ + site_time(); ?> +
+ +get_transient_name( $transient ); + $expiration = $this->get_transient_expiration_time( $transient ); +?> + +
+

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

+ +

+
+
+ +time_now ); + $site_time_utc = gmdate( 'Y-m-d\TH:i:s+00:00', $this->time_now ); + $st_html = esc_html( sprintf( + /* translators: 1: Date and time, 2: Timezone */ + __( 'Site time: %1$s (%2$s)', 'transients-manager' ), + $site_time, + $timezone_name + ) ); + + // Cron time + $cron_time = wp_date( 'Y-m-d H:i:s', $this->next_cron_delete ); + $cron_time_utc = gmdate( 'Y-m-d\TH:i:s+00:00', $this->next_cron_delete ); + $ct_time_since = $this->time_since( $this->next_cron_delete - $this->time_now ); + $nc_time = esc_html( sprintf( + /* translators: 1: Date and time, 2: Timezone, 3: Time since */ + __( 'Expired transients scheduled for deletion on: %1$s (%2$s) – %3$s from now', 'transients-manager' ), + $cron_time, + $timezone_name, + $ct_time_since + ) ); +?> + +

+ %2$s', + $site_time_utc, + $st_html + ); + + ?>

%2$s', + $cron_time_utc, + $nc_time + ); + +?>

+ +capability ) ) { + return; + } + + // Suspended + if ( get_option( 'pw_tm_suspend' ) ) { + $action = 'unsuspend_transients'; + $label = '' . esc_html__( 'Unsuspend Transients', 'transients-manager' ) . ''; + + // Not suspended + } else { + $action = 'suspend_transients'; + $label = esc_html__( 'Suspend Transients', 'transients-manager' ); + } + + // Suspend + $wp_admin_bar->add_node( array( + 'id' => 'tm-suspend', + 'title' => $label, + 'parent' => 'top-secondary', + 'href' => wp_nonce_url( + add_query_arg( + array( + 'action' => $action + ) + ), + 'transients_manager' + ) + ) ); + + // View + $wp_admin_bar->add_node( array( + 'id' => 'tm-view', + 'title' => esc_html__( 'View Transients', 'transients-manager' ), + 'parent' => 'tm-suspend', + 'href' => add_query_arg( + array( + 'page' => $this->page_id + ), + admin_url( 'tools.php' ) + ) + ) ); + } + + /** + * Get transients from the database + * + * These queries are uncached, to prevent race conditions with persistent + * object cache setups and the way Transients use them. + * + * @since 1.0 + * @param array $args + * @return array + */ + private function get_transients( $args = array() ) { + global $wpdb; + + // Parse arguments + $r = $this->parse_args( $args ); + + // Escape some LIKE parts + $esc_name = '%' . $wpdb->esc_like( '_transient_' ) . '%'; + $esc_time = '%' . $wpdb->esc_like( '_transient_timeout_' ) . '%'; + + // SELECT + $sql = array( 'SELECT' ); + + // COUNT + if ( ! empty( $r['count'] ) ) { + $sql[] = 'count(option_id)'; + } else { + $sql[] = '*'; + } + + // FROM + $sql[] = "FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name NOT LIKE %s"; + + // Search + if ( ! empty( $r['search'] ) ) { + $search = '%' . $wpdb->esc_like( $r['search'] ) . '%'; + $sql[] = $wpdb->prepare( "AND option_name LIKE %s", $search ); + } + + // Limits + if ( empty( $r['count'] ) ) { + $offset = absint( $r['offset'] ); + $number = absint( $r['number'] ); + $sql[] = $wpdb->prepare( "ORDER BY option_id DESC LIMIT %d, %d", $offset, $number ); + } + + // Combine the SQL parts + $query = implode( ' ', $sql ); + + // Prepare + $prepared = $wpdb->prepare( $query, $esc_name, $esc_time ); + + // Query + $transients = empty( $r['count'] ) + ? $wpdb->get_results( $prepared ) // Rows + : $wpdb->get_var( $prepared ); // Count + + // Return transients + return $transients; + } + + /** + * Parse the query arguments + * + * @since 2.0 + * @param array $args + * @return array + */ + private function parse_args( $args = array() ) { + + // Parse + $r = wp_parse_args( $args, array( + 'offset' => 0, + 'number' => 30, + 'search' => '', + 'count' => false + ) ); + + // Return + return $r; + } + + /** + * Retrieve the total number transients in the database + * + * If a search is performed, it returns the number of found results + * + * @since 1.0 + * @param string $search + * @return int + */ + private function get_total_transients( $search = '' ) { + + // Query + $count = $this->get_transients( array( + 'count' => true, + 'search' => $search + ) ); + + // Return int + return absint( $count ); + } + + /** + * Retrieve a transient by its ID + * + * @since 1.0 + * @param int $id + * @return object + */ + private function get_transient_by_id( $id = 0 ) { + global $wpdb; + + $id = absint( $id ); + + // Bail if empty ID + if ( empty( $id ) ) { + return false; + } + + // Prepare + $prepared = $wpdb->prepare( "SELECT * FROM {$wpdb->options} WHERE option_id = %d", $id ); + + // Query + return $wpdb->get_row( $prepared ); + } + + /** + * Is a transient name site-wide? + * + * @since 2.0 + * @param string $transient_name + * @return boolean + */ + private function is_site_wide( $transient_name = '' ) { + return ( false !== strpos( $transient_name, '_site_transient' ) ); + } + + /** + * Retrieve the transient name from the transient object + * + * @since 1.0 + * @return string + */ + private function get_transient_name( $transient = false ) { + + // Bail if no Transient + if ( empty( $transient ) ) { + return ''; + } + + // Position + $pos = $this->is_site_wide( $transient->option_name ) + ? 16 + : 11; + + return substr( $transient->option_name, $pos, strlen( $transient->option_name ) ); + } + + /** + * Retrieve the human-friendly transient value from the transient object + * + * @since 1.0 + * @param object $transient + * @return string/int + */ + private function get_transient_value( $transient ) { + + // Get the value type + $type = $this->get_transient_value_type( $transient ); + + // Trim value to 100 chars + $value = substr( $transient->option_value, 0, 100 ); + + // Escape & wrap in tag + $value = '' . esc_html( $value ) . ''; + + // Return + return $value . '
' . esc_html( $type ) . ''; + } + + /** + * Try to guess the type of value the Transient is + * + * @since 2.0 + * @param object $transient + * @return string + */ + private function get_transient_value_type( $transient ) { + + // Default type + $type = esc_html__( 'unknown', 'transients-manager' ); + + // Try to unserialize + $value = maybe_unserialize( $transient->option_value ); + + // Array + if ( is_array( $value ) ) { + $type = esc_html__( 'array', 'transients-manager' ); + + // Object + } elseif ( is_object( $value ) ) { + $type = esc_html__( 'object', 'transients-manager' ); + + // Serialized array + } elseif ( is_serialized( $value ) ) { + $type = esc_html__( 'serialized', 'transients-manager' ); + + // HTML + } elseif ( strip_tags( $value ) !== $value ) { + $type = esc_html__( 'html', 'transients-manager' ); + + // Scalar + } elseif ( is_scalar( $value ) ) { + + if ( is_numeric( $value ) ) { + + // Likely a timestamp + if ( 10 === strlen( $value ) ) { + $type = esc_html__( 'timestamp?', 'transients-manager' ); + + // Likely a boolean + } elseif ( in_array( $value, array( '0', '1' ), true ) ) { + $type = esc_html__( 'boolean?', 'transients-manager' ); + + // Any number + } else { + $type = esc_html__( 'numeric', 'transients-manager' ); + } + + // JSON + } elseif ( is_string( $value ) && is_object( json_decode( $value ) ) ) { + $type = esc_html__( 'json', 'transients-manager' ); + + // Scalar + } else { + $type = esc_html__( 'scalar', 'transients-manager' ); + } + + // Empty + } elseif ( empty( $value ) ) { + $type = esc_html__( 'empty', 'transients-manager' ); + } + + // Return type + return $type; + } + + /** + * Retrieve the expiration timestamp + * + * @since 1.0 + * @param object $transient + * @return int + */ + private function get_transient_expiration_time( $transient ) { + + // Get the same to use in the option key + $name = $this->get_transient_name( $transient ); + + // Get the value of the timeout + $time = $this->is_site_wide( $transient->option_name ) + ? get_option( "_site_transient_timeout_{$name}" ) + : get_option( "_transient_timeout_{$name}" ); + + // Return the value + return $time; + } + + /** + * Retrieve the human-friendly expiration time + * + * @since 1.0 + * @param object $transient + * @return string + */ + private function get_transient_expiration( $transient ) { + + $expiration = $this->get_transient_expiration_time( $transient ); + + // Bail if no expiration + if ( empty( $expiration ) ) { + return '—
' . esc_html__( 'Persistent', 'transients-manager' ) . ''; + } + + // UTC & local dates + $date_utc = gmdate( 'Y-m-d\TH:i:s+00:00', $expiration ); + $date_local = get_date_from_gmt( date( 'Y-m-d H:i:s', $expiration ), 'Y-m-d H:i:s' ); + + // Create