PluginProbe
Hash Form – Drag & Drop Form Builder / trunk
Hash Form – Drag & Drop Form Builder vtrunk
1.4.4 1.4.3 1.4.2 1.4.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.6.1 1.2.7 1.2.8 1.2.9 1.3.0 All 47 releases
hash-form / includes / HashFormListActions.php

HashFormListActions.php in Hash Form – Drag & Drop Form Builder trunk, at includes/HashFormListActions.php

414 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined('ABSPATH') || die();
4
5 /**
6 * Shared list-screen behaviour for the Forms and Entries admin pages.
7 *
8 * Both screens route the same query arguments through the same trash, untrash,
9 * delete and bulk flows; only the table, the request keys and the wording
10 * differ. Holding the flow in one place is what keeps the nonce checks on the
11 * two screens from drifting apart, which is how they came to be missing on the
12 * bulk paths in the first place.
13 *
14 * Message text stays in the using class: every gettext call must keep a literal
15 * string so the translation scanner can find it.
16 */
17 trait HashFormListActions {
18
19 /**
20 * Per-screen settings.
21 *
22 * @return array {
23 * @type string $page Admin page slug, e.g. 'hashform'.
24 * @type string $table Table name without the $wpdb prefix.
25 * @type string $id_key Request key holding the bulk ids, e.g. 'form_id'.
26 * @type string $nonce_item Infix used by single item nonces, e.g. 'form'.
27 * @type string $bulk_nonce Nonce action guarding the list table form.
28 * @type string[] $actions Single item actions this screen dispatches by name.
29 * }
30 */
31 abstract protected static function list_config();
32
33 /** Permanently remove one item and everything hanging off it. */
34 abstract protected static function destroy_item($id);
35
36 /** Render the screen, optionally with a notice. */
37 abstract protected static function render_list($message = '', $class = 'updated');
38
39 abstract protected static function message_trashed($count, $undo_open, $undo_close);
40
41 abstract protected static function message_untrashed($count);
42
43 /** Notice for a single permanent deletion. */
44 abstract protected static function message_destroyed($count);
45
46 /** Notice for bulk and empty-trash permanent deletions. */
47 abstract protected static function message_deleted($count);
48
49 abstract protected static function message_none_specified();
50
51 /**
52 * The capability a list action needs, or '' when the screen does not say.
53 *
54 * @param string $action One of the keys in the screen's 'caps' config.
55 * @return string
56 */
57 protected static function list_cap($action) {
58 $config = static::list_config();
59
60 if (empty($config['caps'][$action])) {
61 return '';
62 }
63
64 return $config['caps'][$action];
65 }
66
67 /**
68 * Stop a list action the current user is not allowed to take.
69 *
70 * The menu capability only decides who reaches the screen. Without this,
71 * anyone who could open the Forms or Entries list could also trash and
72 * permanently delete from it: the rows print their own action links, so
73 * the nonce those links carry was the only thing standing in the way, and
74 * a nonce proves who is asking, not what they may do.
75 *
76 * @param string $action
77 */
78 protected static function require_list_cap($action) {
79 $cap = static::list_cap($action);
80
81 if ('' === $cap || HashFormCapabilities::user_can($cap)) {
82 return;
83 }
84
85 wp_die(
86 esc_html__('You do not have permission to do that.', 'hash-form'),
87 esc_html__('Permission denied', 'hash-form'),
88 array('response' => 403)
89 );
90 }
91
92 public static function route() {
93 $config = static::list_config();
94
95 /* Gets hashform_action value else action value */
96 $action = htmlspecialchars_decode(HashFormHelper::get_var('hashform_action', 'sanitize_text_field', HashFormHelper::get_var('action')));
97
98 // The bulk dropdown below the table posts as action2, and core sends -1
99 // for whichever of the two the user did not submit.
100 if ('' === $action || '-1' === $action) {
101 $action = htmlspecialchars_decode(HashFormHelper::get_var('action2', 'sanitize_text_field'));
102 }
103
104 if (HashFormHelper::get_var('delete_all')) {
105 $action = 'delete_all';
106 }
107
108 // $action reaches a method name below, so only the values this screen
109 // declares may be dispatched.
110 if (in_array($action, $config['actions'], true)) {
111 return static::$action();
112 }
113
114 if (strpos($action, 'bulk_') === 0) {
115 static::bulk_actions();
116 return;
117 }
118
119 static::render_list();
120 }
121
122 /**
123 * Would route() fall through to the list table for this request?
124 *
125 * The header bar is printed on in_admin_header, which fires before the
126 * page callback runs, so it has to work out for itself whether the list
127 * is what is about to render. Mirrors the dispatch above: anything the
128 * screen declares as an action goes somewhere else, everything else ends
129 * on the list.
130 */
131 public static function is_list_view() {
132 $config = static::list_config();
133
134 if (!HashFormHelper::is_admin_page($config['page'])) {
135 return false;
136 }
137
138 $action = htmlspecialchars_decode(HashFormHelper::get_var('hashform_action', 'sanitize_text_field', HashFormHelper::get_var('action')));
139
140 if ('' === $action || '-1' === $action) {
141 $action = htmlspecialchars_decode(HashFormHelper::get_var('action2', 'sanitize_text_field'));
142 }
143
144 return !in_array($action, $config['actions'], true);
145 }
146
147 /**
148 * Admin notices, moved inside the screen's own wrapper.
149 *
150 * Core prints them into #wpbody-content before the page callback runs
151 * (wp-admin/admin-header.php), which puts them above and outside
152 * .hf-content.hf-list-screen and off the measure the rest of the screen
153 * lines up to. They are buffered from before the first notice hook to
154 * after the last, then re-emitted by print_notices() inside the wrapper.
155 */
156 private static $notice_html = '';
157 private static $buffering = false;
158
159 public static function buffer_notices() {
160 if (!static::is_list_view()) {
161 return;
162 }
163
164 self::$buffering = true;
165 ob_start();
166 }
167
168 public static function capture_notices() {
169 // Only ever closes a buffer this class opened, so an early exit
170 // somewhere else cannot leave output swallowed.
171 if (!self::$buffering) {
172 return;
173 }
174
175 self::$buffering = false;
176 self::$notice_html = ob_get_clean();
177 }
178
179 /**
180 * Whatever core and other plugins printed, already escaped by them.
181 */
182 public static function print_notices() {
183 if ('' === self::$notice_html) {
184 return;
185 }
186
187 echo self::$notice_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
188 self::$notice_html = '';
189 }
190
191 public static function display_message($message, $class) {
192 if ('' !== trim($message)) {
193 echo '<div id="message" class="' . esc_attr($class) . ' notice is-dismissible">';
194 echo '<p>' . wp_kses_post($message) . '</p>';
195 echo '</div>';
196 }
197 }
198
199 public static function trash() {
200 self::change_form_status('trash');
201 }
202
203 public static function untrash() {
204 self::change_form_status('untrash');
205 }
206
207 public static function change_form_status($status) {
208 $available_status = array(
209 'untrash' => array('new_status' => 'published'),
210 'trash' => array('new_status' => 'trash'),
211 );
212
213 if (!isset($available_status[$status])) {
214 return;
215 }
216
217 static::require_list_cap('delete');
218
219 $config = static::list_config();
220 $id = HashFormHelper::get_var('id', 'absint');
221
222 check_admin_referer($status . '_' . $config['nonce_item'] . '_' . $id);
223
224 $count = 0;
225 if (static::set_status($id, $available_status[$status]['new_status'])) {
226 $count++;
227 }
228
229 if ('untrash' === $status) {
230 $message = static::message_untrashed($count);
231 } else {
232 $undo_url = wp_nonce_url(
233 '?page=' . $config['page'] . '&hashform_action=untrash&id=' . absint($id),
234 'untrash_' . $config['nonce_item'] . '_' . absint($id)
235 );
236 $message = static::message_trashed($count, '<a href="' . esc_url($undo_url) . '">', '</a>');
237 }
238
239 static::render_list($message);
240 }
241
242 public static function set_status($id, $status) {
243 $statuses = array('published', 'trash');
244 if (!in_array($status, $statuses)) {
245 return false;
246 }
247
248 $id = is_array($id) ? $id : array($id);
249
250 // An empty list would build "IN ()" and error out.
251 if (!$id) {
252 return false;
253 }
254
255 global $wpdb;
256
257 $config = static::list_config();
258 $table = $wpdb->prefix . $config['table'];
259 $placeholders = implode(',', array_fill(0, count($id), '%d'));
260 $prepare_args = array_merge(array($status), $id);
261
262 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is $wpdb->prefix plus a table name from the subclass's own list_config(), and $placeholders is a string of %d markers whose values are bound through $prepare_args.
263 return $wpdb->query($wpdb->prepare("UPDATE {$table} SET status=%s WHERE id IN ({$placeholders})", $prepare_args));
264 }
265
266 public static function delete_all() {
267 static::require_list_cap('delete');
268
269 $config = static::list_config();
270
271 // The "Empty Trash" button submits inside the list table form, which
272 // carries the bulk nonce.
273 check_admin_referer($config['bulk_nonce']);
274
275 $count = static::delete();
276 static::render_list(static::message_deleted($count));
277 }
278
279 public static function delete() {
280 global $wpdb;
281
282 $config = static::list_config();
283 $table = $wpdb->prefix . $config['table'];
284 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is $wpdb->prefix plus a table name from the subclass's own list_config(); the one value is bound.
285 $trashed = $wpdb->get_col($wpdb->prepare("SELECT id FROM {$table} WHERE status=%s", 'trash'));
286
287 if (!$trashed) {
288 return 0;
289 }
290
291 $count = 0;
292 foreach ($trashed as $id) {
293 static::destroy_item($id);
294 $count++;
295 }
296
297 return $count;
298 }
299
300 public static function destroy() {
301 static::require_list_cap('delete');
302
303 $config = static::list_config();
304 $id = HashFormHelper::get_var('id', 'absint');
305
306 check_admin_referer('destroy_' . $config['nonce_item'] . '_' . $id);
307
308 $count = 0;
309 if (static::destroy_item($id)) {
310 $count++;
311 }
312
313 static::render_list(static::message_destroyed($count));
314 }
315
316 public static function bulk_actions() {
317 $message = static::process_bulk_actions();
318 static::render_list($message);
319 }
320
321 public static function process_bulk_actions() {
322 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a presence test only; check_admin_referer() runs three lines down, before anything is read.
323 if (!$_REQUEST) {
324 return;
325 }
326
327 $config = static::list_config();
328
329 check_admin_referer($config['bulk_nonce']);
330
331 $bulkaction = HashFormHelper::get_var('action', 'sanitize_text_field');
332
333 if ($bulkaction == -1) {
334 $bulkaction = HashFormHelper::get_var('action2', 'sanitize_title');
335 }
336
337 if (!empty($bulkaction) && strpos($bulkaction, 'bulk_') === 0) {
338 $bulkaction = str_replace('bulk_', '', $bulkaction);
339 }
340
341 $ids = HashFormHelper::get_var($config['id_key'], 'sanitize_text_field');
342
343 if (empty($ids)) {
344 return static::message_none_specified();
345 }
346
347 if (!is_array($ids)) {
348 $ids = explode(',', $ids);
349 }
350
351 $ids = array_filter(array_map('absint', $ids));
352
353 if (!$ids) {
354 return static::message_none_specified();
355 }
356
357 $message = '';
358
359 switch ($bulkaction) {
360 case 'delete':
361 static::require_list_cap('delete');
362 $message = static::bulk_destroy($ids);
363 break;
364 case 'trash':
365 static::require_list_cap('delete');
366 $message = static::bulk_trash($ids);
367 break;
368 case 'untrash':
369 static::require_list_cap('delete');
370 $message = static::bulk_untrash($ids);
371 }
372
373 if (!empty($message)) {
374 return $message;
375 }
376 }
377
378 public static function bulk_trash($ids) {
379 $count = static::set_status($ids, 'trash');
380 if (!$count) {
381 return '';
382 }
383
384 $config = static::list_config();
385 $undo_url = wp_nonce_url(
386 '?page=' . $config['page'] . '&action=bulk_untrash&status=published&' . $config['id_key'] . '=' . implode(',', $ids),
387 $config['bulk_nonce']
388 );
389
390 return static::message_trashed($count, '<a href="' . esc_url($undo_url) . '">', '</a>');
391 }
392
393 public static function bulk_untrash($ids) {
394 $count = static::set_status($ids, 'published');
395 if (!$count) {
396 return '';
397 }
398
399 return static::message_untrashed($count);
400 }
401
402 public static function bulk_destroy($ids) {
403 $count = 0;
404 foreach ($ids as $id) {
405 if (static::destroy_item($id)) {
406 $count++;
407 }
408 }
409
410 return static::message_deleted($count);
411 }
412
413 }
414