response ) || ! is_array( $transient->response ) ) { $transient->response = []; } if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) { $transient->no_update = []; } $addons = AddonService::instance()->get_remote_data(); $plugins = get_plugins(); foreach ( $addons as $slug => $addon ) { // Skip addons hosted on wp.org - core already handles them. if ( ! empty( $addon->is_org ) ) { continue; } $basename = $addon->basename ?? ''; if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) { continue; } $item = (object) [ 'id' => 'learnpress/' . $slug, 'slug' => $slug, 'plugin' => $basename, 'new_version' => $addon->version ?? '', 'url' => $addon->link ?? '', 'package' => '', 'icons' => isset( $addon->image ) ? [ 'default' => $addon->image ] : [], ]; $current_version = $plugins[ $basename ]['Version']; if ( ! empty( $item->new_version ) && version_compare( $current_version, $item->new_version, '<' ) ) { $transient->response[ $basename ] = $item; } else { $transient->no_update[ $basename ] = $item; } } } catch ( Throwable $e ) { LP_Debug::error_log( $e ); } return $transient; } /** * Register hooks to append update link into plugin update rows on plugins.php. * Only for installed addons not on wp.org and having a newer version. * * @return void */ public function wp_update_plugin_message(): void { try { $addons = AddonService::instance()->get_remote_data(); $plugins = get_plugins(); foreach ( $addons as $addon ) { if ( ! empty( $addon->is_org ) ) { continue; } $basename = $addon->basename ?? ''; if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) { continue; } $new_version = $addon->version ?? ''; if ( empty( $new_version ) || version_compare( $plugins[ $basename ]['Version'], $new_version, '>=' ) ) { continue; } $this->addons_need_update[ $basename ] = $addon; // Priority 5: run before core wp_plugin_update_row (priority 10) to override it. add_action( "after_plugin_row_{$basename}", array( $this, 'render_custom_update_row' ), 5, 2 ); } } catch ( Throwable $e ) { LP_Debug::error_log( $e ); } } /** * Render custom update row to override core default message. * Hook: after_plugin_row_{$basename} - priority 5, before core wp_plugin_update_row. * * @param string $file Plugin basename. * @param array $plugin_data Plugin data. * * @return void */ public function render_custom_update_row( string $file, array $plugin_data ): void { // Remove core default update row to override the message. remove_action( "after_plugin_row_{$file}", 'wp_plugin_update_row' ); $addon = $this->addons_need_update[ $file ] ?? null; if ( ! $addon ) { return; } $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); $slug = sanitize_key( $addon->basename ?? $file ); $new_version = $addon->version ?? ''; $link_update = admin_url( 'admin.php?page=learn-press-addons&tab=update' ); $section = array( 'wrap' => sprintf( '', esc_attr( $slug ), esc_attr( $slug ), esc_attr( $file ) ), 'cell_open' => sprintf( '', esc_attr( $wp_list_table->get_column_count() ) ), 'message_open' => '

', 'text' => sprintf( /* translators: 1: addon name, 2: new version */ esc_html__( 'There is a new version of %1$s (v%2$s) available.', 'learnpress' ), esc_html( $addon->name ?? $plugin_data['Name'] ), esc_html( $new_version ) ), 'link' => sprintf( ' %s', esc_url( $link_update ), esc_html__( 'Go to LearnPress Add-ons page to update.', 'learnpress' ) ), 'message_close' => '

', 'cell_close' => '', 'warp-end' => '', ); echo Template::combine_components( $section ); } /** * HTML the LearnPress Add-ons page. * * @return void */ public function html_page() { /** @use self::render_addons */ $section = apply_filters( 'learn-press/admin/addons/page-section', array( 'wrapper' => '
', 'notice' => sprintf( '
%s
', Template::print_message( sprintf( '
%s
', sprintf( __( 'To download or update add-ons: enter your purchase code (%s), or check your theme\'s plugins tab if using a bundled premium theme.', 'learnpress' ), sprintf( '%s', 'https://thimpress.com/my-account/', __( 'get key', 'learnpress' ) ), ) ), 'info', false ) ), 'subtitle' => sprintf( '

%s

', esc_html__( 'Discover high-performance Premium & Education themes optimized 100% for LearnPress LMS.', 'learnpress' ) ), 'addons' => TemplateAJAX::load_content_via_ajax( array( 'id_url' => 'data-addons', 'tab' => $_REQUEST['tab'] ?? '', ), array( 'class' => self::class, 'method' => 'render_addons', ) ), 'wrapper_end' => '
', ) ); echo AdminTemplate::html_on_wp_admin_screen( array( 'content' => Template::combine_components( $section ), 'title' => __( 'LearnPress Add-ons', 'learnpress' ), 'id' => 'learn-press-addons', ) ); } /** * Render add-ons list for the AJAX response. * * @param array $data * @return stdClass */ public static function render_addons( array $data = [] ): stdClass { $response = new stdClass(); try { // Check permission if ( is_multisite() ? ! is_super_admin() : ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) { throw new Exception( __( 'Access denied.', 'learnpress' ) ); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; $addons = AddonService::instance()->get_addons(); $plugins_installed = get_plugins(); $plugins_activated = get_option( 'active_plugins', array() ); $active_tab = LP_Helper::sanitize_params_submitted( $data['tab'] ) ?? 'all'; $keys_purchase = LP_Settings::get_option( AddonService::instance()->key_purchase_addons, array() ); $section = apply_filters( 'learn-press/admin/addons/section', array( 'wrapper' => '
', 'controls' => self::html_controls( array( 'active_tab' => $active_tab, ) ), 'list' => self::html_addons_list( array( 'addons' => $addons, 'plugins_installed' => $plugins_installed, 'plugins_activated' => $plugins_activated, 'active_tab' => $active_tab, 'keys_purchase' => $keys_purchase, ) ), 'wrapper_end' => '
', ), $addons, $data ); $response->content = Template::combine_components( $section ); } catch ( Throwable $e ) { $response->content = Template::print_message( $e->getMessage(), Response::STATUS_ERROR, false ); } return $response; } /** * Render the add-ons filter controls: tabs + search. * * @param array $data Renderer data. * * @return string */ public static function html_controls( array $data = [] ): string { $active_tab = $data['active_tab'] ?? 'all'; $tabs = array( 'all' => sprintf( '%s ()', __( 'All', 'learnpress' ) ), 'installed' => sprintf( '%s ()', __( 'Installed', 'learnpress' ) ), 'purchase' => sprintf( '%s ()', __( 'Paid', 'learnpress' ) ), 'free' => sprintf( '%s ()', __( 'Free', 'learnpress' ) ), 'update' => sprintf( '%s ()', __( 'Updated', 'learnpress' ) ), 'license' => sprintf( '%s ()', __( 'License', 'learnpress' ) ), 'not_installed' => sprintf( '%s ()', __( 'Not Installed', 'learnpress' ) ), ); $addon_categories = array( 'create-course' => __( 'Create Course', 'learnpress' ), 'engagement' => __( 'Engagement', 'learnpress' ), 'learnpress' => __( 'LearnPress', 'learnpress' ), 'manage-course' => __( 'Manage Course', 'learnpress' ), 'marketing-optimization' => __( 'Marketing Optimization', 'learnpress' ), 'monetize-course' => __( 'Monetize Course', 'learnpress' ), 'payment' => __( 'Payment', 'learnpress' ), ); $section_tabs = array(); foreach ( $tabs as $tab => $tab_title ) { $active_class = ( $tab === $active_tab ) ? ' nav-tab-active' : ''; if ( $active_class ) { $section_tabs[ $tab ] = sprintf( '%s', esc_attr( $active_class ), esc_attr( $tab ), wp_kses_post( $tab_title ) ); } else { $section_tabs[ $tab ] = sprintf( '%s', esc_attr( $tab ), esc_attr( $tab ), wp_kses_post( $tab_title ) ); } } $section = array( 'wrapper' => '
', 'toolbar' => '
', 'filter' => sprintf( '
', esc_attr__( 'Filter add-ons', 'learnpress' ) ), 'tabs' => Template::combine_components( $section_tabs ), 'filter_end' => '
', 'search' => '', 'toolbar_end' => '
', 'categories' => self::html_categories( array( 'addon_categories' => $addon_categories, ) ), 'wrapper_end' => '
', ); return Template::combine_components( $section ); } /** * Render the add-ons category filter buttons. * * @param array $data Renderer data. * * @return string */ public static function html_categories( array $data = [] ): string { $addon_categories = $data['addon_categories'] ?? array(); $section = array( 'wrapper' => sprintf( '
', esc_attr__( 'Filter add-ons by category', 'learnpress' ) ), 'all' => sprintf( '', esc_html__( 'All', 'learnpress' ) ), ); foreach ( $addon_categories as $category_slug => $category_label ) { $section[ $category_slug ] = sprintf( '', esc_attr( $category_slug ), esc_html( $category_label ) ); } $section['wrapper_end'] = '
'; return Template::combine_components( $section ); } /** * Render the list of add-on items. * * @param array $data Renderer data. * * @return string * @throws Exception */ public static function html_addons_list( array $data = [] ): string { $addons = $data['addons'] ?? array(); $html = ''; foreach ( $addons as $slug => $addon ) { $addon->slug = $slug; $html .= self::html_addon_item( $addon, $data ); } return sprintf( '
%s
', $html ); } /** * Render a single add-on card. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string * @throws Exception */ public static function html_addon_item( object $addon, array $data = [] ): string { $state = self::get_addon_classes_and_state( $addon, $data ); $section = apply_filters( 'learn-press/admin/addons/item', array( 'wrapper' => sprintf( '
', esc_attr( implode( ' ', $state['classes_status'] ) ), esc_attr( $addon->slug ), esc_attr( $state['addon_category'] ), esc_attr( htmlentities2( wp_json_encode( $addon ) ) ) ), 'content' => '
', 'header' => self::html_addon_header( $addon ), 'license' => ! $state['is_free'] ? self::html_addon_license( $addon, array( 'license_status' => $state['license_status'], 'number_days_remaining' => $state['number_days_remaining'], 'date_expired' => $state['date_expired'], 'purchase_code_masked' => $state['purchase_code_masked'], 'show_license_panel' => $state['show_license_panel'], ) ) : '', 'description' => sprintf( '

%s

', esc_attr( $addon->description ?? '' ), esc_html( $addon->description ?? '' ) ), 'meta' => self::html_addon_meta( $addon, array( 'version_current' => $state['version_current'], ) ), 'new_update' => $state['is_updated'] ? sprintf( '%s: %s', esc_html__( 'Latest version', 'learnpress' ), esc_html( $state['version_latest'] ) ) : '', 'content_end' => '
', 'actions' => self::html_addon_actions( $addon, array( 'is_free' => $state['is_free'], 'is_installed' => $state['is_installed'], 'is_updated' => $state['is_updated'], ) ), 'purchase' => self::html_purchase_panel( $addon, array( 'purchase_code_masked' => $state['purchase_code_masked'], ) ), 'wrapper_end' => '
', ), $addon, $data ); return Template::combine_components( $section ); } /** * Compute status classes and state flags for an addon. * * @param object $addon Current addon object. * @param array $data Addon state data. * * @return array * @throws Exception */ public static function get_addon_classes_and_state( object $addon, array $data = [] ): array { $plugins_installed = $data['plugins_installed'] ?? array(); $plugins_activated = $data['plugins_activated'] ?? array(); $active_tab = $data['active_tab'] ?? 'all'; $keys_purchase = $data['keys_purchase'] ?? array(); $is_free = $addon->is_free ?? 0; $addon_base = $addon->basename ?? ''; $version_latest = $addon->version ?? 0; $version_current = $addon->version ?? 0; $classes_status = array(); $addon_purchased = $addon->purchase_info ?? false; $addon_category = implode( ' ', array_map( 'sanitize_title', $addon->category ?? array() ) ); $date_expired_str = ''; $number_days_remaining = null; $date_expired = null; if ( ! $is_free ) { $classes_status[] = 'purchase'; } else { $classes_status[] = 'free'; } if ( isset( $plugins_installed[ $addon_base ] ) ) { $is_installed = true; $classes_status[] = 'installed'; $version_current = $plugins_installed[ $addon_base ]['Version']; } else { $is_installed = false; $classes_status[] = 'not_installed'; } if ( in_array( $addon_base, $plugins_activated, true ) ) { $classes_status[] = 'activated'; } $is_updated = false; if ( $is_installed && version_compare( $version_current, $version_latest, '<' ) ) { $classes_status[] = 'update'; $is_updated = true; } if ( $addon_purchased ) { $classes_status[] = 'license'; $date_expired_str = $addon_purchased->date_expire ?? ''; if ( ! empty( $date_expired_str ) ) { $date_expired = new DateTime( $date_expired_str ); $date_now = new DateTime( gmdate( 'Y-m-d' ) ); $date_diff = date_diff( $date_now, $date_expired ); $number_days_remaining = $date_diff->invert ? 0 : $date_diff->days; } } $show_license_panel = ! $is_free && $is_installed; $license_status = 'not-activated'; if ( $addon_purchased ) { $license_status = 'active'; if ( ! empty( $date_expired_str ) && isset( $number_days_remaining ) && 0 === $number_days_remaining ) { $license_status = 'expired'; $classes_status[] = 'expired'; } } $purchase_code_masked = AddonService::mask_purchase_code( $keys_purchase[ $addon->slug ] ?? '' ); if ( ! in_array( $active_tab, $classes_status, true ) && 'all' !== $active_tab ) { $classes_status[] = 'hide'; } return array( 'classes_status' => $classes_status, 'is_free' => $is_free, 'is_installed' => $is_installed, 'is_updated' => $is_updated, 'version_current' => $version_current, 'version_latest' => $version_latest, 'license_status' => $license_status, 'number_days_remaining' => $number_days_remaining, 'date_expired' => $date_expired, 'purchase_code_masked' => $purchase_code_masked, 'show_license_panel' => $show_license_panel, 'addon_category' => $addon_category, ); } /** * Render the header section of an addon item. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_addon_header( object $addon, array $data = [] ): string { $section = array( 'wrapper' => '
', 'image' => sprintf( '', esc_url( $addon->image ?? '' ) ), 'heading' => '
', 'title' => '

', 'link' => sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html( $addon->name ?? '' ) ), 'title_end' => '

', 'badge' => ! empty( $addon->badge ) ? sprintf( '%s', esc_html( $addon->badge ) ) : '', 'heading_end' => '
', 'wrapper_end' => '
', ); return Template::combine_components( $section ); } /** * Render the license status block for a paid addon. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_addon_license( object $addon, array $data = [] ): string { $license_status = $data['license_status'] ?? ''; $number_days_remaining = $data['number_days_remaining'] ?? null; /** @var DateTime $date_expired */ $date_expired = $data['date_expired'] ?? null; $purchase_code_masked = $data['purchase_code_masked'] ?? ''; $show_license_panel = $data['show_license_panel'] ?? false; $license_status_label = 'active' === $license_status ? __( 'Active', 'learnpress' ) : ( 'expired' === $license_status ? __( 'Expired', 'learnpress' ) : ( 'deactivated' === $license_status ? __( 'Deactivated', 'learnpress' ) : __( 'Not Activated', 'learnpress' ) ) ); $expiry_hidden = empty( $date_expired ) || ! in_array( $license_status, array( 'active', 'expired' ), true ) ? ' hidden' : ''; $expiry_text = ''; if ( ! empty( $date_expired ) ) { $lpDate = new LPDateTime( $date_expired->format( LPDateTime::FORMAT_MYSQL ) ); $expiry_format = 'expired' === $license_status ? __( '(on %s)', 'learnpress' ) : __( '(Updates until %s)', 'learnpress' ); $expiry_text = esc_html( sprintf( $expiry_format, $lpDate->format( LPDateTime::FORMAT_I18N_DATE ) ) ); } $extend_link = ''; if ( 'expired' === $license_status ) { $extend_link = sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html__( 'Extend now', 'learnpress' ) ); } $message = ''; $button_extends = ''; if ( ! empty( $addon->purchase_info ) ) { $button_extends = sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html__( 'Extend now', 'learnpress' ) ); if ( isset( $number_days_remaining ) && $number_days_remaining > 0 && $number_days_remaining < 61 ) { $message = sprintf( __( 'You have a license for this item with %s day(s) of update & support remaining. Please extend your update & support license to continue receiving the latest versions and customer support from Thimpress before it expires.', 'learnpress' ), sprintf( '%d', $number_days_remaining ) ); } else { $button_extends = ''; } } $need_extend = ''; if ( ! empty( $message ) || ! empty( $button_extends ) ) { $need_extend = sprintf( '%s %s', $message, $button_extends ); } $section = array( 'wrapper' => sprintf( '
', esc_attr( $license_status ), esc_attr( $purchase_code_masked ), $show_license_panel ? '' : ' hidden' ), 'summary' => '
', 'label' => sprintf( '%s', esc_html__( 'License:', 'learnpress' ) ), 'status' => sprintf( '%s', esc_html( $license_status_label ) ), 'expiry' => sprintf( '%s', $expiry_hidden, $expiry_text ), 'extend_link' => $extend_link, 'summary_end' => '
', 'need_extend' => $need_extend, 'manage' => sprintf( '', esc_html__( 'Manage', 'learnpress' ) ), 'wrapper_end' => '
', ); return Template::combine_components( $section ); } /** * Render addon meta: version, docs, view details. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_addon_meta( object $addon, array $data = [] ): string { $version_current = $data['version_current'] ?? ''; $section = array( 'wrapper' => '
', 'version' => $version_current ? sprintf( '%s %s', esc_html__( 'Version', 'learnpress' ), esc_html( $version_current ) ) : '', 'docs' => ! empty( $addon->link_doc ) ? sprintf( '%s', esc_url( $addon->link_doc ), esc_html__( 'Docs', 'learnpress' ) ) : '', 'details' => sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html__( 'View Details', 'learnpress' ) ), 'settings' => ! empty( $addon->setting ) ? sprintf( '%s', esc_url( site_url( $addon->setting ) ), esc_html__( 'Settings', 'learnpress' ) ) : '', 'wrapper_end' => '
', ); return Template::combine_components( $section ); } /** * Render addon price block. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_addon_price( object $addon, array $data = [] ): string { $is_free = $addon->is_free ?? 0; if ( $is_free ) { return sprintf( '%s', esc_html__( 'Free', 'learnpress' ) ); } if ( ! empty( $addon->sale_price ) ) { $section = array( 'sale' => sprintf( '$%s', esc_html( number_format_i18n( $addon->sale_price ) ) ), 'regular' => sprintf( '$%s', esc_html( number_format_i18n( $addon->regular_price ?? 0 ) ) ), ); return Template::combine_components( $section ); } return sprintf( '$%s', esc_html( number_format_i18n( $addon->regular_price ?? 0 ) ) ); } /** * Render addon action buttons. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_addon_actions( object $addon, array $data = [] ): string { $is_free = $data['is_free'] ?? false; $section_right = apply_filters( 'learn-press/admin/addons/actions/section_right', array( 'install' => $is_free ? sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html__( 'Install', 'learnpress' ) ) : sprintf( '', esc_html__( 'Install', 'learnpress' ) ), 'purchase' => sprintf( '', esc_html__( 'Install', 'learnpress' ) ), 'update' => sprintf( '', esc_html__( 'Update', 'learnpress' ) ), 'deactivate' => sprintf( '', esc_html__( 'Deactivate', 'learnpress' ) ), 'activate' => sprintf( '', esc_html__( 'Activate', 'learnpress' ) ), ), $addon, $data ); $section = array( 'wrapper' => '
', 'actions_left' => '
', 'price' => sprintf( '
%s
', self::html_addon_price( $addon ) ), 'actions_left_end' => '
', 'actions_right' => '
', 'actions' => Template::combine_components( $section_right ), 'actions_right_end' => '
', 'wrapper_end' => '
', ); return Template::combine_components( $section ); } /** * Render the purchase-code overlay panel. * * @param object $addon Current addon object. * @param array $data Renderer data. * * @return string */ public static function html_purchase_panel( object $addon, array $data = [] ): string { $panel = array( 'wrapper' => '
', 'header' => '
', 'title' => sprintf( '%s', esc_html__( 'Purchase Code', 'learnpress' ) ), 'close' => sprintf( '', 'header_end' => '
', 'field' => '', 'submit' => '', 'divider' => sprintf( '
%s
', esc_html__( 'Don\'t have a code?', 'learnpress' ) ), 'buy' => sprintf( '%s', esc_url( $addon->link ?? '' ), esc_html__( 'Buy Now', 'learnpress' ) ), 'wrapper_end' => '
', ); $section = array( 'wrapper' => '
', 'purchase' => '
', 'panel' => Template::combine_components( $panel ), 'purchase_code' => '', 'purchase_end' => '
', 'wrapper_end' => '
', ); return Template::combine_components( $section ); } }