| @@ -871,8 +871,41 @@ | ||
| 871 | 871 | { |
| 872 | 872 | return array_fill_keys(array_values($attrs), true); |
| 873 | 873 | } |
| 874 | 874 | |
| 875 | + /** | |
| 876 | + * Whether the site is served over HTTPS well enough to run the Adminify UI. | |
| 877 | + * | |
| 878 | + * Mirrors the exact condition that gates the dashboard frame in | |
| 879 | + * Inc/Admin/Admin.php ( is_ssl() + an https:// site URL ), so the AJAX | |
| 880 | + * gate behaviour stays consistent with where the UI actually loads. | |
| 881 | + * | |
| 882 | + * @return array{ready:bool,message:string} | |
| 883 | + */ | |
| 884 | + public static function adminify_ui_https_status() | |
| 885 | + { | |
| 886 | + $is_ssl = is_ssl(); | |
| 887 | + $site_https = ( 0 === stripos( (string) site_url(), 'https://' ) ); | |
| 888 | + | |
| 889 | + if ( $is_ssl && $site_https ) { | |
| 890 | + return [ | |
| 891 | + 'ready' => true, | |
| 892 | + 'message' => '', | |
| 893 | + ]; | |
| 894 | + } | |
| 895 | + | |
| 896 | + if ( ! $site_https ) { | |
| 897 | + $message = esc_html__( 'Site URL is not HTTPS. Switch to HTTPS first.', 'adminify' ); | |
| 898 | + } else { | |
| 899 | + $message = esc_html__( 'Open the dashboard over HTTPS first.', 'adminify' ); | |
| 900 | + } | |
| 901 | + | |
| 902 | + return [ | |
| 903 | + 'ready' => false, | |
| 904 | + 'message' => $message, | |
| 905 | + ]; | |
| 906 | + } | |
| 907 | + | |
| 875 | 908 | public static function kses_allowed_html() |
| 876 | 909 | { |
| 877 | 910 | /** |
| 878 | 911 | * 'post' returns the tags allowed in post content (a, p, strong, em, etc.), |
| @@ -1208,8 +1241,16 @@ | ||
| 1208 | 1241 | } |
| 1209 | 1242 | return ''; |
| 1210 | 1243 | } |
| 1211 | 1244 | |
| 1245 | + public static function new_badge($badge_text = 'New') { | |
| 1246 | + if( empty( $badge_text ) ) { | |
| 1247 | + return ''; | |
| 1248 | + } | |
| 1249 | + | |
| 1250 | + return '<span class="adminify-new-badge">' . esc_html($badge_text) . '</span>'; | |
| 1251 | + } | |
| 1252 | + | |
| 1212 | 1253 | /* |
| 1213 | 1254 | * Compares the version of WordPress running to the $version specified. |
| 1214 | 1255 | * Usage: Utils::check_wp_version('>=', '4.0') |
| 1215 | 1256 | version_compare( $wp_version, '4.3', '>=' ) |
| @@ -1221,8 +1262,29 @@ | ||
| 1221 | 1262 | global $wp_version; |
| 1222 | 1263 | return version_compare( $wp_version, $version, $operator ); |
| 1223 | 1264 | } |
| 1224 | 1265 | |
| 1266 | + /** | |
| 1267 | + * Shared guard for features that only work below WordPress 7.1. | |
| 1268 | + * | |
| 1269 | + * 7.1 rewrote the block editor chrome and absorbed features Adminify used to | |
| 1270 | + * provide, so several settings became either inert or actively harmful there. | |
| 1271 | + * Rather than repeat the version literal at each of them, they all gate on this | |
| 1272 | + * one call, so the boundary moves in a single place. | |
| 1273 | + * | |
| 1274 | + * Features behind this guard: | |
| 1275 | + * - `gutenberg_editor_logo`: painted over `.edit-post-fullscreen-mode-close`, | |
| 1276 | + * a class the rebuilt icon-only close button no longer carries. | |
| 1277 | + * - `media_attachments.media_ininite_scroll`: the Media Library grid scrolls | |
| 1278 | + * infinitely by default from 7.1, with a per-user opt-out. Forcing the | |
| 1279 | + * `media_library_infinite_scrolling` filter overrides that preference. | |
| 1280 | + * | |
| 1281 | + * @return bool True on WordPress older than 7.1. | |
| 1282 | + */ | |
| 1283 | + public static function is_wp_below_7_1() { | |
| 1284 | + return self::check_wp_version( '<', '7.1' ); | |
| 1285 | + } | |
| 1286 | + | |
| 1225 | 1287 | public static function help_urls($module_name = '', $docs = '', $youtube = '', $facebook_grp = '', $support = '') |
| 1226 | 1288 | { |
| 1227 | 1289 | $help_content = ''; |
| 1228 | 1290 | |
| @@ -1266,31 +1328,84 @@ | ||
| 1266 | 1328 | * @return bool |
| 1267 | 1329 | */ |
| 1268 | 1330 | public static function is_iframe() |
| 1269 | 1331 | { |
| 1270 | - // Adminify-tagged iframe URL: Templates.php injects a JS interceptor that | |
| 1271 | - // rewrites every iframe src to include ?adminify-iframe=1. Authoritative | |
| 1272 | - // when present and works regardless of browser headers (covers WordPress | |
| 1273 | - // Playground where Sec-Fetch-Dest + Referer are stripped by the SW). The | |
| 1274 | - // param is read-only intent; no sanitization concern beyond isset. | |
| 1275 | - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only flag. | |
| 1276 | - if ( isset($_GET['adminify-iframe']) ) { | |
| 1277 | - return true; | |
| 1332 | + return isset($_SERVER["HTTP_SEC_FETCH_DEST"]) && strtolower(sanitize_text_field(wp_unslash($_SERVER["HTTP_SEC_FETCH_DEST"]))) === "iframe"; | |
| 1333 | + // $isIframe = isset($_SERVER["HTTP_SEC_FETCH_DEST"]) && strtolower($_SERVER["HTTP_SEC_FETCH_DEST"]) === "iframe"; | |
| 1334 | + // if ( $isIframe ) return true; | |
| 1335 | + // if ( isset($_GET['adminify-iframe']) ) return true; | |
| 1336 | + // return false; | |
| 1337 | + } | |
| 1338 | + | |
| 1339 | + /** | |
| 1340 | + * Whether the request carries Fetch Metadata headers at all. | |
| 1341 | + * | |
| 1342 | + * `Sec-Fetch-*` is attached by the browser below the Service Worker layer, so | |
| 1343 | + * installs served through a Service Worker - WordPress Playground, offline | |
| 1344 | + * setups - never pass it to PHP. Older browsers do not send it either. When it | |
| 1345 | + * is missing, is_iframe() cannot tell an iframe request from a top-level one | |
| 1346 | + * and the client has to settle it instead. | |
| 1347 | + * | |
| 1348 | + * @return bool | |
| 1349 | + */ | |
| 1350 | + public static function has_fetch_metadata() | |
| 1351 | + { | |
| 1352 | + return isset( $_SERVER['HTTP_SEC_FETCH_DEST'] ) | |
| 1353 | + || isset( $_SERVER['HTTP_SEC_FETCH_MODE'] ) | |
| 1354 | + || isset( $_SERVER['HTTP_SEC_FETCH_SITE'] ); | |
| 1355 | + } | |
| 1356 | + | |
| 1357 | + /** | |
| 1358 | + * Whether this is a plain GET request. | |
| 1359 | + * | |
| 1360 | + * @return bool | |
| 1361 | + */ | |
| 1362 | + public static function is_get_request() | |
| 1363 | + { | |
| 1364 | + $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; | |
| 1365 | + | |
| 1366 | + return 'GET' === $method || 'HEAD' === $method; | |
| 1367 | + } | |
| 1368 | + | |
| 1369 | + /** | |
| 1370 | + * Whether the current request renders a full admin page. | |
| 1371 | + * | |
| 1372 | + * Endpoints like async-upload.php run through wp-admin/admin.php (so `admin_init` | |
| 1373 | + * fires) but answer with a raw payload - an attachment ID, JSON, an HTML fragment. | |
| 1374 | + * Printing the Admin UI frame into those responses corrupts them: the media uploader | |
| 1375 | + * on media-new.php reads the response as an attachment ID and gives up when it is not | |
| 1376 | + * numeric. `wp_doing_ajax()` does not catch these: async-upload.php only defines | |
| 1377 | + * DOING_AJAX for the `upload-attachment` action, which media-new.php does not send. | |
| 1378 | + * | |
| 1379 | + * @return bool | |
| 1380 | + */ | |
| 1381 | + public static function is_admin_page_request() | |
| 1382 | + { | |
| 1383 | + if ( wp_doing_ajax() || wp_doing_cron() || wp_is_json_request() ) { | |
| 1384 | + return false; | |
| 1278 | 1385 | } |
| 1279 | - // Fetch Metadata Request Headers (modern browsers): authoritative. | |
| 1280 | - if ( isset($_SERVER["HTTP_SEC_FETCH_DEST"]) ) { | |
| 1281 | - return strtolower(sanitize_text_field(wp_unslash($_SERVER["HTTP_SEC_FETCH_DEST"]))) === "iframe"; | |
| 1386 | + | |
| 1387 | + if ( ( defined('REST_REQUEST') && REST_REQUEST ) || ( defined('XMLRPC_REQUEST') && XMLRPC_REQUEST ) || ( defined('WP_CLI') && WP_CLI ) ) { | |
| 1388 | + return false; | |
| 1282 | 1389 | } |
| 1283 | - // Header absent (older browsers): fall back to a same-host /wp-admin Referer. | |
| 1284 | - if ( ! empty($_SERVER['HTTP_REFERER']) ) { | |
| 1285 | - $referer = wp_unslash($_SERVER['HTTP_REFERER']); | |
| 1286 | - $referer_host = wp_parse_url($referer, PHP_URL_HOST); | |
| 1287 | - $self_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : ''; | |
| 1288 | - if ( $referer_host && $self_host && strtolower($referer_host) === strtolower($self_host) && strpos($referer, '/wp-admin') !== false ) { | |
| 1289 | - return true; | |
| 1290 | - } | |
| 1390 | + | |
| 1391 | + $pagenow = isset($GLOBALS['pagenow']) ? $GLOBALS['pagenow'] : ''; | |
| 1392 | + | |
| 1393 | + if ( empty($pagenow) && isset($_SERVER['PHP_SELF']) ) { | |
| 1394 | + $pagenow = basename( sanitize_text_field( wp_unslash($_SERVER['PHP_SELF']) ) ); | |
| 1291 | 1395 | } |
| 1292 | - return false; | |
| 1396 | + | |
| 1397 | + $raw_response_endpoints = [ | |
| 1398 | + 'admin-ajax.php', | |
| 1399 | + 'admin-post.php', | |
| 1400 | + 'async-upload.php', | |
| 1401 | + 'load-scripts.php', | |
| 1402 | + 'load-styles.php', | |
| 1403 | + 'ms-files.php', | |
| 1404 | + 'wp-cron.php', | |
| 1405 | + ]; | |
| 1406 | + | |
| 1407 | + return ! in_array( $pagenow, $raw_response_endpoints, true ); | |
| 1293 | 1408 | } |
| 1294 | 1409 | |
| 1295 | 1410 | /** |
| 1296 | 1411 | * Load a template file. |