Bugzilla – Attachment 191466 Details for
Bug 41619
Add `Koha::CSV`
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41619: Add Koha::CSV
f296f2f.patch (text/plain), 11.22 KB, created by
Tomás Cohen Arazi (tcohen)
on 2026-01-14 19:50:15 UTC
(
hide
)
Description:
Bug 41619: Add Koha::CSV
Filename:
MIME Type:
Creator:
Tomás Cohen Arazi (tcohen)
Created:
2026-01-14 19:50:15 UTC
Size:
11.22 KB
patch
obsolete
>From f296f2fc6cffa22b52316641ff9169b0c5f4ba07 Mon Sep 17 00:00:00 2001 >From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= <tomascohen@theke.io> >Date: Wed, 14 Jan 2026 16:41:33 -0300 >Subject: [PATCH] Bug 41619: Add Koha::CSV > >This patch introduces Koha::CSV, a wrapper around Text::CSV_XS that >provides consistent defaults and methods for CSV generation across Koha. > >The class: >- Inherits CSV delimiter from CSVDelimiter system preference >- Enforces security defaults (binary=1, formula='empty') >- Provides convenient methods for common CSV operations >- Allows per-instance configuration overrides > >This establishes a foundation for standardizing CSV exports throughout >Koha, eliminating inconsistencies in CSV generation and providing a >testable, maintainable approach to CSV handling. > >Test plan: >1. Apply patch >2. Run: > $ ktd --shell > k$ prove t/Koha/CSV.t >=> SUCCESS: Tests pass! >3. Tests cover: > - Default initialization with CSVDelimiter preference > - Preference value inheritance (comma, semicolon, tabulation) > - Constructor parameter overrides > - add_row() method with various data types > - combine() and string() methods > - print() to filehandle > - Proper quote escaping and empty field handling >4. Sign off :-D >--- > Koha/CSV.pm | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++ > t/Koha/CSV.t | 140 +++++++++++++++++++++++++++++++ > 2 files changed, 366 insertions(+) > create mode 100644 Koha/CSV.pm > create mode 100755 t/Koha/CSV.t > >diff --git a/Koha/CSV.pm b/Koha/CSV.pm >new file mode 100644 >index 0000000000..e64c80d128 >--- /dev/null >+++ b/Koha/CSV.pm >@@ -0,0 +1,226 @@ >+package Koha::CSV; >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <https://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Text::CSV_XS; >+use C4::Context; >+ >+use base qw(Class::Accessor); >+ >+__PACKAGE__->mk_accessors(qw( binary formula always_quote eol sep_char )); >+ >+=head1 NAME >+ >+Koha::CSV - Wrapper around Text::CSV_XS with Koha-specific defaults >+ >+=head1 SYNOPSIS >+ >+ my $csv = Koha::CSV->new(); >+ $csv->combine(@fields); >+ my $line = $csv->string(); >+ >+ # Override defaults >+ my $csv = Koha::CSV->new( { sep_char => ';', always_quote => 1 } ); >+ >+=head1 DESCRIPTION >+ >+This class wraps Text::CSV_XS providing consistent defaults and methods >+used across Koha for CSV generation and parsing. >+ >+Configuration is inherited from Koha system preferences where applicable, >+but can be overridden per instance. >+ >+=head2 Configuration Inheritance >+ >+=over 4 >+ >+=item * B<sep_char> - Defaults to I<CSVDelimiter> system preference (or ',' if not set) >+ >+=item * B<binary> - Always 1 (required for UTF-8 support) >+ >+=item * B<formula> - Always 'empty' (security: prevents formula injection) >+ >+=item * B<always_quote> - Defaults to 0, can be overridden >+ >+=item * B<eol> - Defaults to "\n", can be overridden >+ >+=back >+ >+=cut >+ >+=head2 new >+ >+ my $csv = Koha::CSV->new(); >+ my $csv = Koha::CSV->new({ sep_char => ';', always_quote => 1 }); >+ >+Creates a new Koha::CSV object. Accepts optional parameters to override defaults. >+ >+Parameters: >+- sep_char: CSV delimiter (defaults to CSVDelimiter system preference) >+- always_quote: Whether to quote all fields (defaults to 0) >+- eol: End of line character (defaults to "\n") >+ >+Note: binary and formula parameters are fixed for security and cannot be overridden. >+ >+=cut >+ >+sub new { >+ my ( $class, $params ) = @_; >+ $params //= {}; >+ >+ # Get delimiter from Koha configuration if not explicitly provided >+ my $sep_char = $params->{sep_char} // C4::Context->csv_delimiter(); >+ >+ my $self = $class->SUPER::new( >+ { >+ binary => 1, # Always 1 for UTF-8 >+ formula => 'empty', # Always 'empty' for security >+ always_quote => $params->{always_quote} // 0, # Overridable >+ eol => $params->{eol} // "\n", # Overridable >+ sep_char => $sep_char, # From Koha config or override >+ } >+ ); >+ >+ $self->{_csv} = Text::CSV_XS->new( >+ { >+ binary => $self->binary, >+ formula => $self->formula, >+ always_quote => $self->always_quote, >+ eol => $self->eol, >+ sep_char => $self->sep_char, >+ } >+ ); >+ >+ return $self; >+} >+ >+=head2 combine >+ >+ $csv->combine(@fields); >+ >+Combines fields into a CSV line. Returns true on success. >+ >+=cut >+ >+sub combine { >+ my ( $self, @fields ) = @_; >+ return $self->{_csv}->combine(@fields); >+} >+ >+=head2 string >+ >+ my $line = $csv->string(); >+ >+Returns the combined CSV line as a string. >+ >+=cut >+ >+sub string { >+ my ($self) = @_; >+ return $self->{_csv}->string(); >+} >+ >+=head2 add_row >+ >+ my $line = $csv->add_row(@fields); >+ # or >+ my $line = $csv->add_row(\@fields); >+ >+Convenience method that combines fields and returns the CSV line as a string. >+Accepts either an array or arrayref of fields. >+ >+=cut >+ >+sub add_row { >+ my ( $self, @fields ) = @_; >+ >+ # Handle arrayref or array >+ @fields = @{ $fields[0] } if @fields == 1 && ref $fields[0] eq 'ARRAY'; >+ >+ $self->combine(@fields) or return; >+ return $self->string(); >+} >+ >+=head2 parse >+ >+ $csv->parse($line); >+ >+Parses a CSV line. Returns true on success. >+ >+=cut >+ >+sub parse { >+ my ( $self, $line ) = @_; >+ return $self->{_csv}->parse($line); >+} >+ >+=head2 fields >+ >+ my @fields = $csv->fields(); >+ >+Returns the parsed fields from the last parse() call. >+ >+=cut >+ >+sub fields { >+ my ($self) = @_; >+ return $self->{_csv}->fields(); >+} >+ >+=head2 print >+ >+ $csv->print($fh, \@fields); >+ >+Prints fields to a filehandle as a CSV line. >+ >+=cut >+ >+sub print { >+ my ( $self, $fh, $fields ) = @_; >+ return $self->{_csv}->print( $fh, $fields ); >+} >+ >+=head2 getline >+ >+ my $fields = $csv->getline($fh); >+ >+Reads and parses a line from a filehandle. Returns arrayref of fields. >+ >+=cut >+ >+sub getline { >+ my ( $self, $fh ) = @_; >+ return $self->{_csv}->getline($fh); >+} >+ >+=head2 error_diag >+ >+ my $error = $csv->error_diag(); >+ >+Returns error diagnostics from the last operation. >+ >+=cut >+ >+sub error_diag { >+ my ($self) = @_; >+ return $self->{_csv}->error_diag(); >+} >+ >+1; >diff --git a/t/Koha/CSV.t b/t/Koha/CSV.t >new file mode 100755 >index 0000000000..1c0a1e9d76 >--- /dev/null >+++ b/t/Koha/CSV.t >@@ -0,0 +1,140 @@ >+#!/usr/bin/perl >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <https://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Test::More tests => 8; >+use Test::Warn; >+use Test::NoWarnings; >+ >+use t::lib::Mocks; >+ >+use C4::Context; >+ >+BEGIN { >+ use_ok('Koha::CSV'); >+} >+ >+subtest 'new() tests' => sub { >+ plan tests => 5; >+ >+ my $csv = Koha::CSV->new(); >+ isa_ok( $csv, 'Koha::CSV', 'Object created' ); >+ is( $csv->binary, 1, 'binary defaults to 1' ); >+ is( $csv->formula, 'empty', 'formula defaults to empty' ); >+ is( $csv->always_quote, 0, 'always_quote defaults to 0' ); >+ is( $csv->eol, "\n", 'eol defaults to newline' ); >+}; >+ >+subtest 'new() with CSVDelimiter preference tests' => sub { >+ plan tests => 3; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ my $csv = Koha::CSV->new(); >+ is( $csv->sep_char, ',', 'Uses comma from preference' ); >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ';' ); >+ $csv = Koha::CSV->new(); >+ is( $csv->sep_char, ';', 'Uses semicolon from preference' ); >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', 'tabulation' ); >+ $csv = Koha::CSV->new(); >+ is( $csv->sep_char, "\t", 'Converts tabulation to tab character' ); >+}; >+ >+subtest 'new() with overrides tests' => sub { >+ plan tests => 3; >+ >+ my $csv = Koha::CSV->new( >+ { >+ sep_char => '|', >+ always_quote => 1, >+ eol => "\r\n", >+ } >+ ); >+ >+ is( $csv->sep_char, '|', 'sep_char overridden' ); >+ is( $csv->always_quote, 1, 'always_quote overridden' ); >+ is( $csv->eol, "\r\n", 'eol overridden' ); >+}; >+ >+subtest 'add_row() tests' => sub { >+ plan tests => 4; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ my $csv = Koha::CSV->new(); >+ >+ # Test with array >+ my $line = $csv->add_row( 'Title', 'Author', 'Year' ); >+ is( $line, "Title,Author,Year\n", 'add_row with array' ); >+ >+ # Test with arrayref >+ $line = $csv->add_row( [ 'Book 1', 'Smith, John', '2024' ] ); >+ is( $line, "\"Book 1\",\"Smith, John\",2024\n", 'add_row with arrayref and quoted field' ); >+ >+ # Test with special characters >+ $line = $csv->add_row( [ 'Title with "quotes"', 'Normal', 'Value' ] ); >+ is( $line, "\"Title with \"\"quotes\"\"\",Normal,Value\n", 'add_row with quotes escaped' ); >+ >+ # Test with empty fields >+ $line = $csv->add_row( [ 'Field1', '', 'Field3' ] ); >+ is( $line, "Field1,,Field3\n", 'add_row with empty field' ); >+}; >+ >+subtest 'combine() and string() tests' => sub { >+ plan tests => 3; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ my $csv = Koha::CSV->new(); >+ >+ my $status = $csv->combine( 'A', 'B', 'C' ); >+ ok( $status, 'combine returns true on success' ); >+ >+ my $line = $csv->string(); >+ is( $line, "A,B,C\n", 'string returns combined line' ); >+ >+ # Test with always_quote >+ $csv = Koha::CSV->new( { always_quote => 1 } ); >+ $csv->combine( 'A', 'B', 'C' ); >+ $line = $csv->string(); >+ is( $line, "\"A\",\"B\",\"C\"\n", 'always_quote wraps all fields' ); >+}; >+ >+subtest 'print() tests' => sub { >+ plan tests => 2; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ my $csv = Koha::CSV->new(); >+ >+ # Create temp file >+ my $output = ''; >+ open my $fh, '>', \$output or die "Cannot open string as file: $!"; >+ >+ my $status = $csv->print( $fh, [ 'Header1', 'Header2', 'Header3' ] ); >+ ok( $status, 'print returns true on success' ); >+ >+ $csv->print( $fh, [ 'Value1', 'Value2', 'Value3' ] ); >+ close $fh; >+ >+ is( >+ $output, >+ "Header1,Header2,Header3\nValue1,Value2,Value3\n", >+ 'print writes correct CSV to filehandle' >+ ); >+}; >-- >2.50.1 (Apple Git-155) >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 41619
:
191466
|
191491