PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 All 142 releases
← All changes | inc/TemplateHooks/Admin/AdminAddonsPage.php +995 -25 4.4.5 → 4.4.10 View file →
@@ -1,10 +1,22 @@
1 1 <?php
2 2
3 3 namespace LearnPress\TemplateHooks\Admin;
4 4
5 +use DateTime;
6 +use Exception;
7 +use LearnPress\Helpers\LPDateTime;
8 +use LearnPress\Helpers\Response;
5 9 use LearnPress\Helpers\Singleton;
6 10 use LearnPress\Helpers\Template;
11 +use LearnPress\Models\UserModel;
12 +use LearnPress\Services\AddonService;
13 +use LearnPress\TemplateHooks\TemplateAJAX;
14 +use LP_Debug;
15 +use LP_Helper;
16 +use LP_Settings;
17 +use stdClass;
18 +use Throwable;
7 19
8 20 defined( 'ABSPATH' ) || exit();
9 21
10 22 /**
@@ -10,54 +22,1012 @@
10 22 /**
11 23 * Admin Add-ons page renderer.
12 24 *
13 25 * @since 4.2.8
26 + * @version 1.0.2
14 27 */
15 28 class AdminAddonsPage {
16 29 use Singleton;
17 30
18 31 /**
19 - * Singleton initialization hook.
32 + * @var array Addons installed, not on wp.org, having newer version. Keyed by plugin basename.
33 + */
34 + private $addons_need_update = [];
35 +
36 + /**
37 + * Init hooks.
20 38 *
21 39 * @return void
22 40 */
23 - public function init(): void {}
41 + public function init(): void {
42 + if ( is_admin() ) {
43 + // Show update notice for addons not on wp.org on plugins.php page.
44 + add_filter( 'site_transient_update_plugins', array( $this, 'inject_update_wp_plugins' ) );
45 + add_action( 'load-plugins.php', array( $this, 'wp_update_plugin_message' ) );
46 + }
47 + }
24 48
25 49 /**
26 - * Render the LearnPress Add-ons page.
50 + * Inject update info of addons not on wp.org into update_plugins transient
51 + * so WP core renders update rows on wp-admin/plugins.php.
27 52 *
53 + * @param object $transient Value of site_transient_update_plugins.
54 + *
55 + * @return object
56 + */
57 + public function inject_update_wp_plugins( $transient ): object {
58 + try {
59 + if ( ! is_object( $transient ) ) {
60 + $transient = new stdClass();
61 + }
62 +
63 + if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
64 + $transient->response = [];
65 + }
66 +
67 + if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) {
68 + $transient->no_update = [];
69 + }
70 +
71 + $addons = AddonService::instance()->get_addons();
72 + $plugins = get_plugins();
73 +
74 + foreach ( $addons as $slug => $addon ) {
75 + // Skip addons hosted on wp.org - core already handles them.
76 + if ( ! empty( $addon->is_org ) ) {
77 + continue;
78 + }
79 +
80 + $basename = $addon->basename ?? '';
81 + if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
82 + continue;
83 + }
84 +
85 + $item = (object) [
86 + 'id' => 'learnpress/' . $slug,
87 + 'slug' => $slug,
88 + 'plugin' => $basename,
89 + 'new_version' => $addon->version ?? '',
90 + 'url' => $addon->link ?? '',
91 + 'package' => '',
92 + 'icons' => isset( $addon->image ) ? [ 'default' => $addon->image ] : [],
93 + ];
94 +
95 + $current_version = $plugins[ $basename ]['Version'];
96 + if ( ! empty( $item->new_version ) && version_compare( $current_version, $item->new_version, '<' ) ) {
97 + $transient->response[ $basename ] = $item;
98 + } else {
99 + $transient->no_update[ $basename ] = $item;
100 + }
101 + }
102 + } catch ( Throwable $e ) {
103 + LP_Debug::error_log( $e );
104 + }
105 +
106 + return $transient;
107 + }
108 +
109 + /**
110 + * Register hooks to append update link into plugin update rows on plugins.php.
111 + * Only for installed addons not on wp.org and having a newer version.
112 + *
28 113 * @return void
29 114 */
115 + public function wp_update_plugin_message(): void {
116 + try {
117 + $addons = AddonService::instance()->get_addons();
118 + $plugins = get_plugins();
119 +
120 + foreach ( $addons as $addon ) {
121 + if ( ! empty( $addon->is_org ) ) {
122 + continue;
123 + }
124 +
125 + $basename = $addon->basename ?? '';
126 + if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
127 + continue;
128 + }
129 +
130 + $new_version = $addon->version ?? '';
131 + if ( empty( $new_version )
132 + || version_compare( $plugins[ $basename ]['Version'], $new_version, '>=' ) ) {
133 + continue;
134 + }
135 +
136 + $this->addons_need_update[ $basename ] = $addon;
137 +
138 + // Priority 5: run before core wp_plugin_update_row (priority 10) to override it.
139 + add_action( "after_plugin_row_{$basename}", array( $this, 'render_custom_update_row' ), 5, 2 );
140 + }
141 + } catch ( Throwable $e ) {
142 + LP_Debug::error_log( $e );
143 + }
144 + }
145 +
146 + /**
147 + * Render custom update row to override core default message.
148 + * Hook: after_plugin_row_{$basename} - priority 5, before core wp_plugin_update_row.
149 + *
150 + * @param string $file Plugin basename.
151 + * @param array $plugin_data Plugin data.
152 + *
153 + * @return void
154 + */
155 + public function render_custom_update_row( string $file, array $plugin_data ): void {
156 + // Remove core default update row to override the message.
157 + remove_action( "after_plugin_row_{$file}", 'wp_plugin_update_row' );
158 +
159 + $addon = $this->addons_need_update[ $file ] ?? null;
160 + if ( ! $addon ) {
161 + return;
162 + }
163 +
164 + $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
165 + $slug = sanitize_key( $addon->basename ?? $file );
166 + $new_version = $addon->version ?? '';
167 + $link_update = admin_url( 'admin.php?page=learn-press-addons&tab=update' );
168 +
169 + $section = array(
170 + 'wrap' => sprintf(
171 + '<tr class="plugin-update-tr active" id="%s-update" data-slug="%s" data-plugin="%s">',
172 + esc_attr( $slug ),
173 + esc_attr( $slug ),
174 + esc_attr( $file )
175 + ),
176 + 'cell_open' => sprintf(
177 + '<td colspan="%s" class="plugin-update colspanchange">',
178 + esc_attr( $wp_list_table->get_column_count() )
179 + ),
180 + 'message_open' => '<div class="update-message notice inline notice-warning notice-alt"><p>',
181 + 'text' => sprintf(
182 + /* translators: 1: addon name, 2: new version */
183 + esc_html__( 'There is a new version of %1$s (v%2$s) available.', 'learnpress' ),
184 + esc_html( $addon->name ?? $plugin_data['Name'] ),
185 + esc_html( $new_version )
186 + ),
187 + 'link' => sprintf(
188 + ' <a href="%s">%s</a>',
189 + esc_url( $link_update ),
190 + esc_html__( 'Go to LearnPress Add-ons page to update.', 'learnpress' )
191 + ),
192 + 'message_close' => '</p></div>',
193 + 'cell_close' => '</td>',
194 + 'warp-end' => '</tr>',
195 + );
196 +
197 + echo Template::combine_components( $section );
198 + }
199 +
200 + /**
201 + * HTML the LearnPress Add-ons page.
202 + *
203 + * @return void
204 + */
30 205 public function html_page() {
31 - ob_start();
32 - lp_skeleton_animation_html( 20 );
33 - $html_loading = ob_get_clean();
206 + $section = apply_filters(
207 + 'learn-press/admin/addons/page-section',
208 + array(
209 + 'wrapper' => '<div class="lp-addons-page">',
210 + 'notice' => sprintf(
211 + '<div class="lp-addon-notice">%s</div>',
212 + Template::print_message(
213 + sprintf(
214 + '<div>%s</div>',
215 + sprintf(
216 + __( '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' ),
217 + sprintf(
218 + '<a href="%s">%s</a>',
219 + 'https://thimpress.com/my-account/',
220 + __( 'get key', 'learnpress' )
221 + ),
222 + )
223 + ),
224 + 'info',
225 + false
226 + )
227 + ),
228 + 'title' => sprintf(
229 + '<h1 class="lp-be-page-title">%s</h1>',
230 + esc_html__( 'LearnPress Add-ons New', 'learnpress' )
231 + ),
232 + 'subtitle' => sprintf(
233 + '<p class="lp-be-page-subtitle">%s</p>',
234 + esc_html__(
235 + 'Discover high-performance Premium & Education themes optimized 100% for LearnPress LMS.',
236 + 'learnpress'
237 + )
238 + ),
239 + /** @use self::render_addons */
240 + 'addons' => TemplateAJAX::load_content_via_ajax(
241 + array(
242 + 'id_url' => 'data-addons',
243 + 'tab' => $_REQUEST['tab'] ?? '',
244 + 'ex-date-test' => $_REQUEST['ex-date-test'] ?? '',
245 + ),
246 + array(
247 + 'class' => self::class,
248 + 'method' => 'render_addons',
249 + )
250 + ),
251 + 'wrapper_end' => '</div>',
252 + )
253 + );
34 254
255 + echo AdminTemplate::html_on_wp_admin_screen(
256 + array(
257 + 'content' => Template::combine_components( $section ),
258 + 'title' => '',
259 + 'id' => 'learn-press-addons',
260 + 'hide_notices_wp' => true,
261 + )
262 + );
263 + }
264 +
265 + /**
266 + * Render add-ons list for the AJAX response.
267 + *
268 + * @param array $data
269 + * @return stdClass
270 + */
271 + public static function render_addons( array $data = [] ): stdClass {
272 + $response = new stdClass();
273 +
274 + try {
275 + // Check permission
276 + if ( is_multisite() ? ! is_super_admin() : ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {
277 + throw new Exception( __( 'Access denied.', 'learnpress' ) );
278 + }
279 +
280 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
281 +
282 + $addons = AddonService::instance()->get_addons();
283 + $plugins_installed = get_plugins();
284 + $plugins_activated = get_option( 'active_plugins', array() );
285 + $active_tab = ! empty( $data['tab'] ) ? LP_Helper::sanitize_params_submitted( $data['tab'] ) : 'all';
286 + $keys_purchase = LP_Settings::get_option( AddonService::instance()->key_purchase_addons, array() );
287 +
288 + $section = apply_filters(
289 + 'learn-press/admin/addons/section',
290 + array(
291 + 'wrapper' => '<div class="lp-addons-wrapper">',
292 + 'controls' => self::html_controls(
293 + array(
294 + 'active_tab' => $active_tab,
295 + )
296 + ),
297 + 'list' => self::html_addons_list(
298 + array(
299 + 'addons' => $addons,
300 + 'plugins_installed' => $plugins_installed,
301 + 'plugins_activated' => $plugins_activated,
302 + 'active_tab' => $active_tab,
303 + 'keys_purchase' => $keys_purchase,
304 + 'ex-date-test' => $data['ex-date-test'] ?? '',
305 + )
306 + ),
307 + 'wrapper_end' => '</div>',
308 + ),
309 + $addons,
310 + $data
311 + );
312 +
313 + $response->content = Template::combine_components( $section );
314 + } catch ( Throwable $e ) {
315 + $response->content = Template::print_message(
316 + $e->getMessage(),
317 + Response::STATUS_ERROR,
318 + false
319 + );
320 + }
321 +
322 + return $response;
323 + }
324 +
325 + /**
326 + * Render the add-ons filter controls: tabs + search.
327 + *
328 + * @param array $data Renderer data.
329 + *
330 + * @return string
331 + */
332 + public static function html_controls( array $data = [] ): string {
333 + $active_tab = ! empty( $data['active_tab'] ) ? $data['active_tab'] : 'all';
334 + $tabs = array(
335 + 'all' => sprintf( '%s (<span></span>)', __( 'All', 'learnpress' ) ),
336 + 'installed' => sprintf( '%s (<span></span>)', __( 'Installed', 'learnpress' ) ),
337 + 'purchase' => sprintf( '%s (<span></span>)', __( 'Paid', 'learnpress' ) ),
338 + 'free' => sprintf( '%s (<span></span>)', __( 'Free', 'learnpress' ) ),
339 + 'update' => sprintf( '%s (<span></span>)', __( 'Updated Available', 'learnpress' ) ),
340 + 'license' => sprintf( '%s (<span></span>)', __( 'Licensed', 'learnpress' ) ),
341 + 'not_installed' => sprintf( '%s (<span></span>)', __( 'Not Installed', 'learnpress' ) ),
342 + );
343 + $addon_categories = array(
344 + 'create-course' => __( 'Create Course', 'learnpress' ),
345 + 'manage-course' => __( 'Manage Course', 'learnpress' ),
346 + 'monetize-course' => __( 'Monetize Course', 'learnpress' ),
347 + 'payment' => __( 'Payment', 'learnpress' ),
348 + 'engagement' => __( 'Engagement', 'learnpress' ),
349 + 'marketing-optimization' => __( 'Marketing Optimization', 'learnpress' ),
350 + );
351 +
352 + $section_tabs = array();
353 + foreach ( $tabs as $tab => $tab_title ) {
354 + $is_active = ( $tab === $active_tab );
355 + $active_class = $is_active ? ' nav-tab-active' : '';
356 + $aria_pressed = $is_active ? 'true' : 'false';
357 +
358 + $section_tabs[ $tab ] = sprintf(
359 + '<button type="button" class="lp-addons-filter__item nav-tab lp-be-nav-tab%s" data-tab="%s" aria-pressed="%s">%s</button>',
360 + esc_attr( $active_class ),
361 + esc_attr( $tab ),
362 + esc_attr( $aria_pressed ),
363 + wp_kses_post( $tab_title )
364 + );
365 + }
366 +
367 + $section = array(
368 + 'wrapper' => '<div class="lp-addons-controls">',
369 + 'toolbar' => '<div class="lp-nav-tab-wrapper lp-addons-toolbar lp-be-toolbar">',
370 + 'toolbar_primary' => '<div class="lp-addons-toolbar__primary lp-be-toolbar__main">',
371 + 'filter' => sprintf(
372 + '<div class="lp-addons-filter" role="group" aria-label="%s">
373 + <button type="button" class="lp-addons-filter__toggle" aria-label="%s" aria-expanded="false" aria-haspopup="true">
374 + <span class="lp-addons-filter__toggle-content">
375 + <span class="lp-addons-filter__toggle-state lp-addons-filter__toggle-state--filter">
376 + <svg class="lp-addons-filter__toggle-icon lp-addons-filter__toggle-icon--filter" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
377 + <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
378 + </svg>
379 + <span class="lp-addons-filter__toggle-text">%s</span>
380 + </span>
381 + <span class="lp-addons-filter__toggle-state lp-addons-filter__toggle-state--close">
382 + <svg class="lp-addons-filter__toggle-icon lp-addons-filter__toggle-icon--close" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
383 + <line x1="18" y1="6" x2="6" y2="18"></line>
384 + <line x1="6" y1="6" x2="18" y2="18"></line>
385 + </svg>
386 + <span class="lp-addons-filter__toggle-text">%s</span>
387 + </span>
388 + </span>
389 + </button>
390 + <div class="lp-addons-filter__menu">',
391 + esc_attr__( 'Filter add-ons', 'learnpress' ),
392 + esc_attr__( 'Filter add-ons', 'learnpress' ),
393 + esc_html__( 'Filter', 'learnpress' ),
394 + esc_html__( 'Close', 'learnpress' )
395 + ),
396 + 'tabs' => Template::combine_components( $section_tabs ),
397 + 'filter_end' => '</div></div>',
398 + 'search' => '<div class="lp-search-addons lp-addons-search lp-be-search">',
399 + 'search_btn' => sprintf(
400 + '<button type="button" class="lp-addons-search__btn lp-be-search__btn" aria-label="%s" aria-expanded="false">
401 + <span class="lp-addons-search__icon lp-icon-search" aria-hidden="true"></span>
402 + </button>',
403 + esc_attr__( 'Search add-ons', 'learnpress' )
404 + ),
405 + 'search_field' => sprintf(
406 + '<div class="lp-addons-search__field lp-be-search__field">
407 + <span class="screen-reader-text">%s</span>
408 + <div class="lp-addons-search__input-wrap lp-be-search__input-wrap">
409 + <span class="lp-addons-search__field-icon lp-be-search__field-icon lp-icon-search" aria-hidden="true"></span>
410 + <input id="lp-search-addons__input"
411 + class="lp-addons-search__input lp-be-search__input" type="search" placeholder="%s" autocomplete="off"/>
412 + </div>
413 + <button type="button" class="lp-addons-search__btn-clear lp-be-search__btn-clear" disabled>%s</button>
414 + <button type="button" class="lp-addons-search__close lp-be-search__close" aria-label="%s">
415 + <span aria-hidden="true">&times;</span>
416 + </button>
417 + </div>',
418 + esc_html__( 'Search add-ons', 'learnpress' ),
419 + esc_attr__( 'Search add-ons by name…', 'learnpress' ),
420 + esc_html__( 'Clear', 'learnpress' ),
421 + esc_attr__( 'Close search', 'learnpress' )
422 + ),
423 + 'search_end' => '</div>',
424 + 'toolbar_primary_end' => '</div>',
425 + 'categories' => self::html_categories(
426 + array(
427 + 'addon_categories' => $addon_categories,
428 + )
429 + ),
430 + 'toolbar_end' => '</div>',
431 + 'wrapper_end' => '</div>',
432 + );
433 +
434 + return Template::combine_components( $section );
435 + }
436 +
437 + /**
438 + * Render the add-ons category filter buttons.
439 + *
440 + * @param array $data Renderer data.
441 + *
442 + * @return string
443 + */
444 + public static function html_categories( array $data = [] ): string {
445 + $addon_categories = $data['addon_categories'] ?? array();
446 + $section = array(
447 + 'wrapper' => sprintf(
448 + '<div class="lp-addons-categories lp-be-categories" role="group" aria-label="%s">',
449 + esc_attr__( 'Filter add-ons by category', 'learnpress' )
450 + ),
451 + 'all' => sprintf(
452 + '<button type="button" class="lp-addons-category lp-be-category active"
453 + data-category="all" aria-pressed="true">%s</button>',
454 + esc_html__( 'All', 'learnpress' )
455 + ),
456 + );
457 +
458 + foreach ( $addon_categories as $category_slug => $category_label ) {
459 + $section[ $category_slug ] = sprintf(
460 + '<button type="button" class="lp-addons-category lp-be-category"
461 + data-category="%s" aria-pressed="false">%s</button>',
462 + esc_attr( $category_slug ),
463 + esc_html( $category_label )
464 + );
465 + }
466 +
467 + $section['wrapper_end'] = '</div>';
468 +
469 + return Template::combine_components( $section );
470 + }
471 +
472 + /**
473 + * Render the list of add-on items.
474 + *
475 + * @param array $data Renderer data.
476 + *
477 + * @return string
478 + * @throws Exception
479 + */
480 + public static function html_addons_list( array $data = [] ): string {
481 + $addons = $data['addons'] ?? array();
482 + $html = '';
483 + foreach ( $addons as $slug => $addon ) {
484 + $addon->slug = $slug;
485 + $html .= self::html_addon_item( $addon, $data );
486 + }
487 +
488 + return sprintf( '<div id="lp-addons">%s</div>', $html );
489 + }
490 +
491 + /**
492 + * Render a single add-on card.
493 + *
494 + * @param object $addon Current addon object.
495 + * @param array $data Renderer data.
496 + *
497 + * @return string
498 + * @throws Exception
499 + */
500 + public static function html_addon_item( object $addon, array $data = [] ): string {
501 + $state = self::get_addon_classes_and_state( $addon, $data );
502 +
35 503 $section = apply_filters(
36 - 'learn-press/admin/manager-addons/section',
504 + 'learn-press/admin/addons/item',
37 505 array(
38 - 'label' => sprintf(
39 - '<h1>%s</h1>',
40 - __( 'LearnPress Add-ons', 'learnpress' )
506 + 'wrapper' => sprintf(
507 + '<div class="lp-addon-item %s" data-slug="%s" data-category="%s" data-addon="%s">',
508 + esc_attr( implode( ' ', $state['classes_status'] ) ),
509 + esc_attr( $addon->slug ),
510 + esc_attr( $state['addon_category'] ),
511 + esc_attr( htmlentities2( wp_json_encode( $addon ) ) )
41 512 ),
42 - 'note-theme' => sprintf(
43 - '<p style="color: rgba(255,0,0,0.76)"><strong><i>%s</i></strong></p>',
44 - __( '* If you use a Premium Theme that includes LearnPress add-ons, you can go to the <strong>Plugins</strong> tab on Dashboard of theme to download or update them.', 'learnpress' )
513 + 'content' => '<div class="lp-addon-item__content">',
514 + 'header' => self::html_addon_header( $addon ),
515 + 'license' => ! $state['is_free']
516 + ? self::html_addon_license( $addon, $state )
517 + : '',
518 + 'description' => sprintf(
519 + '<p class="lp-addon-item__description" title="%s">%s</p>',
520 + esc_attr( $addon->description ?? '' ),
521 + esc_html( $addon->description ?? '' )
45 522 ),
46 - 'note-addon' => sprintf(
47 - '<p>%s</p>',
48 - sprintf(
49 - __( 'If you have purchased a premium add-on separately, you can enter your purchase code (%s) to download or update the add-ons here.', 'learnpress' ),
50 - sprintf(
51 - '<a href="%s" target="_blank">%s</a>',
52 - 'https://thimpress.com/my-account/',
53 - __( 'get from your account', 'learnpress' )
54 - )
523 + 'meta' => self::html_addon_meta(
524 + $addon,
525 + array(
526 + 'version_current' => $state['version_current'],
55 527 )
56 528 ),
57 - 'list' => sprintf( '<div class="lp-addons-page">%s</div>', $html_loading ),
58 - )
529 + 'new_update' => $state['is_updated']
530 + ? sprintf(
531 + '<span class="new-update">%s: <strong>%s</strong></span>',
532 + esc_html__( 'Latest version', 'learnpress' ),
533 + esc_html( $state['version_latest'] )
534 + )
535 + : '',
536 + 'content_end' => '</div>',
537 + 'actions' => self::html_addon_actions( $addon ),
538 + 'purchase' => self::html_purchase_panel(
539 + $addon,
540 + array(
541 + 'purchase_code_masked' => $state['purchase_code_masked'],
542 + )
543 + ),
544 + 'wrapper_end' => '</div>',
545 + ),
546 + $addon,
547 + $data
59 548 );
60 549
61 - echo Template::combine_components( $section );
550 + return Template::combine_components( $section );
551 + }
552 +
553 + /**
554 + * Compute status classes and state flags for an addon.
555 + *
556 + * @param object $addon Current addon object.
557 + * @param array $data Addon state data.
558 + *
559 + * @return array
560 + * @throws Exception
561 + */
562 + public static function get_addon_classes_and_state( object $addon, array $data = [] ): array {
563 + $plugins_installed = $data['plugins_installed'] ?? array();
564 + $plugins_activated = $data['plugins_activated'] ?? array();
565 + $active_tab = $data['active_tab'] ?? 'all';
566 + $keys_purchase = $data['keys_purchase'] ?? array();
567 + $is_free = $addon->is_free ?? 0;
568 + $addon_base = $addon->basename ?? '';
569 + $version_latest = $addon->version ?? 0;
570 + $version_current = $addon->version ?? 0;
571 + $classes_status = array();
572 + $addon_purchased = $addon->purchase_info ?? false;
573 + $addon_category = implode( ' ', array_map( 'sanitize_title', $addon->category ?? array() ) );
574 + $date_expired_str = '';
575 + $number_days_remaining = null;
576 + $date_expired = null;
577 +
578 + if ( ! $is_free ) {
579 + $classes_status[] = 'purchase';
580 + } else {
581 + $classes_status[] = 'free';
582 + }
583 +
584 + if ( isset( $plugins_installed[ $addon_base ] ) ) {
585 + $is_installed = true;
586 + $classes_status[] = 'installed';
587 + $version_current = $plugins_installed[ $addon_base ]['Version'];
588 + } else {
589 + $is_installed = false;
590 + $classes_status[] = 'not_installed';
591 + }
592 +
593 + if ( in_array( $addon_base, $plugins_activated, true ) ) {
594 + $classes_status[] = 'activated';
595 + }
596 +
597 + $is_updated = false;
598 + if ( $is_installed && version_compare( $version_current, $version_latest, '<' ) ) {
599 + $classes_status[] = 'update';
600 + $is_updated = true;
601 + }
602 +
603 + if ( $addon_purchased ) {
604 + $classes_status[] = 'license';
605 + $date_expired_str = ! empty( $data['ex-date-test'] ) ? $data['ex-date-test'] : ( $addon_purchased->date_expire ?? '' );
606 + if ( ! empty( $date_expired_str ) ) {
607 + $date_expired = new DateTime( $date_expired_str );
608 + $date_now = new DateTime( gmdate( 'Y-m-d' ) );
609 + $date_diff = date_diff( $date_now, $date_expired );
610 + $number_days_remaining = $date_diff->invert ? 0 : $date_diff->days;
611 + }
612 + }
613 +
614 + $show_license_panel = ! $is_free && $is_installed;
615 + $license_status = 'not-activated';
616 + if ( $addon_purchased ) {
617 + $license_status = 'active';
618 + if ( ! empty( $date_expired_str )
619 + && isset( $number_days_remaining )
620 + && 0 === $number_days_remaining ) {
621 + $license_status = 'expired';
622 + $classes_status[] = 'expired';
623 + }
624 + }
625 +
626 + $purchase_code = $keys_purchase[ $addon->slug ] ?? '';
627 + $purchase_code_masked = AddonService::mask_purchase_code( $purchase_code );
628 +
629 + if ( ! in_array( $active_tab, $classes_status, true ) && 'all' !== $active_tab ) {
630 + $classes_status[] = 'hide';
631 + }
632 +
633 + return array(
634 + 'classes_status' => $classes_status,
635 + 'is_free' => $is_free,
636 + 'is_installed' => $is_installed,
637 + 'is_updated' => $is_updated,
638 + 'version_current' => $version_current,
639 + 'version_latest' => $version_latest,
640 + 'license_status' => $license_status,
641 + 'number_days_remaining' => $number_days_remaining,
642 + 'date_expired' => $date_expired,
643 + 'purchase_code' => $purchase_code,
644 + 'purchase_code_masked' => $purchase_code_masked,
645 + 'show_license_panel' => $show_license_panel,
646 + 'addon_category' => $addon_category,
647 + );
648 + }
649 +
650 + /**
651 + * Render the header section of an addon item.
652 + *
653 + * @param object $addon Current addon object.
654 + * @param array $data Renderer data.
655 + *
656 + * @return string
657 + */
658 + public static function html_addon_header( object $addon, array $data = [] ): string {
659 + $section = array(
660 + 'wrapper' => '<div class="lp-addon-item__header">',
661 + 'image' => sprintf(
662 + '<img class="lp-addon-item__image" src="%s" alt=""/>',
663 + esc_url( $addon->image ?? '' )
664 + ),
665 + 'heading' => '<div class="lp-addon-item__heading">',
666 + 'title' => '<h3 class="lp-addon-item__title">',
667 + 'link' => sprintf(
668 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
669 + esc_url( $addon->link ?? '' ),
670 + esc_html( $addon->name ?? '' )
671 + ),
672 + 'title_end' => '</h3>',
673 + 'badge' => ! empty( $addon->badge ) ? sprintf(
674 + '<span class="lp-addon-item__badge">%s</span>',
675 + esc_html( $addon->badge )
676 + ) : '',
677 + 'heading_end' => '</div>',
678 + 'wrapper_end' => '</div>',
679 + );
680 +
681 + return Template::combine_components( $section );
682 + }
683 +
684 + /**
685 + * Render the license status block for a paid addon.
686 + *
687 + * @param object $addon Current addon object.
688 + * @param array $data Renderer data.
689 + *
690 + * @return string
691 + */
692 + public static function html_addon_license( object $addon, array $data = [] ): string {
693 + $license_status = $data['license_status'] ?? '';
694 + $number_days_remaining = $data['number_days_remaining'] ?? null;
695 + /** @var DateTime $date_expired */
696 + $date_expired = $data['date_expired'] ?? null;
697 + $purchase_code = $data['purchase_code'] ?? '';
698 + $purchase_code_masked = $data['purchase_code_masked'] ?? '';
699 + $show_license_panel = $data['show_license_panel'] ?? false;
700 + $license_status_label = 'active' === $license_status
701 + ? __( 'Active', 'learnpress' )
702 + : ( 'expired' === $license_status
703 + ? __( 'Expired', 'learnpress' )
704 + : ( 'deactivated' === $license_status
705 + ? __( 'Deactivated', 'learnpress' )
706 + : __( 'Not Activated', 'learnpress' ) ) );
707 +
708 + $expiry_hidden = empty( $date_expired )
709 + || ! in_array( $license_status, array( 'active', 'expired' ), true ) ? ' hidden' : '';
710 +
711 + $expiry_text = '';
712 + if ( ! empty( $date_expired ) ) {
713 + $lpDate = new LPDateTime( $date_expired->format( LPDateTime::FORMAT_MYSQL ) );
714 +
715 + $expiry_format = 'expired' === $license_status
716 + ? __( '(on %s)', 'learnpress' )
717 + : __( '(Updates until %s)', 'learnpress' );
718 + $expiry_text = esc_html( sprintf( $expiry_format, $lpDate->format( LPDateTime::FORMAT_I18N_DATE ) ) );
719 + }
720 +
721 + $link_extend = add_query_arg(
722 + 'purchase_code',
723 + $purchase_code,
724 + AddonService::instance()->link_extend_site
725 + );
726 + $html_extend_link = sprintf(
727 + '<a class="need-extend__link" href="%s" target="_blank" rel="noopener">%s</a>',
728 + esc_url( $link_extend ),
729 + esc_html__( 'Extend now', 'learnpress' )
730 + );
731 +
732 + $html_near_expire = '';
733 + if ( ! empty( $addon->purchase_info ) ) {
734 + if ( isset( $number_days_remaining ) && $number_days_remaining > 0 && $number_days_remaining < 61 ) {
735 + $message_near_expire = sprintf(
736 + __( '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' ),
737 + sprintf( '<strong class="need-extend__days">%d</strong>', $number_days_remaining )
738 + );
739 +
740 + $html_near_expire = sprintf(
741 + '<span class="need-extend">%s %s</span>',
742 + $message_near_expire,
743 + $html_extend_link
744 + );
745 + }
746 + }
747 +
748 + $text_btn_to_purchase_code = esc_html__( 'Activate Now', 'learnpress' );
749 + if ( ! empty( $purchase_code ) ) {
750 + $text_btn_to_purchase_code = esc_html__( 'Change License', 'learnpress' );
751 + }
752 +
753 + $section = array(
754 + 'wrapper' => sprintf(
755 + '<div class="lp-addon-license lp-addon-license--%s" data-purchase-code-masked="%s"%s>',
756 + esc_attr( $license_status ),
757 + esc_attr( $purchase_code_masked ),
758 + $show_license_panel ? '' : ' hidden'
759 + ),
760 + 'summary' => '<div class="lp-addon-license__summary">',
761 + 'label' => sprintf(
762 + '<span>%s</span>',
763 + esc_html__( 'License:', 'learnpress' )
764 + ),
765 + 'status' => sprintf(
766 + '<strong class="lp-addon-license__status">%s</strong>',
767 + esc_html( $license_status_label )
768 + ),
769 + 'expiry' => sprintf(
770 + '<span class="lp-addon-license__expiry"%s>%s</span>',
771 + $expiry_hidden,
772 + $expiry_text
773 + ),
774 + 'extend_link' => 'expired' === $license_status ? $html_extend_link : '',
775 + 'summary_end' => '</div>',
776 + 'near_expire' => $html_near_expire,
777 + 'cancel_btn' => '<button
778 + class="lp-button btn-addon-action lp-be-btn lp-be-btn--outline lp-addon-purchase__cancel-clear lp-hidden"
779 + data-action="cancel-clear-license" type="button">',
780 + 'cancel_text' => sprintf(
781 + '<span class="text">%s</span>',
782 + esc_html__( 'Cancel', 'learnpress' )
783 + ),
784 + 'cancel_end' => '</button>',
785 + 'manage' => sprintf(
786 + '<button class="btn-addon-action lp-addon-license__manage"
787 + data-action="update-purchase-code" type="button">%s</button>',
788 + $text_btn_to_purchase_code
789 + ),
790 + 'wrapper_end' => '</div>',
791 + );
792 +
793 + return Template::combine_components( $section );
794 + }
795 +
796 + /**
797 + * Render addon meta: version, docs, view details.
798 + *
799 + * @param object $addon Current addon object.
800 + * @param array $data Renderer data.
801 + *
802 + * @return string
803 + */
804 + public static function html_addon_meta( object $addon, array $data = [] ): string {
805 + $version_current = $data['version_current'] ?? '';
806 + $section = array(
807 + 'wrapper' => '<div class="lp-addon-item__meta">',
808 + 'version' => $version_current
809 + ? sprintf(
810 + '<span>%s <span class="addon-version-current">%s</span></span>',
811 + esc_html__( 'Version', 'learnpress' ),
812 + esc_html( $version_current )
813 + )
814 + : '',
815 + 'docs' => ! empty( $addon->link_doc )
816 + ? sprintf(
817 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
818 + esc_url( $addon->link_doc ),
819 + esc_html__( 'Docs', 'learnpress' )
820 + )
821 + : '',
822 + 'details' => sprintf(
823 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
824 + esc_url( $addon->link ?? '' ),
825 + esc_html__( 'View Details', 'learnpress' )
826 + ),
827 + 'settings' => ! empty( $addon->setting )
828 + ? sprintf(
829 + '<a data-action="setting" href="%s" target="_blank" rel="noopener">%s</a>',
830 + esc_url( site_url( $addon->setting ) ),
831 + esc_html__( 'Settings', 'learnpress' )
832 + )
833 + : '',
834 + 'wrapper_end' => '</div>',
835 + );
836 +
837 + return Template::combine_components( $section );
838 + }
839 +
840 + /**
841 + * Render addon price block.
842 + *
843 + * @param object $addon Current addon object.
844 + * @param array $data Renderer data.
845 + *
846 + * @return string
847 + */
848 + public static function html_addon_price( object $addon, array $data = [] ): string {
849 + $is_free = $addon->is_free ?? 0;
850 +
851 + if ( $is_free ) {
852 + return sprintf(
853 + '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--free">%s</strong>',
854 + esc_html__( 'Free', 'learnpress' )
855 + );
856 + }
857 +
858 + if ( ! empty( $addon->sale_price ) ) {
859 + $section = array(
860 + 'sale' => sprintf(
861 + '<strong class="lp-addon-item__price-sale">$%s</strong>',
862 + esc_html( number_format_i18n( $addon->sale_price ) )
863 + ),
864 + 'regular' => sprintf(
865 + '<del class="lp-addon-item__price-regular">$%s</del>',
866 + esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
867 + ),
868 + );
869 +
870 + return Template::combine_components( $section );
871 + }
872 +
873 + return sprintf(
874 + '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--current">$%s</strong>',
875 + esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
876 + );
877 + }
878 +
879 + /**
880 + * Render addon action buttons.
881 + *
882 + * @param object $addon Current addon object.
883 + * @param array $data Renderer data.
884 + *
885 + * @return string
886 + */
887 + public static function html_addon_actions( object $addon, array $data = [] ): string {
888 + $section_right = apply_filters(
889 + 'learn-press/admin/addons/actions/section_right',
890 + array(
891 + 'install' => $addon->is_free && ! $addon->is_org
892 + ? sprintf(
893 + '<a class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="install" href="%s"
894 + target="_blank" rel="noopener">%s</a>',
895 + esc_url( $addon->link ?? '' ),
896 + esc_html__( 'Install', 'learnpress' )
897 + )
898 + : sprintf(
899 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="install">
900 + <span class="text">%s</span>
901 + </button>',
902 + esc_html__( 'Install', 'learnpress' )
903 + ),
904 + 'purchase' => sprintf(
905 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="purchase">%s</button>',
906 + esc_html__( 'Install', 'learnpress' )
907 + ),
908 + 'update' => sprintf(
909 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="update">
910 + <span class="text">%s</span>
911 + </button>',
912 + esc_html__( 'Update', 'learnpress' )
913 + ),
914 + 'deactivate' => sprintf(
915 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm" data-action="deactivate">
916 + <span class="text">%s</span>
917 + </button>',
918 + esc_html__( 'Deactivate', 'learnpress' )
919 + ),
920 + 'activate' => sprintf(
921 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm" data-action="activate">
922 + <span class="text">%s</span>
923 + </button>',
924 + esc_html__( 'Activate', 'learnpress' )
925 + ),
926 + ),
927 + $addon,
928 + $data
929 + );
930 +
931 + $section = array(
932 + 'wrapper' => '<div class="lp-addon-item__actions">',
933 + 'actions_left' => '<div class="lp-addon-item__actions__left">',
934 + 'price' => sprintf(
935 + '<div class="lp-addon-item__price">%s</div>',
936 + self::html_addon_price( $addon )
937 + ),
938 + 'actions_left_end' => '</div>',
939 + 'actions_right' => '<div class="lp-addon-item__actions__right">',
940 + 'actions' => Template::combine_components( $section_right ),
941 + 'actions_right_end' => '</div>',
942 + 'wrapper_end' => '</div>',
943 + );
944 +
945 + return Template::combine_components( $section );
946 + }
947 +
948 + /**
949 + * Render the purchase-code overlay panel.
950 + *
951 + * @param object $addon Current addon object.
952 + * @param array $data Renderer data.
953 + *
954 + * @return string
955 + */
956 + public static function html_purchase_panel( object $addon, array $data = [] ): string {
957 + $purchase_code_masked = $data['purchase_code_masked'] ?? '';
958 + $has_purchase_code = ! empty( $purchase_code_masked );
959 +
960 + $panel = array(
961 + 'wrapper' => '<div class="purchase-install">',
962 + 'header' => '<div class="lp-addon-purchase__header">',
963 + 'title' => sprintf(
964 + '<strong class="lp-addon-purchase__title">%s</strong>',
965 + esc_html__( 'Purchase Code', 'learnpress' )
966 + ),
967 + 'close' => sprintf(
968 + '<button class="btn-addon-action
969 + lp-addon-purchase__close" data-action="cancel" type="button" aria-label="%s">',
970 + esc_attr__( 'Close', 'learnpress' )
971 + ),
972 + 'close_icon' => '<span class="lp-addon-purchase__close-symbol" aria-hidden="true">&times;</span>',
973 + 'close_end' => '</button>',
974 + 'header_end' => '</div>',
975 + 'field' => '<label class="lp-addon-purchase__field">',
976 + 'field_text' => sprintf(
977 + '<span class="screen-reader-text">%s</span>',
978 + esc_html__( 'Purchase Code', 'learnpress' )
979 + ),
980 + 'field_input' => sprintf(
981 + '<input type="text" class="enter-purchase-code" placeholder="%s" value="%s"%s>',
982 + esc_attr__( 'Enter Purchase Code', 'learnpress' ),
983 + esc_attr( $purchase_code_masked ),
984 + $has_purchase_code ? ' disabled' : ''
985 + ),
986 + 'field_end' => '</label>',
987 + 'submit' => sprintf(
988 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--primary lp-addon-purchase__submit%s"
989 + data-action="install" type="button">',
990 + $has_purchase_code ? ' lp-hidden' : ''
991 + ),
992 + 'submit_text' => sprintf( '<span class="text">%s</span>', esc_html__( 'Verify & Activate', 'learnpress' ) ),
993 + 'submit_end' => '</button>',
994 + 'cancel_clear' => '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--outline lp-addon-purchase__cancel-clear lp-hidden"
995 + data-action="cancel-clear-license" type="button">',
996 + 'cancel_clear_text' => sprintf(
997 + '<span class="text">%s</span>',
998 + esc_html__( 'Cancel', 'learnpress' )
999 + ),
1000 + 'cancel_clear_end' => '</button>',
1001 + 'clear' => sprintf(
1002 + '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--primary lp-addon-purchase__clear%s"
1003 + data-action="clear-license" type="button">',
1004 + $has_purchase_code ? '' : ' lp-hidden'
1005 + ),
1006 + 'clear_text' => sprintf( '<span class="text">%s</span>', esc_html__( 'Clear', 'learnpress' ) ),
1007 + 'clear_end' => '</button>',
1008 + 'divider' => sprintf(
1009 + '<div class="lp-addon-purchase__divider"><span>%s</span></div>',
1010 + $has_purchase_code
1011 + ? esc_html__( 'Or', 'learnpress' )
1012 + : esc_html__( 'Don\'t have a code?', 'learnpress' )
1013 + ),
1014 + 'buy' => sprintf(
1015 + '<a class="btn-addon-action lp-addon-purchase__buy lp-be-btn lp-be-btn--outline" href="%s" target="_blank" rel="noopener">%s</a>',
1016 + esc_url( $addon->link ?? '' ),
1017 + esc_html__( 'Buy Now', 'learnpress' )
1018 + ),
1019 + 'wrapper_end' => '</div>',
1020 + );
1021 +
1022 + $section = array(
1023 + 'wrapper' => '<div class="lp-addon-item__purchase">',
1024 + 'purchase' => '<div class="lp-addon-item__purchase__wrapper">',
1025 + 'panel' => Template::combine_components( $panel ),
1026 + 'purchase_code' => '<input type="hidden" name="purchase-code" value="">',
1027 + 'purchase_end' => '</div>',
1028 + 'wrapper_end' => '</div>',
1029 + );
1030 +
1031 + return Template::combine_components( $section );
62 1032 }
63 1033 }