'dashicons-update', 'plugin_activate' => 'dashicons-plus-alt', 'plugin_deactivate' => 'dashicons-dismiss', 'theme_update' => 'dashicons-admin-appearance', 'theme_switch' => 'dashicons-randomize', 'core_update' => 'dashicons-wordpress', 'settings_change' => 'dashicons-admin-settings', 'content_publish' => 'dashicons-edit', 'user_created' => 'dashicons-admin-users', 'php_error' => 'dashicons-warning', ); public static function type_label( string $type ): string { $labels = array( 'plugin_update' => __( 'Plugin update', 'aftercare' ), 'plugin_activate' => __( 'Plugin activated', 'aftercare' ), 'plugin_deactivate' => __( 'Plugin deactivated', 'aftercare' ), 'theme_update' => __( 'Theme update', 'aftercare' ), 'theme_switch' => __( 'Theme switch', 'aftercare' ), 'core_update' => __( 'Core update', 'aftercare' ), 'settings_change' => __( 'Settings change', 'aftercare' ), 'content_publish' => __( 'Content published', 'aftercare' ), 'user_created' => __( 'User created', 'aftercare' ), 'php_error' => __( 'PHP fatal error', 'aftercare' ), ); return $labels[ $type ] ?? $type; } public function render(): void { $ledger = new LedgerRepository(); $filters = $this->current_filters(); $page = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination. $events = $ledger->query( $filters, self::PER_PAGE, $page ); $total = $ledger->count( $filters ); $pages = (int) ceil( $total / self::PER_PAGE ); Menu::header( __( 'Change Ledger', 'aftercare' ) ); echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=aftercare_ledger_export' ), 'aftercare_ledger_export' ); echo ' ' . esc_html__( 'Export CSV', 'aftercare' ) . ''; echo '
'; if ( empty( $events ) ) { echo '

' . esc_html__( 'No events match. Site changes are recorded automatically as they happen.', 'aftercare' ) . '

'; } else { echo ''; } if ( $pages > 1 ) { echo '
'; $base = add_query_arg( array_filter( array_merge( array( 'page' => 'aftercare-ledger' ), $filters ) ), admin_url( 'admin.php' ) ); echo wp_kses_post( paginate_links( array( 'base' => $base . '%_%', 'format' => '&paged=%#%', 'current' => $page, 'total' => $pages, ) ) ?? '' ); echo '
'; } Menu::footer(); } /** * @param array $event */ public static function render_event_row( array $event ): void { $icon = self::TYPE_ICONS[ $event['event_type'] ] ?? 'dashicons-marker'; echo '
  • '; echo ''; echo '' . esc_html( (string) $event['summary'] ) . ''; echo ''; printf( /* translators: 1: actor name, 2: human time diff */ esc_html__( 'by %1$s ยท %2$s ago', 'aftercare' ), esc_html( (string) ( $event['actor_label'] ?: __( 'system', 'aftercare' ) ) ), esc_html( human_time_diff( strtotime( $event['occurred_at'] . ' UTC' ) ) ) ); echo '
  • '; } /** * @return array{type?: string, from?: string, to?: string} */ private function current_filters(): array { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only filters. $filters = array(); if ( ! empty( $_GET['type'] ) ) { $filters['type'] = sanitize_key( wp_unslash( $_GET['type'] ) ); } foreach ( array( 'from', 'to' ) as $key ) { if ( ! empty( $_GET[ $key ] ) ) { $date = sanitize_text_field( wp_unslash( $_GET[ $key ] ) ); if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { $filters[ $key ] = $date; } } } return $filters; // phpcs:enable } /** * Stream the full ledger as CSV. */ public static function handle_export(): void { if ( ! current_user_can( Menu::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'aftercare' ) ); } check_admin_referer( 'aftercare_ledger_export' ); $ledger = new LedgerRepository(); $events = $ledger->query( array(), 100000 ); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=aftercare-ledger-' . gmdate( 'Y-m-d' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); // An empty escape character gives RFC 4180 output (quotes are only // ever doubled), which JSON payloads containing \" need. PHP 8.4 // also deprecates leaving the argument out. fputcsv( $out, array( 'occurred_at', 'event_type', 'summary', 'actor', 'payload' ), ',', '"', '' ); foreach ( $events as $event ) { fputcsv( $out, array( $event['occurred_at'], $event['event_type'], $event['summary'], $event['actor_label'], $event['payload'], ), ',', '"', '' ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } }