Check for code that has been commented out.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * This file contains only the CSV class. |
||
4 | * |
||
5 | * @file |
||
6 | * @package Tabulate |
||
7 | */ |
||
8 | |||
9 | namespace WordPress\Tabulate; |
||
10 | |||
11 | use WordPress\Tabulate\DB\ChangeTracker; |
||
12 | use WordPress\Tabulate\DB\Record; |
||
13 | |||
14 | /** |
||
15 | * A class for parsing a CSV file has either just been uploaded (i.e. $_FILES is |
||
16 | * set), or is stored as a temporary file (as defined herein). |
||
17 | */ |
||
18 | class CSV { |
||
19 | |||
20 | /** |
||
21 | * The headers in the CSV data. |
||
22 | * |
||
23 | * @var string[] |
||
24 | */ |
||
25 | public $headers; |
||
26 | |||
27 | /** |
||
28 | * Two-dimenstional integer-indexed array of the CSV's data. |
||
29 | * |
||
30 | * @var array[] |
||
31 | */ |
||
32 | public $data; |
||
33 | |||
34 | /** |
||
35 | * Temporary identifier for CSV file. |
||
36 | * |
||
37 | * @var string |
||
38 | */ |
||
39 | public $hash = false; |
||
40 | |||
41 | /** |
||
42 | * The filesystem. |
||
43 | * |
||
44 | * @var \WP_Filesystem_Base |
||
45 | */ |
||
46 | protected $filesystem; |
||
47 | |||
48 | /** |
||
49 | * Create a new CSV object based on a file. |
||
50 | * |
||
51 | * 1. If a file is being uploaded (i.e. `$_FILES['file']` is set), attempt |
||
52 | * to use it as the CSV file. |
||
53 | * 2. On the otherhand, if we're given a hash, attempt to use this to locate |
||
54 | * a local temporary file. |
||
55 | * |
||
56 | * In either case, if a valid CSV file cannot be found and parsed, throw an |
||
57 | * exception. |
||
58 | * |
||
59 | * @param \WP_Filesystem_Base $filesystem The filesystem object. |
||
60 | * @param string|boolean $hash The hash of an in-progress import, or false. |
||
61 | * @param string[]|boolean $uploaded The result of wp_handle_upload(), or false. |
||
62 | */ |
||
63 | public function __construct( $filesystem, $hash = false, $uploaded = false ) { |
||
64 | $this->filesystem = $filesystem; |
||
65 | if ( $uploaded ) { |
||
66 | $this->save_file( $uploaded ); |
||
67 | } |
||
68 | |||
69 | if ( ! empty( $hash ) ) { |
||
70 | $this->hash = $hash; |
||
71 | } |
||
72 | |||
73 | $this->load_data(); |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * Check the (already-handled) upload and rename the uploaded file. |
||
78 | * |
||
79 | * @see wp_handle_upload() |
||
80 | * @param array $uploaded The array detailing the uploaded file. |
||
81 | * @throws \Exception On upload error or if the file isn't a CSV. |
||
82 | */ |
||
83 | private function save_file( $uploaded ) { |
||
84 | if ( isset( $uploaded['error'] ) ) { |
||
85 | throw new \Exception( $uploaded['error'] ); |
||
86 | } |
||
87 | if ( 'text/csv' !== $uploaded['type'] ) { |
||
88 | unlink( $uploaded['file'] ); |
||
89 | throw new \Exception( 'Only CSV files can be imported.' ); |
||
90 | } |
||
91 | $this->hash = uniqid( TABULATE_SLUG ); |
||
92 | rename( $uploaded['file'], get_temp_dir() . '/' . $this->hash ); |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * Load CSV data from the file identified by the current hash. If no hash is |
||
97 | * set, this method does nothing. |
||
98 | * |
||
99 | * @return void |
||
100 | * @throws \Exception If the hash-identified file doesn't exist. |
||
101 | */ |
||
102 | public function load_data() { |
||
103 | if ( ! $this->hash ) { |
||
104 | return; |
||
105 | } |
||
106 | $file_path = get_temp_dir() . '/' . $this->hash; |
||
107 | if ( ! file_exists( $file_path ) ) { |
||
108 | throw new \Exception( "No import was found with the identifier ‘$this->hash’" ); |
||
109 | } |
||
110 | |||
111 | // Get all rows. |
||
112 | $this->data = array(); |
||
113 | $lines = $this->filesystem->get_contents_array( $file_path ); |
||
114 | foreach ( $lines as $line ) { |
||
115 | $this->data[] = str_getcsv( $line ); |
||
116 | } |
||
117 | |||
118 | // Extract headers. |
||
119 | $this->headers = $this->data[0]; |
||
120 | unset( $this->data[0] ); |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Get the number of data rows in the file (i.e. excluding the header row). |
||
125 | * |
||
126 | * @return integer The number of rows. |
||
127 | */ |
||
128 | public function row_count() { |
||
129 | return count( $this->data ); |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * Whether or not a file has been successfully loaded. |
||
134 | * |
||
135 | * @return boolean |
||
136 | */ |
||
137 | public function loaded() { |
||
138 | return false !== $this->hash; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Take a mapping of DB column name to CSV column name, and convert it to |
||
143 | * a mapping of CSV column number to DB column name. This ignores empty |
||
144 | * column headers in the CSV (so we don't have to distinguish between |
||
145 | * not-matching and matching-on-empty-string). |
||
146 | * |
||
147 | * @param string[] $column_map The map from column headings to indices. |
||
148 | * @return array Keys are CSV indexes, values are DB column names |
||
149 | */ |
||
150 | private function remap( $column_map ) { |
||
151 | $heads = array(); |
||
152 | foreach ( $column_map as $db_col_name => $csv_col_name ) { |
||
153 | foreach ( $this->headers as $head_num => $head_name ) { |
||
154 | // If the header has a name, and it matches that of the column. |
||
155 | if ( ! empty( $head_name ) && 0 === strcasecmp( $head_name, $csv_col_name ) ) { |
||
156 | $heads[ $head_num ] = $db_col_name; |
||
157 | } |
||
158 | } |
||
159 | } |
||
160 | return $heads; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Rename all keys in all data rows to match DB column names, and normalize |
||
165 | * all values to be valid for the `$table`. |
||
166 | * |
||
167 | * If a _value_ in the array matches a lowercased DB column header, the _key_ |
||
168 | * of that value is the DB column name to which that header has been matched. |
||
169 | * |
||
170 | * @param DB\Table $table The table object. |
||
171 | * @param array $column_map Associating the headings to the indices. |
||
172 | * @return array Array of error messages. |
||
173 | */ |
||
174 | public function match_fields( $table, $column_map ) { |
||
175 | // First get the indexes of the headers, including the PK if it's there. |
||
176 | $heads = $this->remap( $column_map ); |
||
177 | $pk_col_num = false; |
||
178 | foreach ( $heads as $head_index => $head_name ) { |
||
179 | if ( $head_name === $table->get_pk_column()->get_name() ) { |
||
180 | $pk_col_num = $head_index; |
||
181 | break; |
||
182 | } |
||
183 | } |
||
184 | |||
185 | // Collect all errors. |
||
186 | $errors = array(); |
||
187 | $row_count = $this->row_count(); |
||
188 | for ( $row_num = 1; $row_num <= $row_count; $row_num++ ) { |
||
189 | $pk_set = $pk_col_num && isset( $this->data[ $row_num ][ $pk_col_num ] ); |
||
190 | foreach ( $this->data[ $row_num ] as $col_num => $value ) { |
||
191 | if ( ! isset( $heads[ $col_num ] ) ) { |
||
192 | continue; |
||
193 | } |
||
194 | $col_errors = array(); |
||
195 | $db_column_name = $heads[ $col_num ]; |
||
196 | $column = $table->get_column( $db_column_name ); |
||
197 | // Required, is not an update, has no default, and is empty. |
||
198 | if ( $column->is_required() && ! $pk_set && ! $column->get_default() && empty( $value ) ) { |
||
199 | $col_errors[] = 'Required but empty'; |
||
200 | } |
||
201 | // Already exists, and is not an update. |
||
202 | if ( $column->is_unique() && ! $pk_set && $this->value_exists( $table, $column, $value ) ) { |
||
203 | $col_errors[] = "Unique value already present: '$value'"; |
||
204 | } |
||
205 | // Too long (if the column has a size and the value is greater than this). |
||
206 | if ( ! $column->is_foreign_key() && ! $column->is_boolean() |
||
207 | && $column->get_size() > 0 |
||
208 | && strlen( $value ) > $column->get_size() ) { |
||
209 | $col_errors[] = 'Value (' . $value . ') too long (maximum length of ' . $column->get_size() . ')'; |
||
210 | } |
||
211 | // Invalid foreign key value. |
||
212 | if ( ! empty( $value ) && $column->is_foreign_key() ) { |
||
213 | $err = $this->validate_foreign_key( $column, $value ); |
||
214 | if ( $err ) { |
||
215 | $col_errors[] = $err; |
||
216 | } |
||
217 | } |
||
218 | // Dates. |
||
219 | View Code Duplication | if ( 'date' === $column->get_type() && ! empty( $value ) && 1 !== preg_match( '/\d{4}-\d{2}-\d{2}/', $value ) ) { |
|
220 | $col_errors[] = 'Value (' . $value . ') not in date format'; |
||
221 | } |
||
222 | View Code Duplication | if ( 'year' === $column->get_type() && ! empty( $value ) && ( $value < 1901 || $value > 2155 ) ) { |
|
223 | $col_errors[] = 'Year values must be between 1901 and 2155 (' . $value . ' given)'; |
||
224 | } |
||
225 | |||
226 | if ( count( $col_errors ) > 0 ) { |
||
227 | // Construct error details array. |
||
228 | $errors[] = array( |
||
229 | 'column_name' => $this->headers[ $col_num ], |
||
230 | 'column_number' => $col_num, |
||
231 | 'field_name' => $column->get_name(), |
||
232 | 'row_number' => $row_num, |
||
233 | 'messages' => $col_errors, |
||
234 | ); |
||
235 | } |
||
236 | }// End foreach(). |
||
0 ignored issues
–
show
|
|||
237 | }// End for(). |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
43% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them.
Loading history...
|
|||
238 | return $errors; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Assume all data is now valid, and only FK values remain to be translated. |
||
243 | * |
||
244 | * @param DB\Table $table The table into which to import data. |
||
245 | * @param array $column_map array of DB names to import names. |
||
246 | * @return integer The number of rows imported. |
||
247 | */ |
||
248 | public function import_data( $table, $column_map ) { |
||
249 | global $wpdb; |
||
250 | $change_tracker = new ChangeTracker( $wpdb ); |
||
251 | $change_tracker->open_changeset( 'CSV import.', true ); |
||
252 | $count = 0; |
||
253 | $headers = $this->remap( $column_map ); |
||
254 | $row_count = $this->row_count(); |
||
255 | for ( $row_num = 1; $row_num <= $row_count; $row_num++ ) { |
||
256 | $row = array(); |
||
257 | foreach ( $this->data[ $row_num ] as $col_num => $value ) { |
||
258 | if ( ! isset( $headers[ $col_num ] ) ) { |
||
259 | continue; |
||
260 | } |
||
261 | $db_column_name = $headers[ $col_num ]; |
||
262 | $column = $table->get_column( $db_column_name ); |
||
263 | |||
264 | // Get actual foreign key value. |
||
265 | if ( $column->is_foreign_key() && ! empty( $value ) ) { |
||
266 | $fk_rows = $this->get_fk_rows( $column->get_referenced_table(), $value ); |
||
267 | $foreign_row = array_shift( $fk_rows ); |
||
268 | $value = $foreign_row->get_primary_key(); |
||
269 | } |
||
270 | |||
271 | // All other values are used as they are. |
||
272 | $row[ $db_column_name ] = $value; |
||
273 | } |
||
274 | |||
275 | $pk_name = $table->get_pk_column()->get_name(); |
||
276 | $pk_value = ( isset( $row[ $pk_name ] ) ) ? $row[ $pk_name ] : null; |
||
277 | $table->save_record( $row, $pk_value ); |
||
278 | $count++; |
||
279 | } |
||
280 | $change_tracker->close_changeset(); |
||
281 | return $count; |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * Determine whether a given value is valid for a foreign key (i.e. is the |
||
286 | * title of a foreign row). |
||
287 | * |
||
288 | * @param DB\Column $column The column to check in. |
||
289 | * @param string $value The value to validate. |
||
290 | * @return false|string False if the value is valid, error message otherwise. |
||
291 | */ |
||
292 | protected function validate_foreign_key( $column, $value ) { |
||
293 | $foreign_table = $column->get_referenced_table(); |
||
294 | if ( ! $this->get_fk_rows( $foreign_table, $value ) ) { |
||
295 | $link = '<a href="' . $foreign_table->get_url() . '" title="Opens in a new tab or window" target="_blank" >' |
||
296 | . $foreign_table->get_title() |
||
297 | . '</a>'; |
||
298 | return "Value <code>$value</code> not found in $link"; |
||
299 | } |
||
300 | return false; |
||
301 | } |
||
302 | |||
303 | /** |
||
304 | * Get the rows of a foreign table where the title column equals a given |
||
305 | * value. |
||
306 | * |
||
307 | * @param DB\Table $foreign_table The table from which to fetch rows. |
||
308 | * @param string $value The value to match against the title column. |
||
309 | * @return Record[] The foreign records. |
||
310 | */ |
||
311 | protected function get_fk_rows( $foreign_table, $value ) { |
||
312 | $foreign_table->reset_filters(); |
||
313 | $foreign_table->add_filter( $foreign_table->get_title_column()->get_name(), '=', $value ); |
||
314 | return $foreign_table->get_records(); |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Determine whether the given value exists. |
||
319 | * |
||
320 | * @param DB\Table $table The table to check in. |
||
321 | * @param DB\Column $column The column to check. |
||
322 | * @param mixed $value The value to look for. |
||
323 | * @return boolean |
||
324 | */ |
||
325 | protected function value_exists( $table, $column, $value ) { |
||
326 | $db = $table->get_database()->get_wpdb(); |
||
327 | $sql = 'SELECT 1 FROM `' . $table->get_name() . '` ' |
||
328 | . 'WHERE `' . $column->get_name() . '` = %s ' |
||
329 | . 'LIMIT 1'; |
||
330 | $exists = $db->get_row( $db->prepare( $sql, array( $value ) ) ); |
||
331 | return ! is_null( $exists ); |
||
332 | } |
||
333 | } |
||
334 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.