PluginProbe
Ultimate Store Kit / 3.1.0
Ultimate Store Kit v3.1.0
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 2.0.6 All 92 releases
ultimate-store-kit / includes / Admin / Feeds.php

Feeds.php in Ultimate Store Kit 3.1.0, at includes/Admin/Feeds.php

323 lines 8.5 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 use UltimateStoreKit\Base\Singleton;
6
7 if (! defined('ABSPATH')) {
8 exit;
9 }
10
11 /**
12 * Class Feeds
13 */
14 class Feeds {
15 use Singleton;
16 private $settings;
17
18 /**
19 * Max seconds to wait for a remote feed. Kept short so a slow or dead
20 * endpoint can never stall the admin dashboard into a gateway timeout.
21 */
22 const REQUEST_TIMEOUT = 5;
23
24 /**
25 * How long to skip remote requests after a failure.
26 */
27 const FAILURE_BACKOFF = HOUR_IN_SECONDS;
28
29 /**
30 * Static variable to track if the feed has been displayed
31 */
32 private static $feed_displayed = false;
33
34 /**
35 * Feeds constructor.
36 */
37 public function __construct($settings = null) {
38 $this->settings = $settings ?: [
39 'feed_title' => 'BdThemes News & Updates',
40 'transient_key' => 'bdthemes_product_feeds',
41 'feed_link' => 'https://bdthemes.com/feed',
42 'remote_feed_link' => 'https://dashboard.bdthemes.io/wp-json/bdthemes/v1/product-feed/?product_category=element-pack',
43 'text_domain' => 'ultimate-store-kit',
44 'footer_links' => [
45 [
46 'url' => 'https://bdthemes.com/blog/',
47 'title' => 'Blog',
48 ],
49 [
50 'url' => 'https://bdthemes.com/knowledge-base/',
51 'title' => 'Docs',
52 ],
53 [
54 'url' => 'https://store.bdthemes.com/',
55 'title' => 'Get Pro',
56 ],
57 [
58 'url' => 'https://feedback.elementpack.pro/announcements/',
59 'title' => 'Changelog',
60 ],
61 ],
62 ];
63 add_action('wp_dashboard_setup', [$this, 'register_rss_feeds']);
64 }
65
66 /**
67 * Register RSS Feeds for Element Pack
68 */
69 public function register_rss_feeds() {
70 if (self::$feed_displayed) {
71 /**
72 * If the feed has already been displayed, do not add it again
73 */
74 return;
75 }
76
77 wp_add_dashboard_widget(
78 'bdt-dashboard-overview',
79 esc_html($this->settings['feed_title']),
80 [$this, 'display_rss_feeds_content'],
81 null,
82 null,
83 'column4',
84 'core'
85 );
86
87 /**
88 * Mark the feed as displayed
89 */
90 self::$feed_displayed = true;
91 }
92
93 /**
94 * Display RSS Feeds Content
95 */
96 public function display_rss_feeds_content() {
97 $feeds = $this->get_remote_feeds_data();
98 if (is_array($feeds)) {
99 foreach ($feeds as $feed) {
100 if (! is_object($feed)) {
101 continue;
102 }
103
104 $demo_link = isset($feed->demo_link) ? $feed->demo_link : '';
105 $image = isset($feed->image) ? $feed->image : '';
106 $content = isset($feed->content) ? $feed->content : '';
107 ?>
108 <div class="activity-block">
109 <a href="<?php echo esc_url($demo_link); ?>" target="_blank" style="margin-bottom:10px; display: inline-block;">
110 <img src="<?php echo esc_url($image); ?>" style="width:100%;min-height:240px;">
111 </a>
112 <p>
113 <?php echo wp_kses_post(wp_trim_words(wp_strip_all_tags($content), 50)); ?>
114 <a href="<?php echo esc_url($demo_link); ?>" target="_blank">
115 <?php esc_html_e('Learn more...', 'ultimate-store-kit'); ?>
116 </a>
117 </p>
118 </div>
119 <?php
120 }
121 }
122 echo wp_kses_post($this->get_rss_posts_data());
123 }
124
125 /**
126 * Get Remote Feeds Data
127 *
128 * @return array|mixed
129 */
130 private function get_remote_feeds_data() {
131 $transient_key = $this->settings['transient_key'];
132 $cached_data = get_transient($transient_key);
133
134 if (! empty($cached_data)) {
135 $decoded = json_decode($cached_data);
136 return is_array($decoded) ? $decoded : [];
137 }
138
139 /**
140 * A recent request already failed, so don't block the dashboard again.
141 * Serve the last known good response until the backoff expires.
142 */
143 if (get_transient($transient_key . '_failed')) {
144 return $this->get_fallback_feeds_data();
145 }
146
147 $response = wp_remote_get(
148 $this->settings['remote_feed_link'],
149 array(
150 'timeout' => self::REQUEST_TIMEOUT,
151 'headers' => array(
152 'Accept' => 'application/json',
153 ),
154 )
155 );
156
157 if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) {
158 set_transient($transient_key . '_failed', 1, self::FAILURE_BACKOFF);
159 return $this->get_fallback_feeds_data();
160 }
161
162 $response_body = wp_remote_retrieve_body($response);
163 $decoded = json_decode($response_body);
164
165 if (! is_array($decoded)) {
166 set_transient($transient_key . '_failed', 1, self::FAILURE_BACKOFF);
167 return $this->get_fallback_feeds_data();
168 }
169
170 set_transient($transient_key, $response_body, 6 * HOUR_IN_SECONDS);
171
172 /**
173 * Keep a copy outside the transient so the widget still has something
174 * to show while the remote endpoint is unreachable.
175 */
176 update_option($transient_key . '_fallback', $response_body, false);
177
178 return $decoded;
179 }
180
181 /**
182 * Get the last successfully fetched feeds, if any.
183 *
184 * @return array
185 */
186 private function get_fallback_feeds_data() {
187 $fallback = get_option($this->settings['transient_key'] . '_fallback');
188
189 if (empty($fallback)) {
190 return [];
191 }
192
193 $decoded = json_decode($fallback);
194
195 return is_array($decoded) ? $decoded : [];
196 }
197
198 /**
199 * Get RSS Posts Data
200 *
201 * @return string
202 */
203 private function get_rss_posts_data() {
204 $transient_key = $this->settings['transient_key'] . '_rss';
205 $cached_data = get_transient($transient_key);
206
207 if (! empty($cached_data)) {
208 /**
209 * Decode as associative array
210 */
211 $rss_items = json_decode($cached_data, true);
212
213 if (! is_array($rss_items)) {
214 $rss_items = [];
215 }
216 } elseif (get_transient($transient_key . '_failed')) {
217 /**
218 * A recent fetch failed, so skip the blocking request entirely.
219 */
220 $rss_items = [];
221 } else {
222 include_once ABSPATH . WPINC . '/feed.php';
223
224 add_action('wp_feed_options', [$this, 'set_feed_timeout']);
225 $rss = fetch_feed($this->settings['feed_link']);
226 remove_action('wp_feed_options', [$this, 'set_feed_timeout']);
227
228 if (is_wp_error($rss)) {
229 set_transient($transient_key . '_failed', 1, self::FAILURE_BACKOFF);
230 return '<li>' . esc_html__('Items Not Found', 'ultimate-store-kit') . '.</li>';
231 }
232
233 $maxitems = $rss->get_item_quantity(5);
234 $rss_items = $rss->get_items(0, $maxitems);
235
236 /**
237 * Convert RSS items to a simpler array to avoid serialization issues
238 */
239 $simplified_rss_items = array_map(function ($item) {
240 return [
241 'title' => $item->get_title(),
242 'link' => $item->get_permalink(),
243 'date' => $item->get_date('U'),
244 'content' => $item->get_content(),
245 ];
246 }, $rss_items);
247
248 set_transient($transient_key, json_encode($simplified_rss_items), 6 * HOUR_IN_SECONDS);
249 $rss_items = $simplified_rss_items;
250 }
251
252 ob_start();
253 ?>
254 <div class="bdt-widget">
255 <ul>
256 <?php if (empty($rss_items)) : ?>
257 <li><?php esc_html_e('Items Not Found', 'ultimate-store-kit'); ?>.</li>
258 <?php else : ?>
259 <?php foreach ($rss_items as $item) : ?>
260 <li>
261 <a target="_blank" href="<?php echo esc_url($item['link']); ?>"
262 title="<?php echo esc_html($item['date']); ?>">
263 <?php if ($this->is_feed_item_new($item['date'])) : ?>
264 <span class="bdt-feed-badge bdt-feed-badge--new"><?php esc_html_e('New', 'ultimate-store-kit'); ?></span>
265 <?php endif; ?>
266 <?php echo esc_html($item['title']); ?>
267 </a>
268 <span class="bdt-date" style="display: block; margin: 0;">
269 <?php echo esc_html(human_time_diff($item['date'], current_time('timestamp')) . ' ' . __('ago', 'ultimate-store-kit')); ?>
270 </span>
271 <div class="bdt-summary">
272 <?php echo esc_html(wp_html_excerpt($item['content'], 120) . ' [...]'); ?>
273 </div>
274 </li>
275 <?php endforeach; ?>
276 <?php endif; ?>
277 </ul>
278 </div>
279 <p class="community-events-footer" style="margin: 12px -12px 6px -12px; padding: 12px 12px 0px;">
280 <?php
281 foreach ($this->settings['footer_links'] as $link) {
282 printf(
283 '<a href="%s" target="_blank">%s <span class="screen-reader-text"> (opens in a new tab)</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
284 esc_url($link['url']),
285 esc_html($link['title'])
286 );
287
288 if (next($this->settings['footer_links'])) {
289 echo ' | ';
290 }
291 }
292 ?>
293 </p>
294 <?php
295 return ob_get_clean();
296 }
297
298 /**
299 * Keep SimplePie's socket timeout in line with our own limit.
300 *
301 * @param object $feed SimplePie instance.
302 * @return void
303 */
304 public function set_feed_timeout($feed) {
305 $feed->set_timeout(self::REQUEST_TIMEOUT);
306 }
307
308 /**
309 * Check if a feed item is "new" (published within the last 7 days).
310 *
311 * @param int|string $date Unix timestamp.
312 * @return bool
313 */
314 private function is_feed_item_new($date) {
315 $timestamp = is_numeric($date) ? (int) $date : strtotime($date);
316 if (! $timestamp) {
317 return false;
318 }
319 $cutoff = time() - (7 * DAY_IN_SECONDS);
320 return $timestamp >= $cutoff;
321 }
322 }
323