diff --git a/plugins/bcc-login/includes/class-bcc-login-settings.php b/plugins/bcc-login/includes/class-bcc-login-settings.php index b38e599..f194da0 100644 --- a/plugins/bcc-login/includes/class-bcc-login-settings.php +++ b/plugins/bcc-login/includes/class-bcc-login-settings.php @@ -596,6 +596,7 @@ function render_options_page() { ?> render_delete_subscribers_button( ); ?> + _settings->client_id ) || empty( $this->_settings->client_secret ) ) { - error_log( 'BCC Login: The site report needs the client id and secret of the login client.' ); - return; + $sent = $this->deliver(); + if ( is_wp_error( $sent ) ) { + error_log( 'BCC Login: ' . $sent->get_error_message() ); } + } - $token = $this->get_token(); - if ( ! $token ) { - return; + /** + * Sends the report right away, however recently it was sent, e.g. to check the site's setup in Auth0. + * It clears the last claim rather than claiming the next 12 hours, so the next daily report is still sent + * at its usual time. + * + * @return array|WP_Error The report that was sent, or why it couldn't be sent. + */ + function send_now() { + global $wpdb; + + $wpdb->delete( $wpdb->options, array( 'option_name' => self::LOCK_OPTION ) ); + + return $this->deliver(); + } + + /** + * Claims the next 12 hours for sending the daily report: true when the last claim is older than that. Of + * two runs at once, only one can claim them. + * + * The time is kept in an option, but read and written only with SQL. WordPress's own lock + * (WP_Upgrader::create_lock) goes through the option cache, and re-creating an expired lock leaves the + * option cached as missing. With a persistent object cache that stopped the daily report after two days. + */ + private static function claim() { + global $wpdb; + + $now = time(); + + // The first claim adds the option. Of two runs at once, only one can add it. + $added = $wpdb->query( $wpdb->prepare( + "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %d, 'off')", + self::LOCK_OPTION, + $now + ) ); + if ( $added > 0 ) { + return true; } - $response = wp_remote_post( rtrim( $this->_settings->widgets_base_url, '/' ) . '/api/wordpress-sites/report', array( - 'timeout' => 15, - 'headers' => array( - 'Authorization' => 'Bearer ' . $token, - 'Content-Type' => 'application/json', - ), - 'body' => wp_json_encode( $this->get_report() ), + // Later claims move the time forward once it's 12 hours old. Of two runs at once, the second waits for + // the first and then finds the new time. + $moved = $wpdb->query( $wpdb->prepare( + "UPDATE {$wpdb->options} SET option_value = %d WHERE option_name = %s AND CAST(option_value AS UNSIGNED) <= %d", + $now, + self::LOCK_OPTION, + $now - 12 * HOUR_IN_SECONDS ) ); - if ( is_wp_error( $response ) ) { - error_log( 'BCC Login: Could not send the site report: ' . $response->get_error_message() ); + // Both statements count a row only when they claim it, also on connections that count matched rows + // instead of changed ones (MYSQLI_CLIENT_FOUND_ROWS). + return $moved > 0; + } + + /** + * The "Send site report now" button under Maintenance on the settings page, and what happened when it was + * pressed. Like the rest of Maintenance, only administrators see it. + */ + function render_send_now_button() { + if ( ! current_user_can( 'administrator' ) ) { return; } + ?> +
+ + + send_now_handler(); + settings_errors( 'site_report_sent' ); + } - $status = wp_remote_retrieve_response_code( $response ); - if ( $status < 200 || $status >= 300 ) { - error_log( 'BCC Login: The widgets API refused the site report with HTTP ' . $status . '.' ); + /** + * Sends the report when the button was pressed, and shows what was sent or why it couldn't be sent. + */ + function send_now_handler() { + if ( + ! isset( $_POST['send_site_report'], $_POST['send_site_report_nonce'] ) || + ! wp_verify_nonce( $_POST['send_site_report_nonce'], 'send_site_report' ) || + ! current_user_can( 'administrator' ) + ) { + return; } + + $report = $this->send_now(); + + // settings_errors() prints messages as HTML, and Auth0's error description comes from outside. + if ( is_wp_error( $report ) ) { + add_settings_error( 'site_report_sent', 'site_report_not_sent', esc_html( $report->get_error_message() ), 'error' ); + return; + } + + add_settings_error( + 'site_report_sent', + 'site_report_sent', + esc_html( sprintf( + __( 'The site report was sent: %1$s, bcc-login %2$s, WordPress %3$s, PHP %4$s, users: %5$s, subscribers: %6$s.', 'bcc-login' ), + $report['siteUrl'], + $report['pluginVersion'], + $report['wordpressVersion'], + $report['phpVersion'], + null === $report['users'] ? '?' : $report['users'], + null === $report['subscribers'] ? '?' : $report['subscribers'] + ) ), + 'success' + ); } function get_report() { @@ -105,6 +188,43 @@ function get_report() { ); } + /** + * Gets a token for the site's login client and posts the report to the widgets API. + * + * @return array|WP_Error The report that was sent, or why it couldn't be sent. + */ + private function deliver() { + if ( empty( $this->_settings->client_id ) || empty( $this->_settings->client_secret ) ) { + return new WP_Error( 'bcc_login_site_report', 'The site report needs the client id and secret of the login client.' ); + } + + $token = $this->get_token(); + if ( is_wp_error( $token ) ) { + return $token; + } + + $report = $this->get_report(); + $response = wp_remote_post( rtrim( $this->_settings->widgets_base_url, '/' ) . '/api/wordpress-sites/report', array( + 'timeout' => 15, + 'headers' => array( + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $report ), + ) ); + + if ( is_wp_error( $response ) ) { + return new WP_Error( 'bcc_login_site_report', 'Could not send the site report: ' . $response->get_error_message() ); + } + + $status = wp_remote_retrieve_response_code( $response ); + if ( $status < 200 || $status >= 300 ) { + return new WP_Error( 'bcc_login_site_report', 'The widgets API refused the site report with HTTP ' . $status . '.' ); + } + + return $report; + } + /** * Whether WordPress updates this plugin by itself, as its background updater decides: automatic updates * aren't turned off (e.g. by DISALLOW_FILE_MODS), it can write the files without asking for credentials, @@ -134,6 +254,8 @@ private static function load_upgrader() { /** * A token for the widgets API, for the site's own login client. + * + * @return string|WP_Error */ private function get_token() { $response = wp_remote_post( $this->_settings->token_endpoint, array( @@ -147,8 +269,7 @@ private function get_token() { ) ); if ( is_wp_error( $response ) ) { - error_log( 'BCC Login: Could not get a token for the site report: ' . $response->get_error_message() ); - return null; + return new WP_Error( 'bcc_login_site_report', 'Could not get a token for the site report: ' . $response->get_error_message() ); } $body = json_decode( wp_remote_retrieve_body( $response ) ); @@ -156,8 +277,7 @@ private function get_token() { if ( 200 !== wp_remote_retrieve_response_code( $response ) || empty( $body->access_token ) ) { // Auth0's error code and description only, e.g. when the login client may not get a token for the widgets API yet. $error = isset( $body->error ) ? $body->error . ( isset( $body->error_description ) ? ': ' . $body->error_description : '' ) : 'HTTP ' . wp_remote_retrieve_response_code( $response ); - error_log( 'BCC Login: Could not get a token for the site report: ' . $error ); - return null; + return new WP_Error( 'bcc_login_site_report', 'Could not get a token for the site report: ' . $error ); } return $body->access_token;