PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / app / Shared / DataProvider / NotificationData.php

NotificationData.php in Extendify 3.2.1, at app/Shared/DataProvider/NotificationData.php

165 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Notification data.
5 */
6
7 namespace Extendify\Shared\DataProvider;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\Constants;
12 use Extendify\PartnerData;
13
14 /**
15 * Fetches and caches the notifications.
16 */
17 class NotificationData
18 {
19 /**
20 * Kept in sync with the token withSiteHost() swaps in src/Notifications/notification-link.js.
21 *
22 * @var string
23 */
24 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility
25 const SITE_URL_TOKEN = '{SITEURL}';
26
27 /**
28 * Registers the wp-cron handler that refreshes stale notifications.
29 *
30 * @return void
31 */
32 public static function scheduleCache()
33 {
34 \add_action('extendify_notifications_refresh', [self::class, 'refresh']);
35 }
36
37 /**
38 * Returns the cached notifications, refreshing on cold start and
39 * scheduling a background refresh when the cache is stale.
40 *
41 * @return array
42 */
43 public static function get()
44 {
45 if (!PartnerData::$id) {
46 return [];
47 }
48
49 $locale = \get_locale();
50 $cached = \get_option('extendify_notifications_' . $locale);
51
52 if (!is_array($cached) || !isset($cached['fetchedAt'])) {
53 return self::withSafeLinks(self::refresh($locale) ?? []);
54 }
55
56 $age = time() - $cached['fetchedAt'];
57 // A host whose requests are being refused would otherwise re-ask every TTL.
58 $ttl = empty($cached['failed']) ? (5 * MINUTE_IN_SECONDS) : HOUR_IN_SECONDS;
59 if ($age > $ttl) {
60 if (!\wp_next_scheduled('extendify_notifications_refresh', [$locale])) {
61 \wp_schedule_single_event(time(), 'extendify_notifications_refresh', [$locale]);
62 if (\is_admin()) {
63 \spawn_cron();
64 }
65 }
66 }
67
68 return self::withSafeLinks($cached['data'] ?? []);
69 }
70
71 /**
72 * Drops any link a browser must not follow.
73 *
74 * A feed link becomes an href in wp-admin, so javascript: would run as the site owner.
75 *
76 * @param mixed $notifications - Notifications as the feed sent them.
77 * @return array
78 */
79 private static function withSafeLinks($notifications)
80 {
81 if (!is_array($notifications)) {
82 return [];
83 }
84
85 return array_map([self::class, 'withSafeLink'], $notifications);
86 }
87
88 /**
89 * Drops one notification's link unless it is http, https or site-relative.
90 *
91 * esc_url_raw strips the placeholder's braces, so only the probe copy goes through it.
92 *
93 * @param mixed $notification - One notification as the feed sent it.
94 * @return mixed
95 */
96 private static function withSafeLink($notification)
97 {
98 if (!is_array($notification) || !isset($notification['link'])) {
99 return $notification;
100 }
101
102 if (!is_string($notification['link'])) {
103 unset($notification['link']);
104
105 return $notification;
106 }
107
108 $host = (string) \wp_parse_url(\home_url(), PHP_URL_HOST);
109 $probe = str_replace(self::SITE_URL_TOKEN, $host, $notification['link']);
110
111 if (\esc_url_raw($probe, ['http', 'https']) === '') {
112 unset($notification['link']);
113 }
114
115 return $notification;
116 }
117
118 /**
119 * Fetch notifications from the API and persist them.
120 * Called synchronously on cold start and via wp-cron when the cache is stale.
121 *
122 * @param string $locale - Locale to fetch (cron may run in a different site locale).
123 * @return array|null
124 */
125 public static function refresh($locale)
126 {
127 if (!PartnerData::$id) {
128 return [];
129 }
130
131 $optionKey = 'extendify_notifications_' . $locale;
132
133 $url = \add_query_arg(
134 ['partner' => PartnerData::$id, 'wp_language' => $locale],
135 Constants::AI_HOST . '/api/notifications'
136 );
137 $response = \wp_remote_get($url, ['headers' => ['Accept' => 'application/json']]);
138 $result = \is_wp_error($response)
139 ? null
140 : json_decode(\wp_remote_retrieve_body($response), true);
141
142 if (!is_array($result) || !is_array($result['notifications'] ?? null)) {
143 $cached = \get_option($optionKey);
144 \update_option(
145 $optionKey,
146 [
147 'data' => is_array($cached) ? ($cached['data'] ?? []) : [],
148 'fetchedAt' => time(),
149 'failed' => true,
150 ],
151 false
152 );
153 return null;
154 }
155
156 $notifications = $result['notifications'];
157 \update_option(
158 $optionKey,
159 ['data' => $notifications, 'fetchedAt' => time()],
160 false
161 );
162 return $notifications;
163 }
164 }
165