PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.1.0
Forumax – AI Powered Advanced Community Forum Plugin v2.1.0
2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 2.0.0 2.1.0 All 29 releases
bbp-core / includes / admin / notices / class-remote-notice-client.php

class-remote-notice-client.php in Forumax – AI Powered Advanced Community Forum Plugin 2.1.0, at includes/admin/notices/class-remote-notice-client.php

527 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Remote Notice Client SDK
4 *
5 * A reusable SDK for integrating remote HTML notices from the HTML Notice Widget API.
6 * Bundle this file with your plugin to enable remote admin notices.
7 *
8 * @package Remote_Notice_Client
9 * @version 1.1.1
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 if ( ! class_exists( 'Remote_Notice_Client' ) ) {
17
18 /**
19 * Remote Notice Client Class
20 */
21 class Remote_Notice_Client {
22
23 /**
24 * Registered instances
25 *
26 * @var array
27 */
28 private static $instances = array();
29
30 /**
31 * Product identifier
32 *
33 * @var string
34 */
35 private $product;
36
37 /**
38 * API URL
39 *
40 * @var string
41 */
42 private $api_url;
43
44 /**
45 * Cron schedule
46 *
47 * @var string
48 */
49 private $schedule;
50
51 /**
52 * Required capability
53 *
54 * @var string
55 */
56 private $capability;
57
58 /**
59 * Dismiss duration in seconds (default: 1 week)
60 *
61 * @var int
62 */
63 private $dismiss_duration;
64
65 /**
66 * Allow HTML tags for content
67 *
68 * @var array
69 */
70 private $allowed_html;
71
72 /**
73 * Initialize the client
74 *
75 * @param string $product Product identifier.
76 * @param array $config Configuration options.
77 * @return Remote_Notice_Client|false Instance or false if disabled.
78 */
79 public static function init( $product, $config = array() ) {
80 $product = sanitize_key( $product );
81
82 // Check if disabled (e.g., when Pro is active)
83 if ( self::is_disabled( $product ) ) {
84 return false;
85 }
86
87 if ( isset( self::$instances[ $product ] ) ) {
88 return self::$instances[ $product ];
89 }
90
91 $instance = new self( $product, $config );
92 self::$instances[ $product ] = $instance;
93 $instance->setup_hooks();
94
95 return $instance;
96 }
97
98 /**
99 * Constructor
100 *
101 * @param string $product Product identifier.
102 * @param array $config Configuration options.
103 */
104 private function __construct( $product, $config ) {
105 $this->product = $product;
106 $this->api_url = isset( $config['api_url'] ) ? esc_url_raw( $config['api_url'] ) : '';
107 $this->schedule = isset( $config['schedule'] ) ? $config['schedule'] : 'daily';
108 $this->capability = isset( $config['capability'] ) ? $config['capability'] : 'manage_options';
109 $this->dismiss_duration = isset( $config['dismiss_duration'] ) ? absint( $config['dismiss_duration'] ) : WEEK_IN_SECONDS;
110
111 // Define allowed HTML tags for notices
112 $this->allowed_html = array(
113 'div' => array( 'class' => array(), 'style' => array(), 'id' => array() ),
114 'p' => array( 'class' => array(), 'style' => array() ),
115 'span' => array( 'class' => array(), 'style' => array() ),
116 'strong' => array( 'class' => array() ),
117 'em' => array( 'class' => array() ),
118 'b' => array(),
119 'i' => array(),
120 'a' => array( 'href' => array(), 'title' => array(), 'target' => array(), 'class' => array(), 'rel' => array() ),
121 'br' => array(),
122 'h1' => array( 'class' => array() ),
123 'h2' => array( 'class' => array() ),
124 'h3' => array( 'class' => array() ),
125 'h4' => array( 'class' => array() ),
126 'ul' => array( 'class' => array() ),
127 'ol' => array( 'class' => array() ),
128 'li' => array( 'class' => array() ),
129 'img' => array( 'src' => array(), 'alt' => array(), 'class' => array(), 'width' => array(), 'height' => array() ),
130 );
131 }
132
133 /**
134 * Setup WordPress hooks
135 */
136 private function setup_hooks() {
137 // Cron.
138 add_action( 'admin_init', array( $this, 'init_cron' ) );
139 add_action( 'rnc_fetch_content_' . $this->product, array( $this, 'fetch_content' ) );
140
141 // Display notices.
142 add_action( 'admin_notices', array( $this, 'display_notices' ) );
143
144 // AJAX handlers.
145 add_action( 'wp_ajax_rnc_dismiss_content_' . $this->product, array( $this, 'ajax_dismiss_content' ) );
146 }
147
148 /**
149 * Initialize cron schedule
150 */
151 public function init_cron() {
152 $hook = 'rnc_fetch_content_' . $this->product;
153
154 if ( ! wp_next_scheduled( $hook ) ) {
155 wp_schedule_event( time(), $this->schedule, $hook );
156 }
157 }
158
159 /**
160 * Fetch content from remote API
161 */
162 public function fetch_content() {
163 if ( empty( $this->api_url ) ) {
164 return;
165 }
166
167 $response = wp_remote_get(
168 $this->api_url,
169 array(
170 'timeout' => 15,
171 'sslverify' => true,
172 )
173 );
174
175 if ( is_wp_error( $response ) ) {
176 $this->log( 'API Error: ' . $response->get_error_message(), 'error' );
177 return;
178 }
179
180 $http_code = wp_remote_retrieve_response_code( $response );
181
182 if ( 200 !== $http_code ) {
183 $this->log( 'API returned HTTP ' . $http_code, 'error' );
184 return;
185 }
186
187 $body = wp_remote_retrieve_body( $response );
188 $data = json_decode( $body, true );
189
190 if ( ! is_array( $data ) || ! isset( $data['success'] ) || ! $data['success'] ) {
191 $this->log( 'Invalid API response', 'error' );
192 return;
193 }
194
195 $contents = isset( $data['contents'] ) ? $data['contents'] : array();
196
197 if ( empty( $contents ) ) {
198 $this->clear_contents();
199 $this->log( 'No contents - cleared stored notices' );
200 return;
201 }
202
203 $this->store_contents( $contents );
204 $this->log( 'Fetched ' . count( $contents ) . ' contents' );
205 }
206
207 /**
208 * Store contents in database
209 *
210 * @param array $contents Contents array.
211 */
212 private function store_contents( $contents ) {
213 $key = $this->get_option_key( 'contents' );
214 $old_contents = get_option( $key, array() );
215 $old_ids = wp_list_pluck( $old_contents, 'id' );
216 $new_ids = wp_list_pluck( $contents, 'id' );
217
218 // Clean up stale dismiss keys.
219 $removed_ids = array_diff( $old_ids, $new_ids );
220 foreach ( $removed_ids as $removed_id ) {
221 delete_option( $this->get_option_key( 'dismissed_' . $removed_id ) );
222 }
223
224 update_option( $key, $contents, false );
225 update_option( $this->get_option_key( 'fetched_time' ), current_time( 'mysql' ), false );
226 }
227
228 /**
229 * Clear all stored contents
230 */
231 private function clear_contents() {
232 $contents = get_option( $this->get_option_key( 'contents' ), array() );
233
234 // Clean up all dismiss keys.
235 if ( is_array( $contents ) ) {
236 foreach ( $contents as $content ) {
237 if ( isset( $content['id'] ) ) {
238 delete_option( $this->get_option_key( 'dismissed_' . $content['id'] ) );
239 }
240 }
241 }
242
243 delete_option( $this->get_option_key( 'contents' ) );
244 delete_option( $this->get_option_key( 'fetched_time' ) );
245 delete_option( $this->get_option_key( 'temp_dismissed' ) );
246 }
247
248 /**
249 * Get non-dismissed contents
250 *
251 * @return array
252 */
253 private function get_non_dismissed_contents() {
254 $contents = get_option( $this->get_option_key( 'contents' ), array() );
255 $result = array();
256
257 if ( ! is_array( $contents ) ) {
258 return $result;
259 }
260
261 foreach ( $contents as $content ) {
262 if ( ! isset( $content['id'] ) ) {
263 continue;
264 }
265
266 $dismissed = get_option( $this->get_option_key( 'dismissed_' . $content['id'] ), false );
267 if ( ! $dismissed ) {
268 $result[] = $content;
269 }
270 }
271
272 return $result;
273 }
274
275 /**
276 * Check if temporarily dismissed
277 *
278 * @return bool
279 */
280 private function is_temporarily_dismissed() {
281 $dismiss_time = get_option( $this->get_option_key( 'temp_dismissed' ), 0 );
282
283 if ( empty( $dismiss_time ) ) {
284 return false;
285 }
286
287 return ( current_time( 'timestamp' ) - intval( $dismiss_time ) ) < $this->dismiss_duration;
288 }
289
290 /**
291 * Display admin notices
292 */
293 public function display_notices() {
294 if ( ! current_user_can( $this->capability ) ) {
295 return;
296 }
297
298 if ( $this->is_temporarily_dismissed() ) {
299 return;
300 }
301
302 $contents = $this->get_non_dismissed_contents();
303
304 if ( empty( $contents ) ) {
305 return;
306 }
307
308 foreach ( $contents as $content ) {
309 if ( ! isset( $content['id'] ) || ! isset( $content['content'] ) ) {
310 continue;
311 }
312
313 $content_id = sanitize_key( $content['id'] );
314 $html_content = wp_kses( $content['content'], $this->allowed_html );
315 $nonce = wp_create_nonce( 'rnc_dismiss_' . $this->product . '_' . $content_id );
316 $ajax_action = 'rnc_dismiss_content_' . $this->product;
317
318 ?>
319 <div id="rnc-notice-<?php echo esc_attr( $this->product . '-' . $content_id ); ?>"
320 class="notice notice-info rnc-notice-wrapper"
321 data-product="<?php echo esc_attr( $this->product ); ?>"
322 data-content-id="<?php echo esc_attr( $content_id ); ?>"
323 data-nonce="<?php echo esc_attr( $nonce ); ?>"
324 data-action="<?php echo esc_attr( $ajax_action ); ?>">
325 <div class="rnc-notice-content">
326 <?php echo $html_content; // Already sanitized with wp_kses above ?>
327 </div>
328 <button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>
329 </div>
330
331 <style>
332 #rnc-notice-<?php echo esc_attr( $this->product . '-' . $content_id ); ?> {
333 margin: 20px 0 !important;
334 padding: 0 !important;
335 background: transparent !important;
336 border: 0 !important;
337 position: relative;
338 box-shadow: none;
339 }
340 #rnc-notice-<?php echo esc_attr( $this->product . '-' . $content_id ); ?> .notice-dismiss {
341 position: absolute !important;
342 top: 0 !important;
343 right: 1px !important;
344 border: none !important;
345 margin: 0 !important;
346 padding: 9px !important;
347 background: none !important;
348 color: #787c82 !important;
349 cursor: pointer !important;
350 }
351 #rnc-notice-<?php echo esc_attr( $this->product . '-' . $content_id ); ?> .notice-dismiss:hover {
352 color: #c92c2c !important;
353 }
354 </style>
355
356 <script>
357 (function() {
358 var notice = document.getElementById('rnc-notice-<?php echo esc_js( $this->product . '-' . $content_id ); ?>');
359 if (!notice) return;
360
361 var closeBtn = notice.querySelector('.notice-dismiss');
362 if (closeBtn) {
363 closeBtn.addEventListener('click', function(e) {
364 e.preventDefault();
365
366 var action = notice.getAttribute('data-action');
367 var contentId = notice.getAttribute('data-content-id');
368 var nonce = notice.getAttribute('data-nonce');
369
370 var xhr = new XMLHttpRequest();
371 xhr.open('POST', '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>', true);
372 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
373 xhr.onload = function() {
374 if (xhr.status === 200) {
375 notice.style.display = 'none';
376 }
377 };
378 var data = 'action=' + encodeURIComponent(action) +
379 '&content_id=' + encodeURIComponent(contentId) +
380 '&nonce=' + encodeURIComponent(nonce);
381 xhr.send(data);
382 });
383 }
384 })();
385 </script>
386 <?php
387 }
388 }
389
390 /**
391 * AJAX handler for dismissing content
392 */
393 public function ajax_dismiss_content() {
394 $content_id = isset( $_POST['content_id'] ) ? sanitize_text_field( wp_unslash( $_POST['content_id'] ) ) : '';
395 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
396
397 if ( ! wp_verify_nonce( $nonce, 'rnc_dismiss_' . $this->product . '_' . $content_id ) ) {
398 wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
399 }
400
401 if ( ! current_user_can( $this->capability ) ) {
402 wp_send_json_error( array( 'message' => 'Unauthorized' ) );
403 }
404
405 if ( empty( $content_id ) ) {
406 wp_send_json_error( array( 'message' => 'Invalid content ID' ) );
407 }
408
409 update_option( $this->get_option_key( 'dismissed_' . $content_id ), true, false );
410
411 wp_send_json_success( array( 'message' => 'Content dismissed' ) );
412 }
413
414 /**
415 * Get option key with product prefix
416 *
417 * @param string $key Key name.
418 * @return string
419 */
420 private function get_option_key( $key ) {
421 return 'rnc_' . $this->product . '_' . $key;
422 }
423
424 /**
425 * Log activity (only in debug mode)
426 *
427 * @param string $message Message.
428 * @param string $level Log level.
429 */
430 private function log( $message, $level = 'info' ) {
431 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
432 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
433 error_log( '[Remote Notice Client - ' . strtoupper( $level ) . '] [' . $this->product . '] ' . $message );
434 }
435 }
436
437 /**
438 * Manually trigger fetch (useful for testing)
439 *
440 * @param string $product Product identifier.
441 * @return bool True if triggered, false if instance doesn't exist.
442 */
443 public static function trigger_fetch( $product ) {
444 $product = sanitize_key( $product );
445
446 if ( ! isset( self::$instances[ $product ] ) ) {
447 return false;
448 }
449
450 self::$instances[ $product ]->fetch_content();
451 return true;
452 }
453
454 /**
455 * Clear all data for a product
456 *
457 * @param string $product Product identifier.
458 * @return bool True if cleared, false if instance doesn't exist.
459 */
460 public static function clear_all( $product ) {
461 $product = sanitize_key( $product );
462
463 if ( ! isset( self::$instances[ $product ] ) ) {
464 // Still try to clear data even if instance doesn't exist
465 $temp_instance = new self( $product, array() );
466 $temp_instance->clear_contents();
467 return true;
468 }
469
470 self::$instances[ $product ]->clear_contents();
471 return true;
472 }
473
474 /**
475 * Disable the notice client for a product
476 *
477 * Use this when Pro version is active to stop showing notices
478 * and unschedule the cron job.
479 *
480 * @param string $product Product identifier.
481 */
482 public static function disable( $product ) {
483 $product = sanitize_key( $product );
484
485 // Mark as disabled
486 update_option( 'rnc_' . $product . '_disabled', true, false );
487
488 // Unschedule cron
489 $hook = 'rnc_fetch_content_' . $product;
490 $timestamp = wp_next_scheduled( $hook );
491 if ( $timestamp ) {
492 wp_unschedule_event( $timestamp, $hook );
493 }
494
495 // Clear all stored data
496 if ( isset( self::$instances[ $product ] ) ) {
497 self::$instances[ $product ]->clear_contents();
498 unset( self::$instances[ $product ] );
499 } else {
500 // Clear data even if instance doesn't exist
501 $temp_instance = new self( $product, array() );
502 $temp_instance->clear_contents();
503 }
504 }
505
506 /**
507 * Re-enable the notice client for a product
508 *
509 * @param string $product Product identifier.
510 */
511 public static function enable( $product ) {
512 $product = sanitize_key( $product );
513 delete_option( 'rnc_' . $product . '_disabled' );
514 }
515
516 /**
517 * Check if the notice client is disabled for a product
518 *
519 * @param string $product Product identifier.
520 * @return bool
521 */
522 public static function is_disabled( $product ) {
523 $product = sanitize_key( $product );
524 return (bool) get_option( 'rnc_' . $product . '_disabled', false );
525 }
526 }
527 }