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; } wp_enqueue_style( 'aisiteark-admin', AISITEARK_URL . 'admin/assets/css/admin.css', array(), AISITEARK_VERSION ); wp_enqueue_script( 'aisiteark-admin', AISITEARK_URL . 'admin/assets/js/siteark.js', array(), AISITEARK_VERSION, 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' ), ), ) ); } /** * 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: * title + version chip on the left, Free/Pro plan badge (and any page * actions) on the right — matching the AtoZ family look. Called from the * view files as $this->page_header( … ) so the header can never drift * between pages. * * @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 = '' ) { echo '
'; echo '

' . esc_html( $title ); if ( defined( 'AISITEARK_VERSION' ) ) { echo ' v' . esc_html( AISITEARK_VERSION ) . ''; } echo '

'; if ( '' !== $subtitle ) { echo '

' . esc_html( $subtitle ) . '

'; } echo '
'; echo '
'; echo wp_kses_post( $this->plan_badge() ); if ( '' !== $actions ) { echo wp_kses_post( $actions ); } 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' ) . ''; } }