codecanyon = null === $codecanyon ? Distribution::is_codecanyon() : $codecanyon; } /** * Register admin-post handlers. */ public function register(): void { add_action( 'admin_post_' . self::CONNECT_ACTION, array( $this, 'connect' ) ); add_action( 'admin_post_' . self::DISCONNECT_ACTION, array( $this, 'disconnect' ) ); add_action( 'admin_post_' . self::INVENTORY_ACTION, array( $this, 'allow_inventory' ) ); if ( $this->codecanyon ) { add_action( 'admin_post_' . self::REDEEM_ACTION, array( $this, 'redeem' ) ); add_action( 'admin_post_' . self::ACTIVATE_ACTION, array( $this, 'activate' ) ); } } /** * Handle "Connect". */ public function connect(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'keepwatch' ), 403 ); } check_admin_referer( self::CONNECT_ACTION ); $code = isset( $_POST['keepwatch_code'] ) ? sanitize_text_field( wp_unslash( $_POST['keepwatch_code'] ) ) : ''; $consent = ! empty( $_POST['keepwatch_consent'] ); $result = ( new Connection( Connection::client() ) )->connect( $code, $consent ); $this->notice( $result['ok'] ? 'success' : 'error', $result['ok'] ? __( 'This site is now connected to WPXLab.', 'keepwatch' ) : $result['message'] ); $this->back(); } /** * Handle "Disconnect". */ public function disconnect(): void { $this->authorize( self::DISCONNECT_ACTION ); ( new Connection( Connection::client() ) )->disconnect(); $this->notice( 'success', __( 'Disconnected. Keepwatch no longer sends anything to WPXLab.', 'keepwatch' ) ); $this->back(); } /** * Handle "Allow" for the plugin/theme list and site metrics (sites connected before consent version 2). */ public function allow_inventory(): void { $this->authorize( self::INVENTORY_ACTION ); Connection::grant_inventory_consent(); $this->notice( 'success', __( 'Thanks. The next report includes your installed plugins, themes and site metrics.', 'keepwatch' ) ); $this->back(); } /** * Handle "Add purchase code" (CodeCanyon build). The code is sent only while connected (consent given). */ public function redeem(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'keepwatch' ), 403 ); } check_admin_referer( self::REDEEM_ACTION ); $code = isset( $_POST['keepwatch_purchase_code'] ) ? sanitize_text_field( wp_unslash( $_POST['keepwatch_purchase_code'] ) ) : ''; $result = ( new PurchaseCode( Connection::client() ) )->redeem( $code ); $this->notice( $result['ok'] ? 'success' : 'error', $result['ok'] ? __( 'Your CodeCanyon purchase is now linked to your WPXLab account.', 'keepwatch' ) : $result['message'] ); $this->back(); } /** * Handle "Activate" (CodeCanyon build): purchase code + e-mail, no WPXLab account needed. */ public function activate(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'keepwatch' ), 403 ); } check_admin_referer( self::ACTIVATE_ACTION ); $code = isset( $_POST['keepwatch_purchase_code'] ) ? sanitize_text_field( wp_unslash( $_POST['keepwatch_purchase_code'] ) ) : ''; $email = isset( $_POST['keepwatch_email'] ) ? sanitize_email( wp_unslash( $_POST['keepwatch_email'] ) ) : ''; $consent = ! empty( $_POST['keepwatch_consent'] ); $result = ( new PurchaseCode( Connection::client() ) )->activate( $code, $email, $consent ); $this->notice( $result['ok'] ? 'success' : 'error', $result['ok'] ? __( 'Keepwatch is activated and this site is now connected to WPXLab.', 'keepwatch' ) : $result['message'] ); $this->back(); } /** * Render the "WPXLab cloud" tab. */ public function render(): void { $state = Connection::state(); $notice = get_transient( self::NOTICE . '_' . get_current_user_id() ); delete_transient( self::NOTICE . '_' . get_current_user_id() ); if ( is_array( $notice ) && isset( $notice['type'], $notice['message'] ) ) { printf( '

%2$s

', 'success' === $notice['type'] ? 'success' : 'error', esc_html( (string) $notice['message'] ) ); } if ( $state['connected'] ) { echo '
'; echo '

' . esc_html__( 'Connected to WPXLab', 'keepwatch' ) . '

'; printf( '

%s

', esc_html( sprintf( /* translators: 1: site name, 2: date */ __( 'Connected as %1$s since %2$s.', 'keepwatch' ), '' !== $state['name'] ? $state['name'] : (string) wp_parse_url( home_url(), PHP_URL_HOST ), wp_date( get_option( 'date_format' ), $state['connected_at'] ) ) ) ); echo '
'; printf( '%2$s', esc_url( 'https://wpxlab.com/dashboard/keepwatch/sites' ), esc_html__( 'Open WPXLab dashboard', 'keepwatch' ) ); $this->form( self::DISCONNECT_ACTION, '', __( 'Disconnect', 'keepwatch' ) ); echo '
'; if ( ! Connection::inventory_allowed() ) { // Sites connected before this data type existed: nothing extra is sent until an admin agrees here. echo '

' . esc_html__( 'New: component list and site metrics', 'keepwatch' ) . '

'; echo '

' . esc_html( self::inventory_item() ) . '

'; $this->form( self::INVENTORY_ACTION, '', __( 'Allow sending', 'keepwatch' ) ); echo '
'; } if ( $this->codecanyon ) { $this->render_account_suggestion(); $this->render_purchase_code(); } return; } echo '
'; if ( $this->codecanyon ) { $this->render_activation(); // Buyers who already have a WPXLab account connect with a connection code instead. echo '
' . esc_html__( 'Already have a WPXLab account? Connect with a connection code', 'keepwatch' ) . ''; $this->render_steps(); } else { $this->render_pitch(); } printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( self::CONNECT_ACTION ) ); wp_nonce_field( self::CONNECT_ACTION ); echo '

'; echo '

'; // Consent screen (CLAUDE.md rule 7): what, where, how often, plus terms and privacy links. echo '', esc_html__( 'I agree to send this data to WPXLab.', 'keepwatch' ), esc_url( 'https://wpxlab.com/terms' ), esc_html__( 'Terms of service', 'keepwatch' ), esc_url( 'https://wpxlab.com/privacy' ), esc_html__( 'Privacy policy', 'keepwatch' ) ); submit_button( __( 'Connect to WPXLab', 'keepwatch' ), $this->codecanyon ? 'secondary' : 'primary large', 'submit', false ); echo '
'; if ( $this->codecanyon ) { echo '
'; } echo '
'; } /** * CodeCanyon activation: purchase code + e-mail + consent (rule 7). Local features never depend on it (rule 11). */ private function render_activation(): void { printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( self::ACTIVATE_ACTION ) ); wp_nonce_field( self::ACTIVATE_ACTION ); echo '

' . esc_html__( 'Activate with your purchase code', 'keepwatch' ) . '

'; echo '

' . esc_html__( 'All Keepwatch features on this site work without activation. Activating connects this site to WPXLab for outside uptime checks, history and e-mail alerts. No WPXLab account is needed: one is created for your e-mail if you do not have one yet.', 'keepwatch' ) . '

'; echo '

'; echo '

'; echo '

' . esc_html__( 'Find it under CodeCanyon → Downloads → Keepwatch → License certificate & purchase code.', 'keepwatch' ) . '

'; echo '

'; echo '

'; echo '

' . esc_html__( 'Alerts and your WPXLab sign-in go to this address. Use the e-mail of your Envato account if you can.', 'keepwatch' ) . '

'; echo '', esc_html__( 'I agree to send this data to WPXLab.', 'keepwatch' ), esc_url( 'https://wpxlab.com/terms' ), esc_html__( 'Terms of service', 'keepwatch' ), esc_url( 'https://wpxlab.com/privacy' ), esc_html__( 'Privacy policy', 'keepwatch' ) ); submit_button( __( 'Activate', 'keepwatch' ), 'primary large', 'submit', false ); echo '
'; } /** * After a code activation that opened a new account: suggest signing in for more (CodeCanyon build). */ private function render_account_suggestion(): void { $purchase = PurchaseCode::state(); if ( null === $purchase || ! $purchase['account_created'] ) { return; } echo '

' . esc_html__( 'Your WPXLab account is ready', 'keepwatch' ) . '

'; echo '

' . esc_html__( 'We opened a WPXLab account for the e-mail you entered. Set a password and sign in to see the check history, choose who gets alerts and manage your purchase.', 'keepwatch' ) . '

'; echo '
'; printf( '%2$s ', esc_url( 'https://wpxlab.com/forgot-password' ), esc_html__( 'Set a password', 'keepwatch' ) ); printf( '%2$s', esc_url( 'https://wpxlab.com/login' ), esc_html__( 'Sign in', 'keepwatch' ) ); echo '
'; } /** * Service pitch (WordPress.org build). */ private function render_pitch(): void { echo '

' . esc_html__( 'Watch all your sites from one place', 'keepwatch' ) . '

'; echo '

' . esc_html__( 'Keepwatch works fully on this site without an account. Connecting adds outside uptime checks, history and alerts for all your sites in one place at wpxlab.com.', 'keepwatch' ) . '

'; echo ''; $this->render_steps(); echo '
'; } /** * How to connect. */ private function render_steps(): void { echo '
    '; $steps = array( __( 'Create a code at wpxlab.com → Dashboard → Keepwatch → Add a site.', 'keepwatch' ), __( 'Paste the code here and review what will be sent.', 'keepwatch' ), __( 'Accept and connect. The first report arrives within an hour.', 'keepwatch' ), ); foreach ( $steps as $step ) { echo '
  1. ' . esc_html( $step ) . '
  2. '; } echo '
'; } /** * Optional purchase code (CodeCanyon build, connected sites only). */ private function render_purchase_code(): void { $purchase = PurchaseCode::state(); echo '

' . esc_html__( 'CodeCanyon purchase code', 'keepwatch' ) . '

'; if ( null !== $purchase ) { $item = '' !== $purchase['item'] ? $purchase['item'] : 'Keepwatch'; printf( '

%s

', esc_html( sprintf( /* translators: 1: CodeCanyon item name, 2: last four characters of the purchase code */ __( 'Linked to your WPXLab account: %1$s (code ending in %2$s).', 'keepwatch' ), $item, $purchase['code_last4'] ) ) ); $until = '' !== $purchase['supported_until'] ? strtotime( $purchase['supported_until'] ) : false; if ( false !== $until ) { printf( '

%s

', esc_html( sprintf( /* translators: %s: date */ __( 'Support until %s.', 'keepwatch' ), wp_date( get_option( 'date_format' ), $until ) ) ) ); } } if ( null !== $purchase ) { echo '
'; return; } printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( self::REDEEM_ACTION ) ); wp_nonce_field( self::REDEEM_ACTION ); echo '

'; echo '

'; echo '

' . esc_html__( 'Find it under CodeCanyon → Downloads → Keepwatch → License certificate & purchase code. WPXLab checks the code with Envato and links it to the account this site is connected to. Keepwatch works the same on this site without it.', 'keepwatch' ) . '

'; submit_button( __( 'Add purchase code', 'keepwatch' ), 'secondary', 'submit', false ); echo '
'; } /** * What is sent (consent screen and connected view). * * @return string[] */ private function sent_items(): array { return array( __( 'Now: this site\'s address, an anonymous installation id, a public key, and the WordPress, PHP and Keepwatch versions.', 'keepwatch' ), __( 'Then hourly (WP-Cron, never during page loads): the outcome of each check (its id, status and short headline, e.g. "2 plugins have updates") and the WordPress, PHP, Keepwatch and WooCommerce versions. Check descriptions, user names, file paths, e-mails and IP addresses are not sent.', 'keepwatch' ), __( 'For the last 7 days: plugin, theme and WordPress updates (name and version numbers) and when PHP fatal errors started, with only the plugin or theme they came from. Error messages and file paths are not sent.', 'keepwatch' ), __( 'If WooCommerce is active: only totals (for example orders per hour). Never customer names, e-mails or addresses.', 'keepwatch' ), self::inventory_item(), __( 'From WPXLab: only signed settings for this site (how often to report and which of the data above to include). WPXLab cannot run code, change files or read your database.', 'keepwatch' ), __( 'Where: WPXLab (wpxlab.com), servers in the EU.', 'keepwatch' ), __( 'You can disconnect at any time with one click; sending stops immediately.', 'keepwatch' ), ); } /** * Consent text for keepwatch-report.v2 additions. */ private static function inventory_item(): string { return __( 'Installed plugins and themes (name, version, active, available update) and site metrics (memory limit, autoloaded options size, database size, overdue scheduled tasks), so updates and limits can be tracked across your sites.', 'keepwatch' ); } /** * Small single-button form. * * @param string $action Action. * @param string $intro Intro text. * @param string $label Button label. */ private function form( string $action, string $intro, string $label ): void { printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( $action ) ); wp_nonce_field( $action ); if ( '' !== $intro ) { echo '

' . esc_html( $intro ) . '

'; } submit_button( $label, 'secondary', 'submit', false ); echo '
'; } /** * Capability + nonce. * * @param string $action Nonce action. */ private function authorize( string $action ): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'keepwatch' ), 403 ); } check_admin_referer( $action ); } /** * One-time notice for the current user. * * @param string $type success|error. * @param string $message Message. */ private function notice( string $type, string $message ): void { set_transient( self::NOTICE . '_' . get_current_user_id(), array( 'type' => $type, 'message' => $message, ), 60 ); } /** * Back to the dashboard section. */ private function back(): void { wp_safe_redirect( Menu::tab_url( 'cloud' ) ); exit; } }