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, |
|
|
|
|
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; |
|
|
|
|
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]; |
199
|
|
|
$this->wpdb->query( 'BEGIN' ); |
200
|
|
|
$column_map = json_decode( wp_unslash( $_POST['columns'] ), true ); |
201
|
|
|
$result = $csv_file->import_data( $table, $column_map ); |
|
|
|
|
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 ); |
|
|
|
|
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
|
|
|
|
Since your code implements the magic setter
_set
, this function will be called for any write access on an undefined variable. You can add the@property
annotation to your class or interface to document the existence of this variable.Since the property has write access only, you can use the @property-write 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.