Issues (65)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/CSV.php (7 issues)

Upgrade to new PHP Analysis Engine

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 );
0 ignored issues
show
It seems like $uploaded defined by parameter $uploaded on line 63 can also be of type boolean; however, WordPress\Tabulate\CSV::save_file() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
67
		}
68
69
		if ( ! empty( $hash ) ) {
70
			$this->hash = $hash;
0 ignored issues
show
Documentation Bug introduced by
It seems like $hash can also be of type boolean. However, the property $hash is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 &lsquo;$this->hash&rsquo;" );
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 ) ) {
0 ignored issues
show
It seems like $column defined by $table->get_column($db_column_name) on line 196 can also be of type false; however, WordPress\Tabulate\CSV::value_exists() does only seem to accept object<WordPress\Tabulate\DB\Column>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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 );
0 ignored issues
show
It seems like $column defined by $table->get_column($db_column_name) on line 196 can also be of type false; however, WordPress\Tabulate\CSV::validate_foreign_key() does only seem to accept object<WordPress\Tabulate\DB\Column>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
214
					if ( $err ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $err of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
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...
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