$count, 'items' => isset( $hook_extra[ $type . 's' ] ) ? array_slice( (array) $hook_extra[ $type . 's' ], 0, 25 ) : array(), 'action' => isset( $hook_extra['action'] ) ? sanitize_key( $hook_extra['action'] ) : '', ) ); } /** * Core update. * * @since 0.1.0 * * @param string $version New WordPress version. * @return void */ public function track_core_update( $version ) { self::log( Event_Log::TYPE_MAINTENANCE, 'core_updated', sprintf( /* translators: %s: WordPress version. */ __( 'WordPress updated to %s', 'oms-quietadmin' ), sanitize_text_field( (string) $version ) ), array( 'version' => (string) $version ) ); } /** * Automatic updates. * * @since 0.1.0 * * @param array $data Result of the automatic updates. * @return void */ public function track_auto_updates( $data ) { $done = 0; $failed = 0; foreach ( array( 'core', 'plugin', 'theme', 'translation' ) as $group ) { if ( ! empty( $data[ $group ] ) && is_array( $data[ $group ] ) ) { foreach ( $data[ $group ] as $item ) { if ( ! is_array( $item ) || ! array_key_exists( 'result', $item ) ) { continue; } if ( ! empty( $item['result'] ) && ! is_wp_error( $item['result'] ) ) { ++$done; } else { ++$failed; } } } } if ( $failed > 0 ) { self::log_failure( 'auto_update_failed', sprintf( /* translators: %d: number of failed automatic updates. */ _n( '%d automatic update failed', '%d automatic updates failed', $failed, 'oms-quietadmin' ), $failed ) ); } // Successful plugin, theme and core updates are already recorded by the // dedicated upgrader hooks. Logging the aggregate here would count them // twice in care reports. This hook is retained for failure detection. } /** * Count spam comments. * * @since 0.1.0 * * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Comment $comment Comment. * @return void */ public function track_comment_status( $new_status, $old_status, $comment ) { unset( $comment ); if ( 'spam' !== $new_status || 'spam' === $old_status ) { return; } self::log( Event_Log::TYPE_SECURITY, 'spam_blocked', __( 'Spam comment blocked', 'oms-quietadmin' ) ); } /** * Akismet hits. * * @since 0.1.0 * * @return void */ public function track_akismet_spam() { self::log( Event_Log::TYPE_SECURITY, 'spam_blocked', __( 'Spam blocked', 'oms-quietadmin' ) ); } /** * Comments marked as spam directly (comment_post with status 'spam'). * * @since 0.1.0 * * @param int $comment_id Comment ID. * @param int|string $status Approval status (0, 1 or 'spam'). * @return void */ public function track_comment_post( $comment_id, $status ) { if ( 'spam' !== $status ) { return; } self::log( Event_Log::TYPE_SECURITY, 'spam_blocked', __( 'Suspicious comment blocked', 'oms-quietadmin' ), array( 'comment_id' => (int) $comment_id ) ); } /** * Failed login. * * @since 0.1.0 * * @param string $username Attempted username. * @return void */ public function track_failed_login( $username ) { unset( $username ); /** * Maximum number of logged failed attempts per hour. * * Protects the log table during brute-force waves. A value of 0 * disables logging of failed logins. * * @since 0.1.2 * * @param int $limit Entries per hour (default 60). */ $limit = (int) apply_filters( 'quietadmin_failed_login_log_limit', 60 ); if ( $limit < 1 ) { return; } $bucket = 'quietadmin_fl_' . gmdate( 'YmdH' ); $count = (int) get_transient( $bucket ); if ( $count >= $limit ) { return; } set_transient( $bucket, $count + 1, 2 * HOUR_IN_SECONDS ); $meta = array(); /** * Whether to record additional details (truncated IP address). * * By default, no personal data (IP address, * username) is stored. Anyone who needs it must enable this * deliberately and adapt the privacy policy. * * @since 0.1.2 * * @param bool $log_details True = store anonymized IP. */ if ( apply_filters( 'quietadmin_log_login_details', false ) && isset( $_SERVER['REMOTE_ADDR'] ) ) { $meta['ip'] = wp_privacy_anonymize_ip( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ); } self::log( Event_Log::TYPE_SECURITY, 'login_failed', __( 'Failed login attempt recorded', 'oms-quietadmin' ), $meta ); } /** * Successful login of the client (for the "last activity" info). * * @since 0.1.0 * * @param string $user_login Username. * @param \WP_User $user User. * @return void */ public function track_login( $user_login, $user ) { unset( $user_login ); if ( ! $user instanceof \WP_User || ! Roles::is_client( $user ) ) { return; } update_user_meta( $user->ID, 'quietadmin_last_login', time() ); self::log( Event_Log::TYPE_AGENCY, 'client_login', __( 'Client logged in', 'oms-quietadmin' ), array( 'client_id' => $user->ID ) ); } /** * Backup detected. * * @since 0.1.0 * * @param mixed $data Plugin-specific data (unused). * @return void */ public function track_backup( $data = null ) { unset( $data ); // Avoid duplicate entries within 5 minutes. if ( get_transient( 'quietadmin_backup_logged' ) ) { return; } set_transient( 'quietadmin_backup_logged', 1, 5 * MINUTE_IN_SECONDS ); self::log( Event_Log::TYPE_BACKUP, 'backup_created', __( 'Backup created', 'oms-quietadmin' ) ); } /** * Count publications (only on transition to "published"). * * @since 0.1.0 * * @param string $new_status Neuer Status. * @param string $old_status Alter Status. * @param \WP_Post $post Post object. * @return void */ public function track_publish( $new_status, $old_status, $post ) { if ( 'publish' !== $new_status || 'publish' === $old_status ) { return; } if ( ! $post instanceof \WP_Post || ! is_post_type_viewable( $post->post_type ) ) { return; } self::log( Event_Log::TYPE_AGENCY, 'content_published', __( 'New content published', 'oms-quietadmin' ), array( 'post_id' => (int) $post->ID ) ); } }