id || ! current_user_can( Capabilities::ADMIN ) ) { return; } echo '

' . esc_html__( 'AI-SiteArk: checked for updates.', 'ai-siteark' ) . '

'; } /** * Add "Settings" + "Check for updates" links to the plugin's row on the * Plugins screen (matching the other AtoZ plugins). * * @param array $links Existing action links. * @return array */ public function plugin_action_links( $links ) { $mine = array( '' . esc_html__( 'Settings', 'ai-siteark' ) . '', ); if ( ! ( defined( 'AISITEARK_DOTORG' ) && AISITEARK_DOTORG ) ) { $mine[] = '' . esc_html__( 'Check for updates', 'ai-siteark' ) . ''; } return array_merge( $mine, $links ); } /** * Handle the Plugins-screen "Check for updates" link: force a fresh update + * license check, then return to the Plugins page. * * @return void */ public function check_updates() { if ( ! current_user_can( Capabilities::ADMIN ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'ai-siteark' ) ); } check_admin_referer( 'aisiteark_check_updates' ); ( new \AtoZ\SiteArk\Updater\License_Client() )->check_now(); wp_safe_redirect( add_query_arg( array( 'aisiteark_checked' => '1' ), self_admin_url( 'plugins.php' ) ) ); exit; } /** * Register menu + submenus. * * @return void */ public function menu() { add_menu_page( __( 'AI-SiteArk', 'ai-siteark' ), __( 'AI-SiteArk', 'ai-siteark' ), Capabilities::MANAGE, self::SLUG, array( $this, 'render_backup' ), 'dashicons-vault', // All AI-* plugins share the 58.x block so they group together in the // sidebar instead of scattering between unrelated menus. 58.5 ); add_submenu_page( self::SLUG, __( 'Backup', 'ai-siteark' ), __( 'Backup', 'ai-siteark' ), Capabilities::MANAGE, self::SLUG, array( $this, 'render_backup' ) ); add_submenu_page( self::SLUG, __( 'Restore & Migrate', 'ai-siteark' ), __( 'Restore & Migrate', 'ai-siteark' ), Capabilities::ADMIN, self::SLUG_RESTORE, array( $this, 'render_restore' ) ); add_submenu_page( self::SLUG, __( 'Activity & tasks', 'ai-siteark' ), __( 'Activity & tasks', 'ai-siteark' ), Capabilities::MANAGE, self::SLUG_ACTIVITY, array( $this, 'render_activity' ) ); add_submenu_page( self::SLUG, __( 'Settings', 'ai-siteark' ), __( 'Settings', 'ai-siteark' ), Capabilities::ADMIN, self::SLUG_SETTINGS, array( $this, 'render_settings' ) ); add_submenu_page( self::SLUG, __( 'Help & guide', 'ai-siteark' ), __( 'Help & guide', 'ai-siteark' ), Capabilities::MANAGE, 'ai-siteark-help', array( $this, 'render_help' ) ); } /** * Enqueue admin assets on AI-SiteArk screens only. * * @param string $hook Current admin page hook suffix. * @return void */ public function assets( $hook ) { if ( false === strpos( (string) $hook, self::SLUG ) ) { return; } $css_ver = file_exists( AISITEARK_DIR . 'admin/assets/css/admin.css' ) ? (string) filemtime( AISITEARK_DIR . 'admin/assets/css/admin.css' ) : AISITEARK_VERSION; wp_enqueue_style( 'aisiteark-admin', AISITEARK_URL . 'admin/assets/css/admin.css', array( 'dashicons' ), $css_ver ); $js_ver = file_exists( AISITEARK_DIR . 'admin/assets/js/siteark.js' ) ? (string) filemtime( AISITEARK_DIR . 'admin/assets/js/siteark.js' ) : AISITEARK_VERSION; wp_enqueue_script( 'aisiteark-admin', AISITEARK_URL . 'admin/assets/js/siteark.js', array( 'jquery' ), $js_ver, true ); wp_localize_script( 'aisiteark-admin', 'aisiteArkAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aisiteark_ajax' ), // The restore-upload chunk size, sized to THIS host's PHP limits. A fixed // 5 MB chunk fails on the many hosts where upload_max_filesize defaults to // 2 MB — PHP silently drops the oversized part, so the upload never starts. 'chunkSize' => self::upload_chunk_size(), 'i18n' => array( 'working' => __( 'Working…', 'ai-siteark' ), 'failed' => __( 'Something went wrong. See the log below.', 'ai-siteark' ), 'completed' => __( 'Done.', 'ai-siteark' ), 'restoreAnyway' => __( 'Restore anyway? Only continue if you understand the risk of an incompatible or incomplete backup.', 'ai-siteark' ), 'overriding' => __( 'Overriding pre-restore checks…', 'ai-siteark' ), /* translators: %s is replaced with the numeric HTTP status. */ 'serverError' => __( 'The operation could not complete (server error {status}). It may have been interrupted or timed out.', 'ai-siteark' ), 'interrupted' => __( 'The operation was interrupted — the server did not respond. It may have restarted or timed out.', 'ai-siteark' ), 'recover' => __( 'Reload this page. If a restore still shows as in progress, open “Activity & tasks” and cancel it, then try again.', 'ai-siteark' ), 'preBackup' => __( 'Backing up the current site before restoring…', 'ai-siteark' ), 'preBackupDone' => __( 'Safety backup complete. Starting restore…', 'ai-siteark' ), 'preBackupFailed' => __( 'The safety backup failed, so the restore was cancelled — your current site is untouched.', 'ai-siteark' ), 'finishing' => __( 'The restore was interrupted here, but may still be finishing on the server — checking…', 'ai-siteark' ), 'finishingBg' => __( 'Finishing on the server…', 'ai-siteark' ), 'confirmProceed' => __( 'Yes, continue', 'ai-siteark' ), 'cancel' => __( 'Cancel', 'ai-siteark' ), 'restoreAnywayYes' => __( 'Restore anyway', 'ai-siteark' ), 'deleteBackupTitle' => __( 'Delete backup?', 'ai-siteark' ), 'confirmDelete' => __( 'Delete this backup file? This action cannot be undone.', 'ai-siteark' ), 'delete' => __( 'Delete', 'ai-siteark' ), ), ) ); } /** * The largest restore-upload chunk this host will actually accept. * * A chunk is a file part in a multipart POST, so it must fit under BOTH * `upload_max_filesize` (the file part) and `post_max_size` (the whole request, * which also carries the other fields + multipart boundaries). We take a fraction of * the smaller of the two, cap it at 5 MB (plenty, and kind to slow links) and floor it * at 256 KB so uploads still work on very tight hosts. * * @return int Bytes. */ public static function upload_chunk_size() { $upload = self::ini_bytes( ini_get( 'upload_max_filesize' ) ); $post = self::ini_bytes( ini_get( 'post_max_size' ) ); $limits = array_filter( array( $upload, $post ) ); // post_max_size=0 means "no limit". $budget = empty( $limits ) ? 5 * 1024 * 1024 : min( $limits ); $chunk = (int) floor( $budget * 0.75 ); // headroom for fields + boundaries. $chunk = min( $chunk, 5 * 1024 * 1024 ); $chunk = max( $chunk, 262144 ); return $chunk; } /** * Parse a PHP ini size string ("2M", "8M", "512K", "1G") into bytes. 0/'' → 0. * * @param string $val Ini value. * @return int */ private static function ini_bytes( $val ) { $val = trim( (string) $val ); if ( '' === $val ) { return 0; } $unit = strtolower( substr( $val, -1 ) ); $num = (int) $val; switch ( $unit ) { case 'g': return $num * 1024 * 1024 * 1024; case 'm': return $num * 1024 * 1024; case 'k': return $num * 1024; default: return (int) $val; } } /** * Backup page. * * @return void */ public function render_backup() { if ( ! current_user_can( Capabilities::MANAGE ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-siteark' ) ); } require AISITEARK_DIR . 'admin/views/backup.php'; } /** * Activity & running-tasks page. * * @return void */ public function render_activity() { if ( ! current_user_can( Capabilities::MANAGE ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-siteark' ) ); } require AISITEARK_DIR . 'admin/views/activity.php'; } /** * Restore & migrate page. * * @return void */ public function render_restore() { if ( ! current_user_can( Capabilities::ADMIN ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-siteark' ) ); } require AISITEARK_DIR . 'admin/views/restore.php'; } /** * Settings page. * * @return void */ public function render_settings() { if ( ! current_user_can( Capabilities::ADMIN ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-siteark' ) ); } require AISITEARK_DIR . 'admin/views/settings.php'; } /** * Help & guide page. * * @return void */ public function render_help() { if ( ! current_user_can( Capabilities::MANAGE ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'ai-siteark' ) ); } require AISITEARK_DIR . 'admin/views/help.php'; } /** * Render the shared page header used on every AI-SiteArk admin screen: * Enterprise gradient banner (shield icon, title, Pro badge, version, quick action buttons) * followed by the unified pill navigation tabs (.aisv-tabs) matching the modern SaaS design. * * @param string $title Page title (plain text). * @param string $subtitle Optional subtitle (plain text). * @param string $actions Optional right-aligned actions HTML (pre-escaped). * @return void */ public function page_header( $title, $subtitle = '', $actions = '' ) { $is_pro = Plan::is_pro(); $plan_label = $is_pro ? 'PRO' : Plan::label(); $version_str = defined( 'AISITEARK_VERSION' ) ? 'v' . AISITEARK_VERSION : ''; // Detect active page for the navigation tabs. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : self::SLUG; $tabs = array( self::SLUG => array( 'label' => __( 'Dashboard & Backup', 'ai-siteark' ), 'icon' => 'dashicons-database-export', ), self::SLUG_RESTORE => array( 'label' => __( 'Restore & Migrate', 'ai-siteark' ), 'icon' => 'dashicons-backup', ), self::SLUG_ACTIVITY => array( 'label' => __( 'Activity & tasks', 'ai-siteark' ), 'icon' => 'dashicons-list-view', ), self::SLUG_SETTINGS => array( 'label' => __( 'Settings', 'ai-siteark' ), 'icon' => 'dashicons-admin-generic', ), 'ai-siteark-help' => array( 'label' => __( 'Help & guide', 'ai-siteark' ), 'icon' => 'dashicons-editor-help', ), ); // Default header action buttons if none passed. if ( '' === $actions ) { $actions = '' . ' ' . esc_html__( 'Backup Now', 'ai-siteark' ) . '' . '' . ' ' . esc_html__( 'Settings', 'ai-siteark' ) . ''; } echo '
'; echo '
'; echo ''; echo '
'; echo '

'; echo esc_html__( 'AI-SiteArk', 'ai-siteark' ); if ( ! ( defined( 'AISITEARK_DOTORG' ) && AISITEARK_DOTORG ) ) { echo ' ' . esc_html( $plan_label ) . ''; } echo '

'; echo '

'; if ( $version_str ) { echo esc_html( $version_str ) . ' • '; } echo esc_html( $subtitle ? $subtitle : __( 'Enterprise Backup, Restore & Site Migration Engine', 'ai-siteark' ) ); echo '

'; echo '
'; echo '
'; echo '
'; echo wp_kses_post( $actions ); echo '
'; echo '
'; // Render the navigation tabs bar. echo ''; } /** * Render a "locked" card shown in place of a Pro-only settings section on the * Free plan — a clear description + Upgrade button, and NO working controls, so * the feature is inaccessible in the UI (the server also enforces the gate). * * @param string $icon Dashicon class for the heading. * @param string $title Section title. * @param string $desc What the Pro feature does. * @param string $checkout Upgrade/checkout URL. * @return void */ public function pro_lock_card( $icon, $title, $desc, $checkout ) { // The WordPress.org (free) build ships only the free features and never advertises Pro, // so a locked/upgrade card must not appear there at all. if ( defined( 'AISITEARK_DOTORG' ) && AISITEARK_DOTORG ) { return; } echo '
'; echo '

' . esc_html( $title ) . ' ' . esc_html__( 'Pro', 'ai-siteark' ) . '

'; echo '

' . esc_html( $desc ) . '

'; echo '

' . esc_html__( 'Upgrade to Pro', 'ai-siteark' ) . '

'; echo '
'; } /** * Free/Pro plan badge (with an Upgrade link on Free), returned as pre-escaped * HTML so it can be dropped into the page header. * * @return string */ private function plan_badge() { // No Free/Pro badge or upgrade link in the WordPress.org build — it is simply "the plugin". if ( defined( 'AISITEARK_DOTORG' ) && AISITEARK_DOTORG ) { return ''; } if ( Plan::is_pro() ) { return '' . ' ' . esc_html( Plan::label() ) . ''; } return '' . esc_html__( 'Free', 'ai-siteark' ) . '' . ' ' . esc_html__( 'Upgrade to Pro', 'ai-siteark' ) . ''; } }