array( __( 'Dashboard', 'migratenest' ), 'page_dashboard', 10 ), 'ewpm-export' => array( __( 'Migrate out', 'migratenest' ), 'page_export', 20 ), 'ewpm-import' => array( __( 'Migrate in', 'migratenest' ), 'page_import', 30 ), 'ewpm-sync' => array( __( 'Sync', 'migratenest' ), 'page_sync_preview', 40, 'sync' ), 'ewpm-backups' => array( __( 'Backups', 'migratenest' ), 'page_backups', 50 ), 'ewpm-settings' => array( __( 'Settings', 'migratenest' ), 'page_settings', 70 ), ); foreach ( $pages as $slug => $def ) { EWPM_Modules::register_page( $slug, array( 'title' => $def[0], 'callback' => array( __CLASS__, $def[1] ), 'position' => $def[2], 'feature' => isset( $def[3] ) ? $def[3] : '', ) ); } } /** * Multisite only: forward /wp-admin/admin.php?page=ewpm-* to the same screen * under /wp-admin/network/. Runs on admin_init, which fires before WordPress * resolves the (non-existent) page hook and dies. Users without the network * capability are left alone — they get the standard refusal, which is right. */ public static function redirect_to_network() { if ( is_network_admin() || wp_doing_ajax() ) { return; } if ( empty( $_GET['page'] ) || 0 !== strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'ewpm-' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading which admin screen was requested so it can be redirected. No state changes, and ordinary admin navigation carries no nonce. return; } $query = isset( $_SERVER['QUERY_STRING'] ) ? (string) wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Appended to a wp_safe_redirect() target, which runs wp_sanitize_redirect() and strips control characters; the host is validated separately. if ( ! EWPM_Multisite::user_can() ) { // Returning here hands the request back to wp-admin/admin.php, which // finds nothing registered for this slug and answers "Sorry, you are // not allowed to access this page." — true, but it names neither the // plugin nor the reason, and an OAuth round-trip lands here after // the connection has already succeeded. Say what actually happened. wp_die( sprintf( '

%s

%s

%s

', esc_html__( 'Network Administrator required', 'migratenest' ), esc_html__( 'MigrateNest runs across the whole network, so its screens live in Network Admin and only a Network Administrator can open them.', 'migratenest' ), esc_html__( 'You are signed in without that capability. Sign in as a Network Administrator, then open the plugin from Network Admin.', 'migratenest' ) ), esc_html__( 'Network Administrator required', 'migratenest' ), array( 'response' => 403, 'link_url' => esc_url( network_admin_url( 'admin.php?page=ewpm-dashboard' ) ), 'link_text' => esc_html__( 'Go to Network Admin', 'migratenest' ) ) ); } wp_safe_redirect( network_admin_url( 'admin.php' . ( '' !== $query ? '?' . $query : '' ) ) ); exit; } /** * Send a capable user from an unregistered paid screen to public pricing. * * Installed modules keep their local screen even before licence activation; * only a genuinely absent page is redirected. */ public static function redirect_missing_commercial_page() { if ( wp_doing_ajax() || empty( $_GET['page'] ) || ! EWPM_Multisite::user_can() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading which admin screen was requested so it can be redirected. No state changes, and ordinary admin navigation carries no nonce. return; } $page = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading which admin screen was requested so it can be redirected. No state changes, and ordinary admin navigation carries no nonce. if ( 'ewpm-license' !== $page || self::has_page( $page ) ) { return; } $url = esc_url_raw( EWPM_Features::pricing_url() ); if ( '' === $url ) { return; } wp_redirect( $url, 302, 'MigrateNest' ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit; } /** * Tell a Network Administrator, once per network, exactly what this build * covers on a network and what it does not. Better they read it here than * discover the gap after a migration. */ public static function multisite_notice() { if ( ! is_multisite() || ! EWPM_Multisite::user_can() ) { return; } $screen = get_current_screen(); if ( ! $screen || false === strpos( $screen->id, 'ewpm-' ) ) { return; } if ( get_site_option( 'ewpm_multisite_notice_dismissed' ) ) { return; } echo '

' . esc_html( EWPM_Multisite::describe() ) . '

' . esc_html__( 'Got it, hide this', 'migratenest' ) . '

'; } /** * After a restore the browser session is reset, so the in-modal * verification report is easily missed. Surface a dismissible notice * linking to the full report whenever a recent report has warnings or * failures. Pro-only (the report is only generated for Pro). */ public static function verify_notice() { if ( ! EWPM_Multisite::user_can() ) { return; } // This plugin's screens and the main Dashboard only — not every admin // page. Guideline 11 asks that notices stay limited in scope, and being // dismissible is not on its own a licence to appear everywhere. Same // test the health notice already applies, so the two // notices behave alike. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $sid = $screen ? $screen->id : ''; if ( 'dashboard' !== $sid && false === strpos( $sid, 'ewpm-' ) && false === strpos( $sid, 'page_ewpm' ) ) { return; } $report = EWPM_Features::verify_last_report(); if ( ! is_array( $report ) || empty( $report['checks'] ) ) { return; } // Only nag for < 48h-old reports that actually have something to see. $age = isset( $report['created'] ) ? ( time() - (int) strtotime( $report['created'] . ' UTC' ) ) : 0; if ( $age > 2 * DAY_IN_SECONDS ) { return; } if ( empty( $report['overall'] ) || 'pass' === $report['overall'] ) { return; } $class = 'fail' === $report['overall'] ? 'notice-error' : 'notice-warning'; $link = EWPM_Multisite::admin_url( 'admin.php?page=ewpm-import#ewpm-verify-report' ); echo '

' . '' . esc_html__( 'MigrateNest:', 'migratenest' ) . ' ' . esc_html__( 'Your last migration finished with items worth reviewing.', 'migratenest' ) . ' ' . esc_html__( 'View the verification report', 'migratenest' ) . '' . '

'; } public static function action_links( $links ) { $extra = array( '' . esc_html__( 'Settings', 'migratenest' ) . '', ); if ( ! EWPM_Features::is_pro() ) { $extra[] = '' . esc_html__( 'Upgrade to Pro', 'migratenest' ) . ''; } return array_merge( $extra, $links ); } /** * Whether an installed module provides the local License screen. * * A valid licence is deliberately not the test here: an installed but not * yet activated Pro add-on still needs its screen so the key can be entered. */ public static function has_page( $slug ) { $pages = EWPM_Modules::pages(); return isset( $pages[ sanitize_key( $slug ) ] ); } public static function has_license_page() { return self::has_page( 'ewpm-license' ); } /** Local module page when installed, otherwise the public pricing page. */ public static function page_url( $slug ) { $slug = sanitize_key( $slug ); return self::has_page( $slug ) ? EWPM_Multisite::admin_url( 'admin.php?page=' . $slug ) : EWPM_Features::pricing_url(); } /** Where the plan control should send the current installation. */ public static function plan_url() { return self::page_url( 'ewpm-license' ); } /** * Settings sections core ships. Remote Access, Scheduled Backups and * Privacy belong to the paid module and are registered by it; positions * are spaced so the combined screen keeps its established order. */ public static function register_core_settings_sections() { $sections = array( 'language' => array( __( 'Language', 'migratenest' ), 'language.php', 30 ), 'diagnostics' => array( __( 'Diagnostics Log', 'migratenest' ), 'diagnostics.php', 50 ), 'environment' => array( __( 'Environment', 'migratenest' ), 'environment.php', 60 ), ); foreach ( $sections as $id => $def ) { EWPM_Modules::register_settings_section( $id, array( 'title' => $def[0], 'render' => EWPM_PATH . 'admin/views/settings/' . $def[1], 'position' => $def[2], ) ); } } public static function menu() { // 'manage_network_options' on a network, 'manage_options' otherwise — // see EWPM_Multisite::capability() for why a network needs the stricter // one. Registering the pages with it also means a site administrator // never sees a menu entry they would only be refused at. $cap = EWPM_Multisite::capability(); add_menu_page( __( 'MigrateNest', 'migratenest' ), __( 'MigrateNest', 'migratenest' ), $cap, 'ewpm-dashboard', array( __CLASS__, 'page_dashboard' ), // White artwork: the wp-admin sidebar is near-black, and WordPress does // not recolour an icon loaded from a file URL — it only greyscales it. EWPM_URL . 'assets/branding/migratenest-admin-menu.png', 76 ); foreach ( EWPM_Modules::pages() as $slug => $def ) { $menu_title = $def['menu_title']; if ( ! empty( $def['feature'] ) && ! EWPM_Features::can( $def['feature'] ) ) { $required_tier = EWPM_Features::feature_tier( $def['feature'] ); $badge_label = EWPM_Features::is_pro() && $required_tier ? EWPM_Features::tier_label( $required_tier ) : __( 'Pro', 'migratenest' ); $menu_title .= ' '; $menu_title .= ' ' . esc_html( sprintf( /* translators: %s: paid plan name, for example Pro or Freelancer. */ __( '%s plan required', 'migratenest' ), $badge_label ) ) . ''; } add_submenu_page( 'ewpm-dashboard', $def['title'], $menu_title, $cap, $slug, $def['callback'] ); } } /** * Styles for this plugin's admin MENU entry, which is present on every * admin screen — not just ours — so they cannot ride on the product * stylesheet enqueued by assets() below. * * Registered against a src-less handle and attached with * wp_add_inline_style() rather than echoed into admin_head: WordPress then * owns the