Completed
Push — master ( 6cab57...287bd7 )
by Sam
02:34
created

TableController::timeline()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 27
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
rs 8.439
cc 6
eloc 22
nc 24
nop 1
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
Documentation introduced by
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
Bug introduced by
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
Documentation introduced by
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
Bug introduced by
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
Documentation introduced by
$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