Repository navigation
Expand file tree
/
Copy pathcsv_reader.php
More file actions
183 lines (155 loc) · 4.71 KB
/
Copy pathcsv_reader.php
File metadata and controls
183 lines (155 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
/**
*
* Add User extension for the phpBB Forum Software package
*
* @copyright (c) 2026, phpBB Modders, https://www.phpbbmodders.com/
* @license GNU General Public License, version 2 (GPL-2.0)
*
*/
namespace phpbbmodders\adduser;
/**
* Reads the CSV file of the "Add users from CSV" page.
*
* The first row names the columns, in any order. username and email are
* required; password, lang and birthday are optional. The file must be UTF-8
* (a byte order mark is dropped). The separator is a comma or a semicolon,
* whichever the header row has more of.
*
* This class only splits the file into rows. Checking the values is left to
* phpBB's own validation in the ACP module.
*/
class csv_reader
{
/** @var string[] Every column the file may have */
const COLUMNS = ['username', 'email', 'password', 'lang', 'birthday'];
/** @var string[] Columns the file must have */
const REQUIRED_COLUMNS = ['username', 'email'];
/**
* Split CSV text into rows keyed by column name.
*
* @param string $contents The whole file
* @param int $max_rows Most data rows allowed, not counting the header row
* @return array [
* 'error' => array|null File-level error: a language key followed by its arguments, or null
* 'columns' => string[] Column names in file order
* 'rows' => array[] One entry per non-blank data row: [
* 'row' => int Row number in the file, the header row being row 1
* 'values' => string[] Every entry of COLUMNS mapped to its trimmed value ('' when absent)
* 'error' => array|null Row-level error, as above, or null
* ]
* ]
*/
public function parse($contents, $max_rows)
{
$result = ['error' => null, 'columns' => [], 'rows' => []];
// Excel adds a byte order mark when saving "CSV UTF-8"
if (strncmp($contents, "\xEF\xBB\xBF", 3) === 0)
{
$contents = substr($contents, 3);
}
if (trim($contents) === '')
{
$result['error'] = ['ADDUSER_CSV_EMPTY'];
return $result;
}
if (!preg_match('//u', $contents))
{
$result['error'] = ['ADDUSER_CSV_NOT_UTF8'];
return $result;
}
$first_line = strtok($contents, "\r\n");
$separator = (substr_count($first_line, ';') > substr_count($first_line, ',')) ? ';' : ',';
$handle = fopen('php://temp', 'r+');
fwrite($handle, $contents);
rewind($handle);
$header = $this->read_record($handle, $separator);
$columns = [];
// Column name by field position; null for the blank header cells
// spreadsheets leave after the last column
$positions = [];
foreach ($header as $index => $name)
{
$name = strtolower(trim((string) $name));
if ($name === '')
{
$positions[$index] = null;
continue;
}
if (!in_array($name, self::COLUMNS, true))
{
fclose($handle);
$result['error'] = ['ADDUSER_CSV_UNKNOWN_COLUMN', $name, implode(', ', self::COLUMNS)];
return $result;
}
if (in_array($name, $columns, true))
{
fclose($handle);
$result['error'] = ['ADDUSER_CSV_DUPLICATE_COLUMN', $name];
return $result;
}
$columns[] = $name;
$positions[$index] = $name;
}
foreach (self::REQUIRED_COLUMNS as $name)
{
if (!in_array($name, $columns, true))
{
fclose($handle);
$result['error'] = ['ADDUSER_CSV_MISSING_COLUMN', $name];
return $result;
}
}
$result['columns'] = $columns;
$row_number = 1;
while (($record = $this->read_record($handle, $separator)) !== false)
{
$row_number++;
$fields = array_map('trim', array_map('strval', $record));
// Skip blank rows, including the ",,," rows spreadsheets leave behind
if (implode('', $fields) === '')
{
continue;
}
if (count($result['rows']) >= $max_rows)
{
fclose($handle);
$result['error'] = ['ADDUSER_CSV_TOO_MANY_ROWS', $max_rows];
$result['rows'] = [];
return $result;
}
$values = array_fill_keys(self::COLUMNS, '');
$error = null;
foreach ($fields as $index => $value)
{
if (isset($positions[$index]))
{
$values[$positions[$index]] = $value;
}
else if ($value !== '')
{
$error = ['ADDUSER_CSV_TOO_MANY_FIELDS', count($columns)];
}
}
$result['rows'][] = ['row' => $row_number, 'values' => $values, 'error' => $error];
}
fclose($handle);
if (!count($result['rows']))
{
$result['error'] = ['ADDUSER_CSV_NO_ROWS'];
}
return $result;
}
/**
* Read one CSV record, following RFC 4180: quotes are escaped by doubling
* them, never by a backslash.
*
* @param resource $handle Open stream
* @param string $separator Field separator
* @return array|false Fields of the record, or false at the end of the stream
*/
protected function read_record($handle, $separator)
{
return fgetcsv($handle, 0, $separator, '"', '');
}
}