'GET', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'format' => array( 'type' => 'string', 'enum' => $this->formats(), 'default' => 'csv', ), ), 'callback' => array( $this, 'export' ), ) ); } /** * Stream the compliance report in the requested format and terminate the request. * * @since 1.1.0 * * @param WP_REST_Request $request The REST request. * @return void Streams the file. */ public function export( $request ) { $format = sanitize_key( (string) $request->get_param( 'format' ) ); if ( '' === $format || ! in_array( $format, $this->formats(), true ) ) { $format = 'csv'; } $headers = array( 'ID', 'File', 'Type', 'Disclosure label', 'Embedded', 'IPTC DigitalSourceType', 'URL' ); $rows = $this->rows(); $lit = $this->literacy(); $date = gmdate( 'Y-m-d' ); nocache_headers(); if ( 'csv' !== $format ) { /** * Stream a report format registered by an add-on. * * The handler streams the document itself and returns true when it has * handled the format; any unhandled format falls back to CSV. * * @since 1.1.0 * @param bool $handled Whether the format was streamed. Default false. * @param string $format The requested format key. * @param string[] $headers Column headers. * @param array[] $rows Report rows. * @param array $lit Article 4 literacy data. * @param string $date Report date. */ $handled = apply_filters( 'aicl_report_stream', false, $format, $headers, $rows, $lit, $date ); if ( true === $handled ) { exit; } } $this->send_csv( $headers, $rows, $lit, $date ); exit; } /** * The available report formats: CSV, plus any registered by add-ons. * * @since 1.1.0 * * @return string[] The format keys. */ protected function formats() { /** * Filter the available compliance-report formats. * * @since 1.1.0 * @param string[] $formats The format keys. Default array( 'csv' ). */ $formats = apply_filters( 'aicl_report_formats', array( 'csv' ) ); return array_values( array_unique( array_merge( array( 'csv' ), array_map( 'sanitize_key', (array) $formats ) ) ) ); } /** * Build the download filename for a given extension. * * @since 1.1.0 * * @param string $date Report date. * @param string $ext File extension. * @return string The filename. */ protected function filename( $date, $ext ) { return 'aim-transparency-report-' . $date . '.' . $ext; } /** * Stream the report as CSV. * * @since 1.1.0 * * @param string[] $headers Column headers. * @param array[] $rows Report rows. * @param array $lit Article 4 literacy data. * @param string $date Report date. * @return void */ protected function send_csv( $headers, $rows, $lit, $date ) { header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $this->filename( $date, 'csv' ) . '"' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming to the response. fputcsv( $out, $headers ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- see note. foreach ( $rows as $row ) { fputcsv( $out, $row ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- see note. } fputcsv( $out, array() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- blank separator row. fputcsv( $out, array( 'EU AI Act Article 4 — AI literacy', $lit['met'] . ' of ' . $lit['total'] . ' measures in place' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- see note. fputcsv( $out, array( 'Measure', 'Status', 'Evidence note' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- see note. foreach ( $lit['rows'] as $lrow ) { fputcsv( $out, $lrow ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv -- see note. } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- see note. } /** * Build the report rows from every flagged image attachment. * * @since 1.1.0 * * @return array[] One row per flagged attachment. */ protected function rows() { $ids = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'image', 'fields' => 'ids', 'posts_per_page' => self::LIMIT, 'meta_key' => AICL_Plugin::META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- exporting the flagged subset only. 'no_found_rows' => true, 'update_post_term_cache' => false, ) ); AICL_Plugin::prime( $ids ); $rows = array(); foreach ( $ids as $id ) { $token = AICL_Plugin::get_type( $id ); $file = get_attached_file( $id ); $embed = (string) get_post_meta( $id, AICL_Plugin::META_EMBED, true ); $rows[] = array( $id, $file ? wp_basename( $file ) : '', $token, AICL_Plugin::label_for( $token ), '' !== $embed ? $embed : 'no', AICL_Plugin::uri_for( $token ), (string) wp_get_attachment_url( $id ), ); } return $rows; } /** * Build the Article 4 (AI literacy) section from the saved readiness checklist. * * @since 1.1.0 * * @return array array( 'met' => int, 'total' => int, 'rows' => array(array(label,status,note)) ). */ protected function literacy() { $state = AICL_Readiness::get(); $labels = AICL_Readiness::labels(); $rows = array(); $met = 0; foreach ( $labels as $key => $label ) { $done = ! empty( $state[ $key ]['done'] ); $met += $done ? 1 : 0; $rows[] = array( $label, $done ? 'done' : 'todo', isset( $state[ $key ]['note'] ) ? (string) $state[ $key ]['note'] : '', ); } return array( 'met' => $met, 'total' => count( $labels ), 'rows' => $rows, ); } }