PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
ultimate-store-kit / includes / Admin / Biggopties.php

Biggopties.php in Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder 3.1.4, at includes/Admin/Biggopties.php

655 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace UltimateStoreKit\Admin;
4
5 if (! defined('ABSPATH')) {
6 exit; // Exit if accessed directly
7 }
8
9 use UltimateStoreKit\Base\Singleton;
10
11 /**
12 * Biggopties class
13 */
14 class Biggopties {
15 use Singleton;
16
17 /**
18 * Max seconds to wait for the remote API.
19 */
20 const REQUEST_TIMEOUT = 5;
21
22 /**
23 * How long to skip remote requests after a failure.
24 */
25 const FAILURE_BACKOFF = HOUR_IN_SECONDS;
26
27 /**
28 * How long a successful response stays cached.
29 */
30 const CACHE_LIFETIME = HOUR_IN_SECONDS;
31
32 private static $biggopties = [];
33
34 /**
35 * Every notice id this plugin renders starts with this. The dismiss handler
36 * writes the id straight into a transient / user-meta key, so it will only
37 * accept keys that carry the prefix.
38 */
39 const DISMISS_KEY_PREFIX = 'bdt-admin-biggopti-';
40
41 public function __construct() {
42
43 // add_action('admin_notices', [$this, 'show_biggopties']);
44 add_action('wp_ajax_ultimate-store-kit-biggopties', [$this, 'dismiss']);
45
46 // AJAX endpoint to fetch API biggopties on demand (after page load)
47 add_action('wp_ajax_usk_fetch_api_biggopties', [$this, 'ajax_fetch_api_biggopties']);
48 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
49 }
50
51 /**
52 * Enqueue admin scripts
53 */
54 public function enqueue_admin_scripts() {
55 wp_enqueue_style('ultimate-store-kit-product-feed', BDTUSK_ASSETS_URL . 'admin/others/css/product-feed.css', [], BDTUSK_VER);
56 wp_enqueue_script('usk-biggopti', BDTUSK_ASSETS_URL . 'admin/others/js/biggopti.js', ['jquery'], BDTUSK_VER, true);
57
58 $dismissals = get_option('bdt_biggopti_dismissals', []);
59 $dismissed_display_ids = [];
60 $prefix = self::DISMISS_KEY_PREFIX . 'api-biggopti-';
61 foreach (array_keys($dismissals) as $key) {
62 if (strpos($key, $prefix) === 0) {
63 $dismissed_display_ids[] = substr($key, strlen($prefix));
64 } else {
65 $dismissed_display_ids[] = $key;
66 }
67 }
68
69 $current_sector = '';
70 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of which admin screen is showing.
71 if (isset($_GET['page']) && $_GET['page'] === 'ultimate_store_kit_options') {
72 $current_sector = 'plugin_dashboard';
73 }
74
75 $script_config = [
76 'ajaxurl' => admin_url('admin-ajax.php'),
77 'nonce' => wp_create_nonce('ultimate-store-kit'),
78 'isPro' => function_exists('ultimate_store_kit_license_validation') && ultimate_store_kit_license_validation(),
79 'assetsUrl' => defined('BDTUSK_ASSETS_URL') ? BDTUSK_ASSETS_URL : '',
80 'dismissedDisplayIds' => $dismissed_display_ids,
81 'currentSector' => $current_sector,
82 ];
83
84 wp_localize_script('usk-biggopti', 'UltimateStoreKitBiggoptiConfig', $script_config);
85 }
86
87 /**
88 * Get Remote Biggopties Data from API
89 *
90 * @return array|mixed
91 */
92 private function get_api_biggopties_data() {
93 // API endpoint for biggopties - you can change this to your actual endpoint
94 $api_url = '';
95 $transient_key = 'ultimate_store_kit_biggopties_api';
96
97 $cached_data = get_transient($transient_key);
98
99 if (! empty($cached_data)) {
100 $biggopties = json_decode($cached_data);
101 return $this->extract_biggopties($biggopties);
102 }
103
104 /**
105 * A recent request failed, so don't retry on every admin page load.
106 */
107 if (get_transient($transient_key . '_failed')) {
108 return [];
109 }
110
111 $response = wp_remote_get($api_url, [
112 'timeout' => self::REQUEST_TIMEOUT,
113 'headers' => [
114 'Accept' => 'application/json',
115 ],
116 ]);
117
118 if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) {
119 set_transient($transient_key . '_failed', 1, self::FAILURE_BACKOFF);
120 return [];
121 }
122
123 $response_body = wp_remote_retrieve_body($response);
124
125 $biggopties = json_decode($response_body);
126
127 if (null === $biggopties) {
128 set_transient($transient_key . '_failed', 1, self::FAILURE_BACKOFF);
129 return [];
130 }
131
132 set_transient($transient_key, $response_body, self::CACHE_LIFETIME);
133
134 return $this->extract_biggopties($biggopties);
135 }
136
137 /**
138 * Pull this plugin's records out of the decoded API payload.
139 *
140 * @param mixed $biggopties Decoded API response.
141 * @return array
142 */
143 private function extract_biggopties($biggopties) {
144
145 if (isset($biggopties) && isset($biggopties->{'ultimate-store-kit'})) {
146 $data = $biggopties->{'ultimate-store-kit'};
147 if (is_array($data)) {
148 return $data;
149 }
150 }
151
152 return [];
153 }
154
155 /**
156 * Check if a biggopti should be shown based on its enabled status and date range.
157 *
158 * @param object $biggopti The biggopti data from the API.
159 * @return bool True if the biggopti should be shown, false otherwise.
160 */
161 private function should_show_biggopti($biggopti) {
162 // Development override - set to true to bypass date checks for testing
163 $development_mode = false; // Set to true to bypass date checks
164
165 if ($development_mode) {
166 return true;
167 }
168
169 // Check if the biggopti is enabled
170 if (!isset($biggopti->is_enabled) || !$biggopti->is_enabled) {
171 return false;
172 }
173
174 // Check plugin compatibility
175 if (!$this->is_biggopti_compatible_with_plugin($biggopti)) {
176 return false;
177 }
178
179 // Check if the biggopti has a start date and end date
180 if (!isset($biggopti->start_date) || !isset($biggopti->end_date)) {
181 return false;
182 }
183
184 // Get timezone from biggopti or default to UTC
185 $timezone = isset($biggopti->timezone) ? $biggopti->timezone : 'UTC';
186
187 // Create DateTime objects with proper timezone (using global namespace)
188 $start_date = new \DateTime($biggopti->start_date, new \DateTimeZone($timezone));
189 $end_date = new \DateTime($biggopti->end_date, new \DateTimeZone($timezone));
190 $current_date = new \DateTime('now', new \DateTimeZone($timezone));
191
192 // Convert to timestamps for comparison
193 $start_timestamp = $start_date->getTimestamp();
194 $end_timestamp = $end_date->getTimestamp();
195 $current_timestamp = $current_date->getTimestamp();
196
197 // Check if the current date is within the start and end dates
198 if ($current_timestamp < $start_timestamp || $current_timestamp > $end_timestamp) {
199 return false;
200 }
201
202 // Check if biggopti should be visible after a certain time
203 if (isset($biggopti->visible_after) && $biggopti->visible_after > 0) {
204 $visible_after_timestamp = $start_timestamp + $biggopti->visible_after;
205 if ($current_timestamp < $visible_after_timestamp) {
206 return false;
207 }
208 }
209
210 return true;
211 }
212
213 /**
214 * Check if a biggopti is compatible with the current plugin installation
215 *
216 * @param object $biggopti The biggopti data from the API.
217 * @return bool True if the biggopti should be shown, false otherwise.
218 */
219 private function is_biggopti_compatible_with_plugin($biggopti) {
220 // Get current plugin info
221 $current_plugin_slug = $this->get_current_plugin_slug();
222 $is_pro_active = function_exists('ultimate_store_kit_is_pro_activated') ? ultimate_store_kit_is_pro_activated() : false;
223 $is_lite_active = $current_plugin_slug === 'ultimate-store-kit';
224 $is_pro_plugin = $current_plugin_slug === 'ultimate-store-kit-pro';
225
226 // Get client targets, default to ['both'] if not set or not an array
227 $client_targets = (isset($biggopti->client_targets) && is_array($biggopti->client_targets))
228 ? $biggopti->client_targets
229 : ['both'];
230
231 // Determine if this is targeted at Pro users
232 $pro_targeted = in_array('pro_targeted', $client_targets, true);
233
234 // Ensure client_targets is always an array
235 if (!is_array($client_targets)) {
236 $client_targets = [$client_targets];
237 }
238
239 // Handle pro_targeted parameter (only for free version)
240 if ($pro_targeted && $is_lite_active) {
241 // If pro_targeted is true, only show if pro is NOT active
242 $should_show = !$is_pro_active;
243 return $should_show;
244 }
245
246 // Check if any of the client targets match current plugin status
247 foreach ($client_targets as $target) {
248 $target = trim($target); // Clean up any whitespace
249
250 switch ($target) {
251 case 'pro':
252 // Pro-only biggopties: show only if pro is active
253 if ($is_pro_active) {
254 return true;
255 }
256 break;
257
258 case 'free':
259 if ($is_lite_active) {
260 return true;
261 }
262 break;
263 }
264 }
265
266 return false;
267 }
268
269 /**
270 * Get current plugin slug
271 *
272 * @return string
273 */
274 private function get_current_plugin_slug() {
275 // Get plugin basename from current file
276 $plugin_file = plugin_basename(BDTUSK__FILE__);
277
278 // Extract plugin slug from basename
279 $plugin_slug = dirname($plugin_file);
280
281 return $plugin_slug;
282 }
283
284 /**
285 * Render API biggopti HTML
286 *
287 * @param object $biggopti
288 * @return string
289 */
290 private function render_api_biggopti($biggopti) {
291 ob_start();
292
293 // Add custom CSS if provided
294 if (isset($biggopti->custom_css) && !empty($biggopti->custom_css)) {
295 echo '<style>' . wp_kses_post($biggopti->custom_css) . '</style>';
296 }
297
298 // Prepare background styles
299 $background_style = '';
300 $wrapper_classes = 'bdt-biggopti-wrapper';
301
302 if (isset($biggopti->background_color) && !empty($biggopti->background_color)) {
303 $background_style .= 'background-color: ' . sanitize_text_field($biggopti->background_color) . ';';
304 }
305
306 if (isset($biggopti->image) && !empty($biggopti->image)) {
307 $background_style .= 'background-image: url(' . esc_url_raw($biggopti->image) . ');';
308 $wrapper_classes .= ' has-background-image';
309 }
310
311 ?>
312 <div class="<?php echo esc_attr($wrapper_classes); ?>" <?php echo $background_style ? 'style="' . esc_attr($background_style) . '"' : ''; ?>>
313
314
315 <?php $title = (isset($biggopti->title) && !empty($biggopti->title)) ? $biggopti->title : ''; ?>
316
317 <div class="bdt-api-biggopti-content">
318 <div class="bdt-plugin-logo-wrapper">
319 <img height="auto" width="40" src="<?php echo esc_url(BDTUSK_ASSETS_URL); ?>images/logo.svg" alt="Ultimate Store Kit Logo">
320 </div>
321
322 <div class="bdt-biggopti-content">
323 <div class="bdt-biggopti-content-inner">
324 <?php if (isset($biggopti->logo) && !empty($biggopti->logo)) : ?>
325 <div class="bdt-biggopti-logo-wrapper">
326 <img width="100" src="<?php echo esc_url($biggopti->logo); ?>" alt="Logo">
327 </div>
328 <?php endif; ?>
329 <div class="bdt-biggopti-title-description">
330 <?php if (isset($title) && !empty($title)) : ?>
331 <h2 class="bdt-biggopti-title"><?php echo wp_kses_post($title); ?></h2>
332 <?php endif; ?>
333
334 <?php if (isset($biggopti->content) && !empty($biggopti->content)) : ?>
335 <div class="bdt-biggopti-html-content">
336 <?php echo wp_kses_post($biggopti->content); ?>
337 </div>
338 <?php endif; ?>
339 </div>
340 </div>
341
342 <div class="bdt-biggopti-content-right">
343 <?php
344 // Only show countdown if it's enabled, has an end date, and the end date is in the future
345 $show_countdown = isset($biggopti->show_countdown) && $biggopti->show_countdown && isset($biggopti->end_date);
346 if ($show_countdown) {
347 $end_timestamp = strtotime($biggopti->end_date);
348 $current_timestamp = current_time('timestamp');
349 $show_countdown = $end_timestamp > $current_timestamp;
350 }
351 ?>
352 <?php if ($show_countdown) : ?>
353 <div class="bdt-biggopti-countdown" data-end-date="<?php echo esc_attr($biggopti->end_date); ?>" data-timezone="<?php echo esc_attr($biggopti->timezone ? $biggopti->timezone : 'UTC'); ?>">
354 <div class="countdown-timer">Loading...</div>
355 </div>
356 <?php endif; ?>
357
358 <?php if (isset($biggopti->link) && !empty($biggopti->link)) : ?>
359 <div class="bdt-biggopti-btn">
360 <a href="<?php echo esc_url($biggopti->link); ?>" target="_blank">
361 <div class="nm-biggopti-btn">
362 <?php echo isset($biggopti->button_text) ? esc_html($biggopti->button_text) : 'Read More'; ?>
363 <span class="dashicons dashicons-arrow-right-alt"></span>
364 </div>
365 </a>
366 </div>
367 <?php endif; ?>
368 </div>
369 </div>
370 </div>
371 </div>
372 <?php
373 return ob_get_clean();
374 }
375
376 public static function add_biggopti($args = []) {
377 if (is_array($args)) {
378 self::$biggopties[] = $args;
379 }
380 }
381
382 /**
383 * AJAX: Build and return API biggopties HTML for dynamic injection
384 */
385 public function ajax_fetch_api_biggopties() {
386 $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
387 if (!wp_verify_nonce($nonce, 'ultimate-store-kit')) {
388 wp_send_json_error(['message' => 'invalid_nonce']);
389 }
390
391 if (!current_user_can('manage_options')) {
392 wp_send_json_error(['message' => 'forbidden']);
393 }
394
395 // Don't show biggopties on plugin/theme install and upload pages
396 $current_url = isset($_POST['current_url']) ? sanitize_text_field(wp_unslash($_POST['current_url'])) : '';
397
398 if (!empty($current_url)) {
399 $excluded_patterns = [
400 'plugin-install.php',
401 'theme-install.php',
402 'action=upload-plugin',
403 'action=upload-theme'
404 ];
405
406 foreach ($excluded_patterns as $pattern) {
407 if (strpos($current_url, $pattern) !== false) {
408 wp_send_json_success(['html' => '']);
409 }
410 }
411 }
412
413 $biggopties = $this->get_api_biggopties_data();
414 $grouped_biggopties = [];
415
416 if (is_array($biggopties)) {
417 foreach ($biggopties as $index => $biggopti) {
418 if ($this->should_show_biggopti($biggopti)) {
419 $display_id = isset($biggopti->display_id) ? $biggopti->display_id : 'default-' . $index;
420 if (!isset($grouped_biggopties[$display_id])) {
421 $grouped_biggopties[$display_id] = $biggopti;
422 }
423 }
424 }
425 }
426
427 // Build biggopties using the same pipeline as synchronous rendering
428 foreach ($grouped_biggopties as $display_id => $biggopti) {
429 $biggopti_id = isset($biggopti->id) ? $display_id : $biggopti->id;
430
431 self::add_biggopti([
432 'id' => 'api-biggopti-' . $biggopti_id,
433 'type' => isset($biggopti->type) ? $biggopti->type : 'info',
434 'category' => isset($biggopti->category) ? $biggopti->category : 'regular',
435 'dismissible' => true,
436 'html_message' => $this->render_api_biggopti($biggopti),
437 'dismissible-meta' => 'transient',
438 'dismissible-time' => isset($biggopti->end_date) ? max((new \DateTime($biggopti->end_date, new \DateTimeZone('UTC')))->getTimestamp() - time(), 0) : WEEK_IN_SECONDS,
439 ]);
440 }
441
442 ob_start();
443 $this->show_biggopties();
444 $markup = ob_get_clean();
445
446 wp_send_json_success(['html' => $markup]);
447 }
448
449 /**
450 * Dismiss Biggopti.
451 */
452 public function dismiss() {
453 $nonce = (isset($_POST['_wpnonce'])) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
454 // Not sanitize_key(): the id is echoed back from the notice markup and has to
455 // match the key show_biggopties() reads verbatim, so it is validated below
456 // rather than rewritten here.
457 $id = (isset($_POST['id'])) ? sanitize_text_field(wp_unslash($_POST['id'])) : '';
458 $time = (isset($_POST['time'])) ? absint(wp_unslash($_POST['time'])) : 0;
459 $meta = (isset($_POST['meta'])) ? sanitize_key(wp_unslash($_POST['meta'])) : '';
460
461 if (! wp_verify_nonce($nonce, 'ultimate-store-kit')) {
462 wp_send_json_error();
463 }
464
465 if (! current_user_can('manage_options')) {
466 wp_send_json_error();
467 }
468
469 /**
470 * The id becomes a transient or user-meta key, so it has to be one of ours.
471 * Every notice rendered by show_biggopties() carries this prefix; anything
472 * else is a request to write a key this handler has no business writing.
473 */
474 if (! preg_match('/^' . preg_quote(self::DISMISS_KEY_PREFIX, '/') . '[A-Za-z0-9_.-]{1,120}$/', $id)) {
475 wp_send_json_error();
476 }
477
478 // Likewise the lifetime is request-supplied — keep it inside a sane window
479 // so a dismissal cannot be made effectively permanent.
480 $time = min(max($time, MINUTE_IN_SECONDS), YEAR_IN_SECONDS);
481
482 /**
483 * Valid inputs?
484 */
485 if (!empty($id)) {
486 // Handle regular biggopties
487 if ('user' === $meta) {
488 update_user_meta(get_current_user_id(), $id, true);
489 } else {
490 set_transient($id, true, $time);
491
492 // Also store in options table for persistence
493 $dismissals_option = get_option('bdt_biggopti_dismissals', []);
494 $dismissals_option[$id] = [
495 'dismissed_at' => time(),
496 'expires_at' => time() + intval($time),
497 ];
498 update_option('bdt_biggopti_dismissals', $dismissals_option, false);
499 }
500
501 wp_send_json_success();
502 }
503
504 wp_send_json_error();
505 }
506
507 /**
508 * Biggopti Types
509 */
510 public function show_biggopties() {
511
512 $defaults = [
513 'id' => '',
514 'type' => 'info',
515 'category' => 'regular',
516 'show_if' => true,
517 'title' => '',
518 'message' => '',
519 'class' => 'ultimate-store-kit-biggopti',
520 'dismissible' => false,
521 'dismissible-meta' => 'transient',
522 'dismissible-time' => WEEK_IN_SECONDS,
523 'data' => '',
524 'action_link' => '',
525 ];
526
527 foreach (self::$biggopties as $key => $biggopti) {
528
529 $biggopti = wp_parse_args($biggopti, $defaults);
530
531 // Check if biggopti is for White Label
532 if (defined('BDTUSK_WL') && $biggopti['category'] === 'regular') {
533 continue;
534 }
535
536 $classes = ['biggopti'];
537
538 $classes[] = $biggopti['class'];
539 if (isset($biggopti['type'])) {
540 $classes[] = 'biggopti-' . $biggopti['type'];
541 }
542
543 // Is biggopti dismissible?
544 if (true === $biggopti['dismissible']) {
545 $classes[] = 'is-dismissible';
546
547 // Dismissable time.
548 $biggopti['data'] = ' dismissible-time=' . esc_attr($biggopti['dismissible-time']) . ' ';
549 }
550
551 // Biggopti ID.
552 $biggopti_id = self::DISMISS_KEY_PREFIX . $biggopti['id'];
553 $biggopti['id'] = $biggopti_id;
554 if (!isset($biggopti['id'])) {
555 $biggopti_id = self::DISMISS_KEY_PREFIX . $biggopti['id'];
556 $biggopti['id'] = $biggopti_id;
557 } else {
558 $biggopti_id = $biggopti['id'];
559 }
560
561 $biggopti['classes'] = implode(' ', $classes);
562
563 // User meta.
564 $biggopti['data'] .= ' dismissible-meta=' . esc_attr($biggopti['dismissible-meta']) . ' ';
565 if ('user' === $biggopti['dismissible-meta']) {
566 $expired = get_user_meta(get_current_user_id(), $biggopti_id, true);
567 } elseif ('transient' === $biggopti['dismissible-meta']) {
568 $expired = get_transient($biggopti_id);
569
570 // If transient not found, check options table for persistent dismissal
571 if (false === $expired || empty($expired)) {
572 $dismissals_option = get_option('bdt_biggopti_dismissals', []);
573 if (isset($dismissals_option[$biggopti_id])) {
574 $dismissal = $dismissals_option[$biggopti_id];
575 // Check if dismissal is still valid (not expired)
576 if (isset($dismissal['expires_at']) && time() < $dismissal['expires_at']) {
577 $expired = true;
578 } else {
579 // Clean up expired dismissal from options
580 unset($dismissals_option[$biggopti_id]);
581 update_option('bdt_biggopti_dismissals', $dismissals_option, false);
582 }
583 }
584 }
585 }
586
587 // Biggopties visible after transient expire.
588 if (isset($biggopti['show_if'])) {
589
590 if (true === $biggopti['show_if']) {
591
592 // Is transient expired?
593 if (false === $expired || empty($expired)) {
594 self::biggopti_layout($biggopti);
595 }
596 }
597 } else {
598
599 // No transient biggopties.
600 self::biggopti_layout($biggopti);
601 }
602 }
603 }
604
605 /**
606 * New Biggopti Layout
607 * @param array $biggopti Biggopti biggopti_layout.
608 * @return void
609 * @since 6.11.3
610 */
611
612 public static function biggopti_layout($biggopti = []) {
613
614 if (isset($biggopti['html_message']) && ! empty($biggopti['html_message'])) {
615 self::new_biggopti_layout($biggopti);
616 return;
617 }
618
619 ?>
620 <div id="<?php echo esc_attr($biggopti['id']); ?>" class="<?php echo esc_attr($biggopti['classes']); ?>" <?php echo esc_attr($biggopti['data']); ?>>
621 <div class="bdt-biggopti-wrapper">
622 <div class="bdt-biggopti-icon-wrapper">
623 <img height="25" width="25" src="<?php echo esc_url(BDTUSK_ASSETS_URL); ?>images/logo.svg">
624 </div>
625
626 <div class="bdt-biggopti-content">
627 <?php if (isset($biggopti['title']) && !empty($biggopti['title'])) : ?>
628 <h2 class="bdt-biggopti-title"><?php echo wp_kses_post($biggopti['title']); ?></h2>
629 <?php endif; ?>
630
631 <p class="bdt-biggopti-text"><?php echo wp_kses_post($biggopti['message']); ?></p>
632
633 <?php if (isset($biggopti['action_link']) && !empty($biggopti['action_link'])) : ?>
634 <div class="bdt-biggopti-btn">
635 <a href="#">Renew Now</a>
636 </div>
637 <?php endif; ?>
638 </div>
639 </div>
640 </div>
641 <?php
642 }
643
644 public static function new_biggopti_layout($biggopti = []) {
645 ?>
646 <div id="<?php echo esc_attr($biggopti['id']); ?>" class="<?php echo esc_attr($biggopti['classes']); ?>" <?php echo esc_attr($biggopti['data']); ?>>
647 <?php
648 echo wp_kses_post($biggopti['html_message']);
649 ?>
650 </div>
651
652 <?php
653 }
654 }
655