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, |
||
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
|
|||
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 ); |
||
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 ); |
||
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 |
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:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.