diff --git a/assets/css/admin-notices.scss b/assets/css/admin-notices.scss index ce450aa534..d991d6bf61 100644 --- a/assets/css/admin-notices.scss +++ b/assets/css/admin-notices.scss @@ -64,19 +64,19 @@ $sensei-notice-icon-width: 30px; position: static; } - &.sensei-notice-error { + &--error { border-left-color: $notice-error; } - &.sensei-notice-warning { + &--warning { border-left-color: $notice-warning; } - &.sensei-notice-info { + &--info { border-left-color: $notice-info; } - &.sensei-notice-success { + &--success { border-left-color: $notice-success; } } diff --git a/assets/css/senseilms-licensing.scss b/assets/css/senseilms-licensing.scss new file mode 100644 index 0000000000..a46e3075cf --- /dev/null +++ b/assets/css/senseilms-licensing.scss @@ -0,0 +1,8 @@ +#section-changelog { + h2 { + clear: none; + } + h3 { + margin: 0; + } +} diff --git a/assets/home/notices.js b/assets/home/notices.js index 48e52d0cab..e0bc249c7c 100644 --- a/assets/home/notices.js +++ b/assets/home/notices.js @@ -103,7 +103,7 @@ const NoticeInfoLink = ( { infoLink } ) => { const Notice = ( { noticeId, notice, dismissNonce } ) => { let noticeClass = ''; if ( !! notice.level ) { - noticeClass = 'sensei-notice-' + notice.level; + noticeClass = 'sensei-notice--' + notice.level; } const isDismissible = notice.dismissible && dismissNonce; diff --git a/changelog/add-invalid-license-update-disclaimer b/changelog/add-invalid-license-update-disclaimer new file mode 100644 index 0000000000..99adfea04e --- /dev/null +++ b/changelog/add-invalid-license-update-disclaimer @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add disclamer with the reason that Sensei Pro can't be updated when license is not active diff --git a/changelog/add-notice-condition-date-range b/changelog/add-notice-condition-date-range new file mode 100644 index 0000000000..15004011a0 --- /dev/null +++ b/changelog/add-notice-condition-date-range @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Ability to set conditions on admin notices based on a date range diff --git a/includes/admin/class-sensei-admin-notices.php b/includes/admin/class-sensei-admin-notices.php index 92aa0988fe..42843ac8f2 100644 --- a/includes/admin/class-sensei-admin-notices.php +++ b/includes/admin/class-sensei-admin-notices.php @@ -142,7 +142,6 @@ protected function get_notices( $max_age = null ) { $transient_key = implode( '_', [ 'sensei_notices', Sensei()->version, determine_locale() ] ); $data = get_transient( $transient_key ); $notices = false; - // If the data is too old, fetch it again. if ( $max_age && is_array( $data ) ) { $age = time() - ( $data['_fetched'] ?? 0 ); @@ -236,20 +235,18 @@ private function add_admin_notice( $notice_id, $notice ) { $notice['actions'] = []; } - $notice_class = ''; - if ( ! empty( $notice['style'] ) ) { - $notice_class = 'sensei-notice-' . $notice['style']; - } + $notice_classes = []; + $notice_classes[] = 'sensei-notice--' . $notice['level']; $is_dismissible = $notice['dismissible']; $notice_wrapper_extra = ''; if ( $is_dismissible ) { wp_enqueue_script( 'sensei-dismiss-notices' ); - $notice_class .= ' is-dismissible'; + $notice_classes[] = 'is-dismissible'; $notice_wrapper_extra = sprintf( ' data-dismiss-action="sensei_dismiss_notice" data-dismiss-notice="%1$s" data-dismiss-nonce="%2$s"', esc_attr( $notice_id ), esc_attr( wp_create_nonce( self::DISMISS_NOTICE_NONCE_ACTION ) ) ); } ?> -
condition_check_date_range( $condition['start_date'] ?? null, $condition['end_date'] ?? null ) ) { + $can_see_notice = false; + break 2; + } + + break; } } @@ -506,6 +515,8 @@ private function condition_check_screen( array $allowed_screens, $screen_id = nu /** * Check an "installed since" condition * + * @since 4.10.0 + * * @param int|string $installed_since Time to check the installation time for. * * @return bool @@ -521,6 +532,39 @@ private function condition_installed_since( $installed_since ) : bool { return $installed_at <= $installed_since; } + /** + * Check a date range condition. + * + * @since $$next-version$$ + * + * @param ?string $start_date_str Start date. + * @param ?string $end_date_str End date. + * + * @return bool + */ + private function condition_check_date_range( ?string $start_date_str, ?string $end_date_str ) : bool { + $now = new DateTime(); + + // Defaults to WP timezone, but can be overridden by passing string that includes timezone. + $start_date = $start_date_str ? date_create( $start_date_str, wp_timezone() ) : null; + $end_date = $end_date_str ? date_create( $end_date_str, wp_timezone() ) : null; + + // If the passed date strings are invalid, don't show the notice. + if ( false === $start_date || false === $end_date ) { + return false; + } + + if ( $start_date && $now < $start_date ) { + return false; + } + + if ( $end_date && $now > $end_date ) { + return false; + } + + return true; + } + /** * Check a plugin condition. * @@ -616,6 +660,11 @@ private function normalize_notice( $notice ) { ]; } + $notice_levels = [ 'error', 'warning', 'success', 'info' ]; + if ( ! isset( $notice['level'] ) || ! in_array( $notice['level'], $notice_levels, true ) ) { + $notice['level'] = 'info'; + } + if ( ! isset( $notice['dismissible'] ) ) { $notice['dismissible'] = true; } diff --git a/includes/admin/class-senseilms-plugin-updater.php b/includes/admin/class-senseilms-plugin-updater.php new file mode 100644 index 0000000000..70812369fd --- /dev/null +++ b/includes/admin/class-senseilms-plugin-updater.php @@ -0,0 +1,261 @@ +plugin_full_name = plugin_basename( $main_plugin_file_absolute_path ); + $this->plugin_slug = basename( $main_plugin_file_absolute_path, '.php' ); + $this->version = $version; + } + + /** + * Initialize the plugin updater. + */ + public static function init() { + add_action( 'plugins_loaded', [ __CLASS__, 'plugins_loaded' ] ); + } + + /** + * Add hooks after the `plugins_loaded`, so we make sure Sensei Pro was + * already loaded. + */ + public static function plugins_loaded() { + // Early return if license and updates are managed by WooCommerce. + if ( defined( 'SENSEI_COMPAT_PLUGIN' ) && SENSEI_COMPAT_PLUGIN ) { + return; + } + + if ( class_exists( '\Sensei_Pro_Interactive_Blocks\Setup_Context' ) ) { + $instance = new self( SENSEI_IB_PLUGIN_FILE, SENSEI_IB_VERSION ); + } elseif ( class_exists( '\Sensei_Pro\Setup_Context' ) ) { + $instance = new self( SENSEI_PRO_PLUGIN_FILE, SENSEI_PRO_VERSION ); + } else { + return; + } + + add_filter( 'plugins_api', [ $instance, 'get_plugin_info' ], 15, 3 ); + add_filter( 'site_transient_update_plugins', [ $instance, 'maybe_inject_custom_update_to_update_plugins_transient' ], 15 ); + add_action( 'in_plugin_update_message-' . $instance->plugin_full_name, [ $instance, 'invalid_license_update_disclaimer' ] ); + } + + /** + * Get plugin information as expected by the `plugins_api` hook. + * This will be called to display the details for the updated in the detailed view. + * + * @param false|object|array $res Result. As defined per the `plugins_api` hook. + * @param string $action The action being executed. As defined per the `plugins_api` hook. + * @param object $args The arguments. As defined per the `plugins_api` hook. + * + * @hooked plugins_api + * + * @return false|object If other than false is returned the actual call to wordpress.org is not done. + */ + public function get_plugin_info( $res, $action, $args ) { + if ( + 'plugin_information' !== $action + || $this->plugin_slug !== $args->slug + || false !== $res + ) { + return $res; + } + + $remote = $this->request_info(); + if ( is_wp_error( $remote ) ) { + // Early return in case request to SenseiLMS.com failed. + return $res; + } + + $res = new stdClass(); + $res->name = $remote->name; + $res->slug = $remote->slug; + $res->author = $remote->author; + $res->version = $remote->version; + $res->requires = $remote->requires; + $res->tested = $remote->tested; + $res->requires_php = $remote->requires_php; + $res->last_updated = $remote->last_updated; + $res->sections = [ + 'description' => $remote->sections->description, + 'installation' => $remote->sections->installation, + 'changelog' => $remote->sections->changelog, + ]; + $res->download_link = $remote->download_url; + $res->banners = [ + 'low' => $remote->banners->{'1x'}, + 'high' => $remote->banners->{'2x'}, + ]; + + Sensei()->assets->enqueue( 'sensei-updater-styles', 'css/senseilms-licensing.css' ); + + return $res; + } + + /** + * Potentially injects the details for a new plugin version by checking against the remote server. + * This is done by hooking into the `update_plugins` transient by using the `site_transient_update_plugins` hook. + * + * @param mixed $transient The plugin_update transient. + * + * @hooked site_transient_update_plugins See reference for `site_transient_transient`. + * + * @return mixed + */ + public function maybe_inject_custom_update_to_update_plugins_transient( $transient ) { + + // Skip empty transients or if it was already set by Sensei Pro. + if ( empty( $transient ) || isset( $transient->response[ $this->plugin_full_name ] ) ) { + return $transient; + } + + $remote = $this->request_info(); + if ( is_wp_error( $remote ) ) { + // Request failed so do not inject anything into the transient. + return $transient; + } + + if ( + $remote + && version_compare( $this->version, $remote->version, '<' ) + && version_compare( get_bloginfo( 'version' ), $remote->requires, '>=' ) + && version_compare( PHP_VERSION, $remote->requires_php, '>=' ) + ) { + + $res = new stdClass(); + $res->slug = $remote->slug; + $res->plugin = $this->plugin_full_name; + $res->new_version = $remote->version; + $res->tested = $remote->tested; + $res->package = $remote->download_url; + $res->icons = (array) $remote->icons; + $transient->response[ $res->plugin ] = $res; + } + return $transient; + } + + /** + * Helper function that retrieves the latest version information from the remote server if there is a valid license in the system. + * This function caches remote response by using transients. + * + * @return array|WP_Error + */ + private function request_info() { + $cache_key = self::CACHE_KEY_PREFIX . $this->plugin_slug; + $remote = get_transient( $cache_key ); + + if ( false === $remote ) { + // @phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $api_url = apply_filters( 'senseilms_licensing_api', 'https://senseilms.com/wp-json' ); + + $remote = wp_remote_get( + add_query_arg( + [ + 'plugin_slug' => $this->plugin_slug, + 'ts' => time(), // Adding some timestamp to workaround cache issues. + ], + $api_url . '/plugin-updater/v1/info' + ), + [ + 'timeout' => 10, + 'headers' => [ + 'Accept' => 'application/json', + 'Cache-Control' => 'no-cache', + ], + ] + ); + + // Caching any response. + set_transient( $cache_key, $remote, self::CACHE_TTL ); + } + + // Check response for errors. + if ( + is_wp_error( $remote ) + || 200 !== wp_remote_retrieve_response_code( $remote ) + || empty( wp_remote_retrieve_body( $remote ) ) + ) { + return new WP_Error( 'remote-error', __( 'Remote answered with an error.', 'sensei-lms' ) ); + } + + // Check response for valid json. + $response = json_decode( wp_remote_retrieve_body( $remote ) ); + if ( is_null( $response ) ) { + return new WP_Error( 'invalid-remote-response', __( 'Remote answered with an invalid response.', 'sensei-lms' ) ); + } + + return $response; + } + + /** + * Add update disclaimer for invalid license. + * + * @since $$next-version$$ + * + * @internal + */ + public function invalid_license_update_disclaimer() { + if ( ! class_exists( '\SenseiLMS_Licensing\License_Manager' ) ) { + return; + } + + // Checks if Sensei Pro method exists. So it's already being done there. + if ( class_exists( '\SenseiLMS_Licensing\SenseiLMS_Plugin_Updater' ) && method_exists( '\SenseiLMS_Licensing\SenseiLMS_Plugin_Updater', 'invalid_license_update_disclaimer' ) ) { + return; + } + + $license_status = \SenseiLMS_Licensing\License_Manager::get_license_status( $this->plugin_slug ); + if ( ! $license_status['is_valid'] ) { + printf( + '
%s', + esc_html__( 'Update will be available after you activate your license.', 'sensei-lms' ) + ); + } + } +} diff --git a/includes/admin/home/notices/class-sensei-home-notices-provider.php b/includes/admin/home/notices/class-sensei-home-notices-provider.php index c7c2fc52a7..e5808835d1 100644 --- a/includes/admin/home/notices/class-sensei-home-notices-provider.php +++ b/includes/admin/home/notices/class-sensei-home-notices-provider.php @@ -96,14 +96,8 @@ public function get_badge_count(): int { * @return array */ private function format_item( $notice ) { - $level = 'info'; - if ( array_key_exists( 'level', $notice ) ) { - $level = $notice['level']; - } elseif ( array_key_exists( 'style', $notice ) ) { - $level = $notice['style']; - } return [ - 'level' => $level, + 'level' => $notice['level'] ?? 'info', 'heading' => $notice['heading'] ?? null, 'message' => $notice['message'], 'info_link' => $notice['info_link'] ?? null, diff --git a/includes/admin/home/notices/class-sensei-home-notices.php b/includes/admin/home/notices/class-sensei-home-notices.php index cad7dcbea5..540940bb69 100644 --- a/includes/admin/home/notices/class-sensei-home-notices.php +++ b/includes/admin/home/notices/class-sensei-home-notices.php @@ -289,10 +289,11 @@ private function get_local_plugin_updates() { * Get the base settings for a plugin update notice. * * @param array $plugin_data The plugin update data. + * @param array $screens The screens to show the notice on. * * @return array */ - private function get_base_plugin_notice( $plugin_data ) { + private function get_base_plugin_notice( $plugin_data, $screens = [] ) { $changelog_url = $plugin_data['changelog'] ?? false; $info_link = false; @@ -302,18 +303,21 @@ private function get_base_plugin_notice( $plugin_data ) { 'url' => esc_url_raw( $changelog_url ), ]; } + if ( empty( $screens ) ) { + $screens = [ $this->screen_id ]; + } // We only want this to be dismissible if Sensei LMS is active and available because it can handle the dismiss requests. $is_dismissible = class_exists( 'Sensei_Admin_Notices' ); return [ - 'level' => 'info', + 'level' => 'warning', 'type' => 'site-wide', 'info_link' => $info_link, 'conditions' => [ [ 'type' => 'screens', - 'screens' => [ $this->screen_id ], + 'screens' => $screens, ], ], 'dismissible' => $is_dismissible, @@ -389,7 +393,7 @@ private function get_plugin_update_notice( array $plugin_data ): array { $plugin_file = $plugin_data['plugin_basename']; $latest_version = $plugin_data['latest_version']; - $notice = $this->get_base_plugin_notice( $plugin_data ); + $notice = $this->get_base_plugin_notice( $plugin_data, [ 'sensei*' ] ); $notice['message'] = wp_kses( sprintf( // translators: First placeholder is the plugin name and second placeholder is the latest version available. diff --git a/includes/admin/views/html-admin-page-home.php b/includes/admin/views/html-admin-page-home.php index 069a673046..021257dc4b 100644 --- a/includes/admin/views/html-admin-page-home.php +++ b/includes/admin/views/html-admin-page-home.php @@ -12,7 +12,7 @@