$view, 'post_type' => $filter_type, 'author' => $filter_author, 'search' => $search, 'per_page' => 0, // Get all records. 'paged' => 1, 'orderby' => 'days', 'order' => 'DESC', ) ); $items = $query->get_results(); $filename = sprintf( 'outdated-content-report-%s.csv', gmdate( 'Y-m-d' ) ); // Clean any previous output buffer to prevent corrupting CSV. if ( ob_get_level() ) { ob_end_clean(); } header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Streaming CSV download directly to php://output. $output = fopen( 'php://output', 'w' ); // Add UTF-8 BOM for Excel compatibility. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputs -- Writing BOM to php://output stream. fputs( $output, "\xEF\xBB\xBF" ); // CSV Header. $headers = array( __( 'ID', 'contentcortex' ), __( 'Title', 'contentcortex' ), __( 'URL', 'contentcortex' ), __( 'Post Type', 'contentcortex' ), __( 'Author', 'contentcortex' ), __( 'Last Updated (GMT)', 'contentcortex' ), __( 'Last Reviewed (GMT)', 'contentcortex' ), __( 'Days Since Update', 'contentcortex' ), __( 'Day Limit', 'contentcortex' ), ); fputcsv( $output, $headers ); $basis = ocap_get_setting( 'date_basis', 'modified' ); foreach ( $items as $item ) { $date_gmt = ( 'published' === $basis ) ? $item['post_date_gmt'] : $item['post_modified_gmt']; $row = array( $this->sanitize_for_csv( $item['ID'] ), $this->sanitize_for_csv( $item['title'] ), $this->sanitize_for_csv( $item['view_url'] ), $this->sanitize_for_csv( $item['post_type_label'] ), $this->sanitize_for_csv( $item['author_name'] ), $this->sanitize_for_csv( $date_gmt ), $this->sanitize_for_csv( ! empty( $item['last_reviewed'] ) ? $item['last_reviewed'] : '' ), $this->sanitize_for_csv( $item['days_since_update'] ), $this->sanitize_for_csv( $item['day_limit'] ), ); fputcsv( $output, $row ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing php://output stream. fclose( $output ); exit; } /** * Escape cells to prevent CSV injection (formula injection). * Prefix cells starting with '=', '+', '-', '@' with a single quote. * * @param mixed $value Value to sanitize. * @return string Sanitized string value. */ protected function sanitize_for_csv( $value ) { $str = (string) $value; if ( '' === $str ) { return ''; } $first_char = substr( $str, 0, 1 ); if ( in_array( $first_char, array( '=', '+', '-', '@', "\t", "\r" ), true ) ) { return "'" . $str; } return $str; } }