PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.7.0 All 126 releases
extendify / app / PluginNotifications / Admin.php

Admin.php in Extendify 3.1.5, at app/PluginNotifications/Admin.php

318 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\PluginNotifications;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 use Extendify\Config;
8 use Extendify\Shared\Services\SupportedPlugins;
9
10 class Admin
11 {
12 private static $pluginsCache = null;
13
14 public function __construct()
15 {
16 \add_action('in_admin_header', [$this, 'captureAndSuppressNotices'], 1);
17 \add_action('admin_init', [AdminPage::class, 'handleActions']);
18 \add_action('admin_menu', [$this, 'addMenu']);
19 \add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
20 }
21
22 public function captureAndSuppressNotices()
23 {
24 global $wp_filter;
25
26 $allCallbacks = [];
27
28 foreach (['admin_notices', 'all_admin_notices'] as $hook) {
29 if (!isset($wp_filter[$hook])) {
30 continue;
31 }
32
33 foreach ($wp_filter[$hook]->callbacks as $priority => $callbacks) {
34 foreach ($callbacks as $id => $cb) {
35 $allCallbacks[] = $cb;
36 }
37 }
38 }
39
40 \remove_all_actions('admin_notices');
41 \remove_all_actions('all_admin_notices');
42
43 \add_action('admin_notices', function () use ($allCallbacks) {
44 $stored = \get_option('extendify_captured_notifications') ?: [];
45 $changed = false;
46
47 foreach ($allCallbacks as $cb) {
48 $fn = $cb['function'];
49 $file = self::getCallbackFile($fn);
50
51 $extendifyDir = basename(rtrim(EXTENDIFY_PATH, '/\\'));
52 try {
53 if ($file && str_contains($file, '/plugins/' . $extendifyDir . '/')) {
54 \call_user_func($fn);
55 continue;
56 }
57
58 \ob_start();
59 \call_user_func($fn);
60 $html = \ob_get_clean();
61 } catch (\Throwable $e) {
62 \ob_end_clean();
63 \add_action('admin_notices', $fn, 11);
64 continue;
65 }
66
67 if (empty(trim($html))) {
68 continue;
69 }
70
71 if (empty(trim(strip_tags($html)))) {
72 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
73 echo $html;
74 continue;
75 }
76
77 $slug = self::extractPluginSlug($file);
78 if (!$slug || !in_array($slug, SupportedPlugins::SLUGS, true)) {
79 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
80 echo $html;
81 continue;
82 }
83
84 $hash = md5(trim($html));
85 $sourceName = self::resolvePluginNameFromSlug($slug);
86
87 if (isset($stored[$hash])) {
88 if ($stored[$hash]['source_name'] !== $sourceName) {
89 $stored[$hash]['source_name'] = $sourceName;
90 $changed = true;
91 }
92 continue;
93 }
94
95 $stored[$hash] = [
96 'id' => $hash,
97 'content' => $html,
98 'source_name' => $sourceName,
99 'notice_type' => self::detectNoticeType($html),
100 'first_seen' => current_time('mysql'),
101 'dismissed' => false,
102 ];
103 $changed = true;
104 }
105
106 if ($changed) {
107 if (count($stored) > 200) {
108 $stored = self::pruneOldDismissed($stored);
109 }
110
111 \update_option('extendify_captured_notifications', $stored, false);
112 }
113 });
114 }
115
116 private static function getCallbackFile($fn)
117 {
118 try {
119 if ($fn instanceof \Closure) {
120 return (new \ReflectionFunction($fn))->getFileName();
121 }
122
123 if (is_array($fn)) {
124 return (new \ReflectionMethod($fn[0], $fn[1]))->getFileName();
125 }
126
127 if (is_string($fn) && function_exists($fn)) {
128 return (new \ReflectionFunction($fn))->getFileName();
129 }
130 } catch (\ReflectionException $e) {
131 return null;
132 }
133
134 return null;
135 }
136
137 private static function extractPluginSlug($file)
138 {
139 if (!$file) {
140 return null;
141 }
142
143 if (preg_match('#[/\\\\]plugins[/\\\\]([^/\\\\]+)[/\\\\]#', $file, $matches)) {
144 return $matches[1];
145 }
146
147 return null;
148 }
149
150 private static function resolvePluginNameFromSlug($slug)
151 {
152 if (!$slug) {
153 return 'WordPress';
154 }
155
156 if (self::$pluginsCache === null) {
157 if (!function_exists('get_plugins')) {
158 require_once ABSPATH . 'wp-admin/includes/plugin.php';
159 }
160
161 self::$pluginsCache = \get_plugins();
162 }
163
164 foreach (self::$pluginsCache as $pluginFile => $data) {
165 if (strpos($pluginFile, $slug . '/') === 0) {
166 return $data['Name'];
167 }
168 }
169
170 return $slug;
171 }
172
173 private static function detectNoticeType($html)
174 {
175 if (str_contains($html, 'notice-error')) {
176 return 'error';
177 }
178
179 if (str_contains($html, 'notice-warning')) {
180 return 'warning';
181 }
182
183 if (str_contains($html, 'notice-success')) {
184 return 'success';
185 }
186
187 return 'info';
188 }
189
190 private static function pruneOldDismissed($stored)
191 {
192 uasort($stored, function ($a, $b) {
193 if ($a['dismissed'] !== $b['dismissed']) {
194 return $a['dismissed'] ? -1 : 1;
195 }
196
197 return strcmp($a['first_seen'], $b['first_seen']);
198 });
199
200 return array_slice($stored, -200, null, true);
201 }
202
203 public function addMenu()
204 {
205 $stored = \get_option('extendify_captured_notifications') ?: [];
206 $count = count(array_filter($stored, function ($n) {
207 return !$n['dismissed'];
208 }));
209
210 $badge = $count > 0
211 ? sprintf(' <span class="awaiting-mod">%d</span>', $count)
212 : '';
213
214 $menuLabel = sprintf(
215 '%s <span style="white-space:nowrap">%s%s</span>',
216 \__('Plugin', 'extendify-local'),
217 \__('Notifications', 'extendify-local'),
218 $badge
219 );
220
221 \add_submenu_page(
222 'index.php',
223 \__('Plugin Notifications', 'extendify-local'),
224 $menuLabel,
225 Config::$requiredCapability,
226 'extendify-notifications',
227 [AdminPage::class, 'render']
228 );
229 }
230
231 public function enqueueAssets($hook)
232 {
233 if ($hook !== 'dashboard_page_extendify-notifications') {
234 return;
235 }
236
237 \wp_add_inline_style('wp-admin', '
238 .extendify-notice-detail td {
239 padding: 12px 16px;
240 background: #f9f9f9;
241 }
242 .extendify-notice-detail .notice {
243 margin: 0;
244 box-shadow: none;
245 }
246 .extendify-notice-badge {
247 display: inline-block;
248 padding: 2px 8px;
249 border-radius: 3px;
250 font-size: 12px;
251 font-weight: 500;
252 }
253 .extendify-notice-badge.type-error { background: #fcf0f0; color: #8a1f1f; }
254 .extendify-notice-badge.type-warning { background: #fef8ee; color: #8a6d1f; }
255 .extendify-notice-badge.type-success { background: #edf8ee; color: #1f6d3a; }
256 .extendify-notice-badge.type-info { background: #f0f0fc; color: #1f3a8a; }
257 ');
258
259 \wp_add_inline_script('common', "
260 document.addEventListener('click', function(e) {
261 var btn = e.target.closest('.extendify-notice-toggle');
262 if (!btn) return;
263 e.preventDefault();
264 var id = btn.dataset.noticeId;
265 var row = document.querySelector('tr[data-detail=\"' + id + '\"]');
266 if (row) {
267 var isHidden = row.style.display === 'none';
268 row.style.display = isHidden ? '' : 'none';
269 btn.textContent = isHidden ?
270 '" . esc_js(__('Hide', 'extendify-local')) . "' :
271 '" . esc_js(__('View', 'extendify-local')) . "';
272 }
273 });
274 ");
275 }
276
277 public static function getNotices($filter = 'active')
278 {
279 $stored = \get_option('extendify_captured_notifications') ?: [];
280
281 if ($filter === 'active') {
282 return array_filter($stored, function ($n) {
283 return !$n['dismissed'];
284 });
285 }
286
287 if ($filter === 'dismissed') {
288 return array_filter($stored, function ($n) {
289 return $n['dismissed'];
290 });
291 }
292
293 return $stored;
294 }
295
296 public static function dismissNotice($id)
297 {
298 $stored = \get_option('extendify_captured_notifications') ?: [];
299
300 if (isset($stored[$id])) {
301 $stored[$id]['dismissed'] = true;
302 \update_option('extendify_captured_notifications', $stored, false);
303 }
304 }
305
306 public static function dismissAll()
307 {
308 $stored = \get_option('extendify_captured_notifications') ?: [];
309
310 foreach ($stored as &$notice) {
311 $notice['dismissed'] = true;
312 }
313 unset($notice);
314
315 \update_option('extendify_captured_notifications', $stored, false);
316 }
317 }
318