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/Controllers/TableController.php (5 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 a single class.
4
 *
5
 * @file
6
 * @package Tabulate
7
 */
8
9
namespace WordPress\Tabulate\Controllers;
10
11
use \WordPress\Tabulate\Util;
12
use \WordPress\Tabulate\DB\Grants;
13
use \WordPress\Tabulate\DB\Database;
14
use \WordPress\Tabulate\DB\Table;
15
use \WordPress\Tabulate\CSV;
16
17
/**
18
 * The table controller handles viewing, exporting, and importing table data.
19
 */
20
class TableController extends ControllerBase {
21
22
	/**
23
	 * Get a Table object for a given table, or an error message and the
24
	 * Tabulate overview page.
25
	 *
26
	 * @param string $table_name The name of the table to get.
27
	 * @return Table|string The table, or an HTML error message.
28
	 */
29
	protected function get_table( $table_name ) {
30
		$db = new Database( $this->wpdb );
31
		$db->set_filesystem( $this->filesystem );
32
		$table = $db->get_table( $table_name );
33
		if ( ! $table ) {
34
			add_action( 'admin_notices', function() use ( $table_name ) {
35
				// Translators: Error message shown when the table can not be found.
36
				$err = __( 'Table "%s" not found.', 'tabulate' );
37
				echo "<div class='error'><p>" . sprintf( $err, $table_name ) . "</p></div>";
38
			} );
39
			$home = new HomeController( $this->wpdb );
40
			return $home->index();
41
		}
42
		return $table;
43
	}
44
45
	/**
46
	 * View and search a table's data.
47
	 *
48
	 * @param string[] $args The request arguments.
49
	 * @return string
50
	 */
51
	public function index( $args ) {
52
		$table = $this->get_table( $args['table'] );
53
		if ( ! $table instanceof Table ) {
54
			return $table;
55
		}
56
57
		// Pagination.
58
		$page_num = (isset( $args['p'] ) && is_numeric( $args['p'] ) ) ? abs( $args['p'] ) : 1;
59
		$table->set_current_page_num( $page_num );
60
		if ( isset( $args['psize'] ) ) {
61
			$table->set_records_per_page( $args['psize'] );
62
		}
63
64
		// Ordering.
65
		if ( isset( $args['order_by'] ) ) {
66
			$table->set_order_by( $args['order_by'] );
67
		}
68
		if ( isset( $args['order_dir'] ) ) {
69
			$table->set_order_dir( $args['order_dir'] );
70
		}
71
72
		// Filters.
73
		$filter_param = (isset( $args['filter'] )) ? $args['filter'] : array();
74
		$table->add_filters( $filter_param );
75
76
		// Give it all to the template.
77
		$template = new \WordPress\Tabulate\Template( 'table.html' );
78
		$template->controller = 'table';
79
		$template->table = $table;
80
		$template->columns = $table->get_columns();
81
		$template->sortable = true;
82
		$template->record = $table->get_default_record();
83
		$template->records = $table->get_records();
84
		return $template->render();
85
	}
86
87
	/**
88
	 * This action is for importing a single CSV file into a single database table.
89
	 * It guides the user through the four stages of importing:
90
	 * uploading, field matching, previewing, and doing the actual import.
91
	 * All of the actual work is done in [WebDB_File_CSV].
92
	 *
93
	 * 1. In the first stage, a CSV file is **uploaded**, validated, and moved to a temporary directory.
94
	 *    The file is then accessed from this location in the subsequent stages of importing,
95
	 *    and only deleted upon either successful import or the user cancelling the process.
96
	 *    (The 'id' parameter of this action is the identifier for the uploaded file.)
97
	 * 2. Once a valid CSV file has been uploaded,
98
	 *    its colums are presented to the user to be **matched** to those in the database table.
99
	 *    The columns from the database are presented first and the CSV columns are matched to these,
100
	 *    rather than vice versa,
101
	 *    because this way the user sees immediately what columns are available to be imported into.
102
	 * 3. The column matches are then used to produce a **preview** of what will be added to and/or changed in the database.
103
	 *    All columns from the database are shown (regardless of whether they were in the import) and all rows of the import.
104
	 *    If a column is not present in the import the database will (obviously) use the default value if there is one;
105
	 *    this will be shown in the preview.
106
	 * 4. When the user accepts the preview, the actual **import** of data is carried out.
107
	 *    Rows are saved to the database using the usual Table::save() method
108
	 *    and a message presented to the user to indicate successful completion.
109
	 *
110
	 * @param string[] $args The request parameters.
111
	 * @return string
112
	 */
113
	public function import( $args ) {
114
		$template = new \WordPress\Tabulate\Template( 'import.html' );
115
		// Set up the progress bar.
116
		$template->stages = array(
117
			'choose_file',
118
			'match_fields',
119
			'preview',
120
			'complete_import',
121
		);
122
		$template->stage = 'choose_file';
123
124
		// First make sure the user is allowed to import data into this table.
125
		$table = $this->get_table( $args['table'] );
126
		$template->record = $table->get_default_record();
127
		$template->action = 'import';
128
		$template->table = $table;
129
		$template->maxsize = size_format( wp_max_upload_size() );
130
		if ( ! Grants::current_user_can( Grants::IMPORT, $table->get_name() ) ) {
131
			$template->add_notice( 'error', 'You do not have permission to import data into this table.' );
132
			return $template->render();
133
		}
134
135
		/*
136
		 * Stage 1 of 4: Uploading.
137
		 */
138
		require_once ABSPATH . '/wp-admin/includes/file.php';
139
		$template->form_action = $table->get_url( 'import' );
140
		try {
141
			$hash = isset( $_GET['hash'] ) ? $_GET['hash'] : false;
142
			$uploaded = false;
143
			if ( isset( $_FILES['file'] ) ) {
144
				check_admin_referer( 'import-upload' );
145
				$uploaded = wp_handle_upload( $_FILES['file'], array(
146
					'action' => $template->action,
0 ignored issues
show
The property action does not exist on object<WordPress\Tabulate\Template>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
147
				) );
148
			}
149
			$csv_file = new CSV( $this->filesystem, $hash, $uploaded );
150
		} catch ( \Exception $e ) {
151
			$template->add_notice( 'error', $e->getMessage() );
152
			return $template->render();
153
		}
154
155
		/*
156
		 * Stage 2 of 4: Matching fields.
157
		 */
158
		if ( $csv_file->loaded() ) {
159
			$template->file = $csv_file;
160
			$template->stage = $template->stages[1];
161
			$template->form_action .= "&hash=" . $csv_file->hash;
162
		}
163
164
		/*
165
		 * Stage 3 of 4: Previewing.
166
		 */
167
		if ( $csv_file->loaded() && isset( $_POST['preview'] ) ) {
168
			check_admin_referer( 'import-preview' );
169
			$template->stage = $template->stages[2];
170
			$template->columns = wp_json_encode( $_POST['columns'] );
171
			$errors = array();
172
			// Make sure all required columns are selected.
173
			foreach ( $table->get_columns() as $col ) {
174
				// Handle missing columns separately; other column errors are
175
				// done in the CSV class. Missing columns don't matter if importing
176
				// existing records.
177
				$is_missing = empty( $_POST['columns'][ $col->get_name() ] );
178
				$pk = $table->get_pk_column();
179
				$pk_present = $pk && isset( $_POST['columns'][ $pk->get_name() ] );
180
				if ( ! $pk_present && $col->is_required() && $is_missing ) {
181
					$errors[] = array(
182
						'column_name' => '',
183
						'column_number' => '',
184
						'field_name' => $col->get_name(),
185
						'row_number' => 'N/A',
186
						'messages' => array( 'Column required, but not found in CSV' ),
187
					);
188
				}
189
			}
190
			$template->errors = empty( $errors ) ? $csv_file->match_fields( $table, wp_unslash( $_POST['columns'] ) ) : $errors;
0 ignored issues
show
It seems like $table defined by $this->get_table($args['table']) on line 125 can also be of type string; however, WordPress\Tabulate\CSV::match_fields() does only seem to accept object<WordPress\Tabulate\DB\Table>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
191
		}
192
193
		/*
194
		 * Stage 4 of 4: Import.
195
		 */
196
		if ( $csv_file->loaded() && isset( $_POST['import'] ) ) {
197
			check_admin_referer( 'import-finish' );
198
			$template->stage = $template->stages[3];
0 ignored issues
show
The property stages does not exist on object<WordPress\Tabulate\Template>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
199
			$this->wpdb->query( 'BEGIN' );
200
			$column_map = json_decode( wp_unslash( $_POST['columns'] ), true );
201
			$result = $csv_file->import_data( $table, $column_map );
0 ignored issues
show
It seems like $table defined by $this->get_table($args['table']) on line 125 can also be of type string; however, WordPress\Tabulate\CSV::import_data() does only seem to accept object<WordPress\Tabulate\DB\Table>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
202
			$this->wpdb->query( 'COMMIT' );
203
			$template->add_notice( 'updated', 'Import complete; ' . $result . ' rows imported.' );
204
		}
205
206
		return $template->render();
207
	}
208
209
	/**
210
	 * A calendar for tables with a date column.
211
	 *
212
	 * @param string[] $args The request parameters.
213
	 * @return string The calendar HTML.
214
	 */
215
	public function calendar( $args ) {
216
		// @todo Validate args.
217
		$year_num = (isset( $args['year'] )) ? $args['year'] : date( 'Y' );
218
		$month_num = (isset( $args['month'] )) ? $args['month'] : date( 'm' );
219
220
		$template = new \WordPress\Tabulate\Template( 'calendar.html' );
221
		$table = $this->get_table( $args['table'] );
222
223
		$template->table = $table;
224
		$template->action = 'calendar';
225
		$template->record = $table->get_default_record();
226
227
		$factory = new \CalendR\Calendar();
228
		$template->weekdays = $factory->getWeek( new \DateTime( 'Monday this week' ) );
229
		$month = $factory->getMonth( new \DateTime( $year_num . '-' . $month_num . '-01' ) );
230
		$template->month = $month;
231
		$records = array();
232
		foreach ( $table->get_columns( 'date' ) as $date_col ) {
233
			$date_col_name = $date_col->get_name();
234
			// Filter to the just the requested month.
235
			$table->add_filter( $date_col_name, '>=', $month->getBegin()->format( 'Y-m-d' ) );
236
			$table->add_filter( $date_col_name, '<=', $month->getEnd()->format( 'Y-m-d' ) );
237
			foreach ( $table->get_records() as $rec ) {
238
				$date_val = $rec->$date_col_name();
239
				// Initialise the day's list of records.
240
				if ( ! isset( $records[ $date_val ] ) ) {
241
					$records[ $date_val ] = array();
242
				}
243
				// Add this record to the day's list.
244
				$records[ $date_val ][] = $rec;
245
			}
246
		}
247
		// $records is grouped by date, with each item in a single date being
248
		// an array with 'record' and 'column' keys.
249
		$template->records = $records;
250
251
		return $template->render();
252
	}
253
254
	/**
255
	 * Export the current table with the current filters applied.
256
	 * Filters are passed as request parameters, just as for the index action.
257
	 *
258
	 * @param string[] $args The request parameters.
259
	 * @return void
260
	 */
261
	public function export( $args ) {
262
		// Get table.
263
		$table = $this->get_table( $args['table'] );
264
265
		// Filter and export.
266
		$filter_param = ( isset( $args['filter'] )) ? $args['filter'] : array();
267
		$table->add_filters( $filter_param );
0 ignored issues
show
$filter_param is of type string|array, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
268
		$filename = $table->export();
269
270
		// Send CSV to client.
271
		$download_name = date( 'Y-m-d' ) . '_' . $table->get_name() . '.csv';
272
		header( 'Content-Encoding: UTF-8' );
273
		header( 'Content-type: text/csv; charset=UTF-8' );
274
		header( 'Content-Disposition: attachment; filename="' . $download_name . '"' );
275
		echo "\xEF\xBB\xBF";
276
		echo $table->get_database()->get_filesystem()->get_contents( $filename );
277
		exit;
278
	}
279
280
	/**
281
	 * Download a CSV of given titles that could not be found in this table.
282
	 *
283
	 * @param string[] $args The request parameters.
284
	 */
285
	public function notfound( $args ) {
286
		// Get table.
287
		$table = $this->get_table( $args['table'] );
288
289
		// Get the values from the request, or give up.
290
		$filter_id = isset( $args['notfound'] ) ? $args['notfound'] : false;
291
		$values_string = isset( $args['filter'][ $filter_id ]['value'] ) ? $args['filter'][ $filter_id ]['value'] : false;
292
		if ( ! $table instanceof Table || ! $values_string ) {
293
			return;
294
		}
295
		$values = Util::split_newline( $values_string );
296
297
		// Find all values that exist.
298
		$title_col = $table->get_title_column();
299
		$table->add_filter( $title_col, 'in', $values_string );
300
301
		// And remove them from the list of supplied values.
302
		$recs = $table->get_records( false );
303
		foreach ( $recs as $rec ) {
304
			$key = array_search( $rec->get_title(), $values, true );
305
			if ( false !== $key ) {
306
				unset( $values[ $key ] );
307
			}
308
		}
309
310
		$download_name = date( 'Y-m-d' ) . '_' . $table->get_name() . '_not_found.csv';
311
		header( 'Content-Encoding: UTF-8' );
312
		header( 'Content-type: text/csv; charset=UTF-8' );
313
		header( 'Content-Disposition: attachment; filename="' . $download_name . '"' );
314
		echo "\xEF\xBB\xBF";
315
		echo $title_col->get_title() . "\n" . join( "\n", $values );
316
		exit;
317
	}
318
319
	/**
320
	 * Display a horizontal timeline of any table with a date field.
321
	 *
322
	 * @param string[] $args Request arguments.
323
	 * @return string
324
	 */
325
	public function timeline( $args ) {
326
		$table = $this->get_table( $args['table'] );
327
		$template = new \WordPress\Tabulate\Template( 'timeline.html' );
328
		$template->action = 'timeline';
329
		$template->table = $table;
330
		$start_date_arg = (isset( $args['start_date'] )) ? $args['start_date'] : date( 'Y-m-d' );
331
		$end_date_arg = (isset( $args['end_date'] )) ? $args['end_date'] : date( 'Y-m-d' );
332
		$start_date = new \DateTime( $start_date_arg );
333
		$end_date = new \DateTime( $end_date_arg );
334
		if ( $start_date->diff( $end_date, true )->d < 7 ) {
335
			// Add two weeks to the end date.
336
			$end_date->add( new \DateInterval( 'P14D' ) );
337
		}
338
		$date_period = new \DatePeriod( $start_date, new \DateInterval( 'P1D' ), $end_date );
339
		$template->start_date = $start_date->format( 'Y-m-d' );
340
		$template->end_date = $end_date->format( 'Y-m-d' );
341
		$template->date_period = $date_period;
342
		$data = array();
343
		foreach ( $table->get_records( false ) as $record ) {
344
			if ( ! isset( $data[ $record->get_title() ] ) ) {
345
				$data[ $record->get_title() ] = array();
346
			}
347
			$data[ $record->get_title() ][] = $record;
348
		}
349
		$template->data = $data;
350
		return $template->render();
351
	}
352
}
353