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() )
. '
';
foreach ( EWPM_Multisite::caveats() as $line ) {
echo '- ' . esc_html( $line ) . '
';
}
$dismiss = wp_nonce_url(
add_query_arg( 'ewpm_ms_notice', 'dismiss', network_admin_url( 'admin.php?page=ewpm-dashboard' ) ),
'ewpm_ms_notice_dismiss'
);
echo '
' . 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 '';
}
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