PluginProbe
Site Reviews / trunk
Site Reviews vtrunk
8.3.1 8.3.0 8.2.2 8.2.1 8.2.0 8.1.0 8.0.13 8.0.12 8.0.11 trunk 1.2.2 2.17.1 3.5.4 4.7.0 5.25.1 6.11.8 7.0.10 7.0.11 7.0.12 7.0.13 7.0.14 7.0.15 7.0.16 7.0.17 7.0.18 All 54 releases
site-reviews / plugin / Controllers / ListTableController.php

ListTableController.php in Site Reviews trunk, at plugin/Controllers/ListTableController.php

454 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GeminiLabs\SiteReviews\Controllers;
4
5 use GeminiLabs\SiteReviews\Database\ReviewManager;
6 use GeminiLabs\SiteReviews\Database\Tables;
7 use GeminiLabs\SiteReviews\Defaults\ColumnFilterbyDefaults;
8 use GeminiLabs\SiteReviews\Defaults\ColumnOrderbyDefaults;
9 use GeminiLabs\SiteReviews\Defaults\ListtableFiltersDefaults;
10 use GeminiLabs\SiteReviews\Defaults\SqlClauseDefaults;
11 use GeminiLabs\SiteReviews\Helper;
12 use GeminiLabs\SiteReviews\Helpers\Arr;
13 use GeminiLabs\SiteReviews\Helpers\Cast;
14 use GeminiLabs\SiteReviews\Helpers\Str;
15 use GeminiLabs\SiteReviews\Modules\Html\Builder;
16 use GeminiLabs\SiteReviews\Modules\Migrate;
17 use GeminiLabs\SiteReviews\Modules\Sanitizer;
18 use GeminiLabs\SiteReviews\Overrides\ReviewsListTable;
19
20 class ListTableController extends AbstractController
21 {
22 /**
23 * @filter heartbeat_received
24 */
25 public function filterCheckLockedReviews(array $response, array $data): array
26 {
27 $checked = [];
28 $postIds = Arr::consolidate(Arr::get($data, 'wp-check-locked-posts'));
29 foreach ($postIds as $key) {
30 $postId = absint(substr($key, 5));
31 $userId = (int) wp_check_post_lock($postId);
32 $user = get_user_by('id', $userId);
33 if (!$user) {
34 continue;
35 }
36 $name = glsr(Sanitizer::class)->sanitizeUserName($user);
37 if (!glsr()->can('edit_post', $postId) && glsr()->can('respond_to_post', $postId)) {
38 $send = [
39 /* translators: %s: user's name */
40 'text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name),
41 ];
42 if (get_option('show_avatars')) {
43 $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
44 $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
45 }
46 $checked[$key] = $send;
47 }
48 if (glsr()->can('edit_post', $postId)) {
49 continue;
50 }
51 if (!glsr()->can('respond_to_post', $postId)) {
52 continue;
53 }
54 /* translators: %s: user's name */
55 $send = ['text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name)];
56 if (get_option('show_avatars')) {
57 $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
58 $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
59 }
60 $checked[$key] = $send;
61 }
62 if (!empty($checked)) {
63 $response['wp-check-locked-posts'] = $checked;
64 }
65 return $response;
66 }
67
68 /**
69 * @param string[] $columns
70 *
71 * @filter manage_{glsr()->post_type}_posts_columns
72 */
73 public function filterColumnsForPostType(array $columns): array
74 {
75 $postTypeColumns = glsr()->retrieveAs('array', 'columns.'.glsr()->post_type, []);
76 foreach ($postTypeColumns as $key => &$value) {
77 if (array_key_exists($key, $columns) && empty($value)) {
78 $value = $columns[$key];
79 }
80 }
81 return array_filter($postTypeColumns, 'strlen');
82 }
83
84 /**
85 * @filter post_date_column_status
86 */
87 public function filterDateColumnStatus(string $status, \WP_Post $post): string
88 {
89 if (glsr()->post_type === $post->post_type) {
90 return _x('Submitted', 'admin-text', 'site-reviews');
91 }
92 return $status;
93 }
94
95 /**
96 * @param string[] $hidden
97 *
98 * @filter default_hidden_columns
99 */
100 public function filterDefaultHiddenColumns(array $hidden, \WP_Screen $screen): array
101 {
102 if ('edit-'.glsr()->post_type === $screen->id) {
103 $hiddenColumns = glsr()->retrieveAs('array', 'columns_hidden.'.glsr()->post_type, []);
104 return array_unique(array_merge($hidden, $hiddenColumns));
105 }
106 return $hidden;
107 }
108
109 /**
110 * @filter wp_list_table_class_name
111 */
112 public function filterListTableClass(string $className): string
113 {
114 $screen = glsr_current_screen();
115 if (glsr()->post_type !== $screen->post_type) {
116 return $className;
117 }
118 if ('edit' !== $screen->base) {
119 return $className;
120 }
121 return ReviewsListTable::class;
122 }
123
124 /**
125 * @filter posts_clauses
126 */
127 public function filterPostClauses(array $postClauses, \WP_Query $query): array
128 {
129 if (!$this->hasQueryPermission($query)) {
130 return $postClauses;
131 }
132 $clauses = array_fill_keys(array_keys($postClauses), []);
133 if ($this->isListFiltered() || $this->isListOrdered()) {
134 foreach ($postClauses as $key => $clause) {
135 $method = Helper::buildMethodName('modifyClause', $key);
136 if (method_exists($this, $method)) {
137 $clauses[$key] = call_user_func([$this, $method], $clause, $query);
138 }
139 }
140 }
141 $clauses = glsr()->filterArray('review-table/clauses', $clauses, $postClauses, $query);
142 foreach ($clauses as $key => $clause) {
143 $clause = glsr(SqlClauseDefaults::class)->restrict($clause);
144 if (empty($clause['clauses'])) {
145 continue;
146 }
147 $values = array_values(array_unique($clause['clauses']));
148 $values = implode(' ', $values);
149 if (!$clause['replace']) {
150 $values = trim($postClauses[$key])." {$values}";
151 }
152 $postClauses[$key] = " {$values} ";
153 }
154 return $postClauses;
155 }
156
157 /**
158 * @param string[] $actions
159 *
160 * @filter post_row_actions
161 */
162 public function filterRowActions(array $actions, \WP_Post $post): array
163 {
164 if (glsr()->post_type !== $post->post_type || 'trash' === $post->post_status) {
165 return $actions;
166 }
167 unset($actions['inline hide-if-no-js']);
168 $baseurl = admin_url("post.php?post={$post->ID}&plugin=".glsr()->id);
169 $newActions = ['id' => sprintf('<span>ID: %d</span>', $post->ID)];
170 if (glsr()->can('publish_post', $post->ID)) {
171 $newActions['approve'] = glsr(Builder::class)->a([
172 'aria-label' => esc_attr(_x('Approve this review', 'admin-text', 'site-reviews')),
173 'class' => 'glsr-toggle-status',
174 'href' => wp_nonce_url(add_query_arg('action', 'approve', $baseurl), "approve-review_{$post->ID}"),
175 'text' => _x('Approve', 'admin-text', 'site-reviews'),
176 ]);
177 $newActions['unapprove'] = glsr(Builder::class)->a([
178 'aria-label' => esc_attr(_x('Unapprove this review', 'admin-text', 'site-reviews')),
179 'class' => 'glsr-toggle-status',
180 'href' => wp_nonce_url(add_query_arg('action', 'unapprove', $baseurl), "unapprove-review_{$post->ID}"),
181 'text' => _x('Unapprove', 'admin-text', 'site-reviews'),
182 ]);
183 }
184 if (glsr()->can('respond_to_post', $post->ID)) {
185 $newActions['respond hide-if-no-js'] = glsr(Builder::class)->button([
186 'aria-expanded' => false,
187 /* translators: %s: review title */
188 'aria-label' => esc_attr(sprintf(_x('Respond inline to &#8220;%s&#8221;', 'admin-text', 'site-reviews'), _draft_or_post_title())),
189 'class' => 'button-link editinline',
190 'text' => _x('Respond', 'admin-text', 'site-reviews'),
191 'type' => 'button',
192 ]);
193 }
194 return $newActions + $actions;
195 }
196
197 /**
198 * @filter screen_settings
199 */
200 public function filterScreenFilters(?string $settings, \WP_Screen $screen): string
201 {
202 if ('edit-'.glsr()->post_type === $screen->id) {
203 $userId = get_current_user_id();
204 $filters = glsr(ListtableFiltersDefaults::class)->defaults();
205 if (count(glsr()->retrieveAs('array', 'review_types')) < 2) {
206 unset($filters['type']);
207 }
208 foreach ($filters as $key => &$value) {
209 $value = glsr($value)->title();
210 }
211 ksort($filters);
212 $setting = 'edit_'.glsr()->post_type.'_filters';
213 $enabled = get_user_meta($userId, $setting, true);
214 if (!is_array($enabled)) {
215 $enabled = ['rating']; // the default enabled filters
216 update_user_meta($userId, $setting, $enabled);
217 }
218 $settings .= glsr()->build('partials/screen/filters', [
219 'enabled' => $enabled,
220 'filters' => $filters,
221 'setting' => $setting,
222 ]);
223 }
224 return (string) $settings;
225 }
226
227 /**
228 * @action posts_search
229 */
230 public function filterSearchQuery(string $search, \WP_Query $query): string
231 {
232 if (!$this->hasQueryPermission($query)) {
233 return $search;
234 }
235 if (!is_numeric($query->get('s')) || empty($search)) {
236 return $search;
237 }
238 global $wpdb;
239 $replace = $wpdb->prepare("{$wpdb->posts}.ID = %d", $query->get('s'));
240 return str_replace('AND (((', "AND ((({$replace}) OR (", $search);
241 }
242
243 /**
244 * @filter manage_edit-{glsr()->post_type}_sortable_columns
245 */
246 public function filterSortableColumns(array $columns): array
247 {
248 $postTypeColumns = glsr()->retrieveAs('array', 'columns.'.glsr()->post_type, []);
249 unset($postTypeColumns['cb']);
250 foreach ($postTypeColumns as $key => $value) {
251 if (!Str::startsWith($key, ['assigned', 'taxonomy'])) {
252 $columns[$key] = $key;
253 }
254 }
255 return $columns;
256 }
257
258 /**
259 * @action wp_ajax_inline_save
260 */
261 public function overrideInlineSaveAjax(): void
262 {
263 $screen = filter_input(\INPUT_POST, 'screen');
264 if ('edit-'.glsr()->post_type !== $screen) {
265 return; // don't override
266 }
267 check_ajax_referer('inlineeditnonce', '_inline_edit');
268 if (empty($postId = filter_input(\INPUT_POST, 'post_ID', \FILTER_VALIDATE_INT))) {
269 wp_die();
270 }
271 if (!glsr()->can('respond_to_post', $postId)) {
272 wp_die(_x('Sorry, you are not allowed to respond to this review.', 'admin-text', 'site-reviews'));
273 }
274 if ($last = wp_check_post_lock($postId)) {
275 $name = _x('Someone', 'admin-text', 'site-reviews');
276 $user = get_user_by('id', $last);
277 if ($user) {
278 $name = glsr(Sanitizer::class)->sanitizeUserName($user);
279 }
280 /* translators: %s: user's name */
281 $message = esc_html_x('Saving is disabled: %s is currently editing this review.', 'admin-text', 'site-reviews');
282 printf($message, $name);
283 wp_die();
284 }
285 $response = (string) filter_input(\INPUT_POST, '_response');
286 glsr(ReviewManager::class)->updateResponse($postId, compact('response'));
287 $review = glsr_get_review($postId);
288 glsr()->action('cache/flush', "review_{$review->ID}_responded", $review);
289 global $mode;
290 $mode = Str::restrictTo(['excerpt', 'list'], (string) filter_input(\INPUT_POST, 'post_view'), 'list');
291 $table = new ReviewsListTable(['screen' => convert_to_screen($screen)]);
292 $table->display_rows([get_post($postId)], 0);
293 wp_die();
294 }
295
296 /**
297 * @action restrict_manage_posts
298 */
299 public function renderColumnFilters(string $postType): void
300 {
301 if (glsr()->post_type === $postType) {
302 $filters = glsr(ListtableFiltersDefaults::class)->defaults();
303 $enabledFilters = Arr::consolidate(
304 get_user_meta(get_current_user_id(), 'edit_'.glsr()->post_type.'_filters', true)
305 );
306 foreach ($filters as $filter) {
307 echo Cast::toString(glsr()->runIf($filter, $enabledFilters));
308 }
309 }
310 }
311
312 /**
313 * @action manage_{glsr()->post_type}_posts_custom_column
314 */
315 public function renderColumnValues(string $column, int $postId): void
316 {
317 $review = glsr(ReviewManager::class)->get((int) $postId);
318 if (!$review->isValid()) {
319 glsr(Migrate::class)->reset(); // looks like a migration is needed!
320 return;
321 }
322 $className = Helper::buildClassName(['ColumnValue', $column], 'Controllers\ListTableColumns');
323 $className = glsr()->filterString("column/{$column}", $className);
324 $value = glsr()->runIf($className, $review);
325 $value = glsr()->filterString("columns/{$column}", $value, $postId);
326 echo Helper::ifEmpty($value, '&mdash;');
327 }
328
329 /**
330 * @action pre_get_posts
331 */
332 public function setQueryForTable(\WP_Query $query): void
333 {
334 if (!$this->hasQueryPermission($query)) {
335 return;
336 }
337 $orderby = $query->get('orderby');
338 if ('response' === $orderby) {
339 $query->set('meta_key', Str::prefix($orderby, '_'));
340 $query->set('orderby', 'meta_value');
341 }
342 if ($termId = filter_input(\INPUT_GET, 'category', \FILTER_SANITIZE_NUMBER_INT)) {
343 $taxQuery = ['taxonomy' => glsr()->taxonomy];
344 if (-1 === Cast::toInt($termId)) {
345 $taxQuery['operator'] = 'NOT EXISTS';
346 } else {
347 $taxQuery['terms'] = $termId;
348 }
349 $query->set('tax_query', [$taxQuery]);
350 }
351 }
352
353 protected function filterByValues(): array
354 {
355 $filterBy = glsr(ColumnFilterbyDefaults::class)->defaults();
356 $filterBy = filter_input_array(\INPUT_GET, $filterBy);
357 return Arr::removeEmptyValues(Arr::consolidate($filterBy));
358 }
359
360 protected function isListFiltered(): bool
361 {
362 return !empty($this->filterByValues());
363 }
364
365 protected function isListOrdered(): bool
366 {
367 $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
368 $column = Cast::toString(get_query_var('orderby')); // get_query_var output is unpredictable
369 return array_key_exists($column, $columns);
370 }
371
372 protected function isOrderbyWithIsNull(string $column): bool
373 {
374 $columns = [
375 'email', 'name', 'ip_address', 'type',
376 ];
377 $columns = glsr()->filterArray('columns/orderby-is-null', $columns);
378 return in_array($column, $columns);
379 }
380
381 protected function modifyClauseJoin(string $join, \WP_Query $query): array
382 {
383 $clause = glsr(SqlClauseDefaults::class)->restrict([
384 'clauses' => [],
385 'replace' => false,
386 ]);
387 $posts = glsr(Tables::class)->table('posts');
388 $ratings = glsr(Tables::class)->table('ratings');
389 $clause['clauses'][] = "INNER JOIN {$ratings} ON ({$ratings}.review_id = {$posts}.ID)";
390 foreach ($this->filterByValues() as $key => $value) {
391 if (!in_array($key, ['assigned_post', 'assigned_user'])) {
392 continue;
393 }
394 $assignedTable = glsr(Tables::class)->table($key.'s');
395 $value = Cast::toInt($value);
396 if (0 === $value) {
397 $clause['clauses'][] = "LEFT JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
398 } else {
399 $clause['clauses'][] = "INNER JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
400 }
401 }
402 return $clause;
403 }
404
405 protected function modifyClauseOrderby(string $orderby, \WP_Query $query): array
406 {
407 $clause = glsr(SqlClauseDefaults::class)->restrict([
408 'clauses' => [],
409 'replace' => true,
410 ]);
411 $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
412 $column = Arr::get($columns, $query->get('orderby'));
413 if (empty($column)) {
414 return $clause;
415 }
416 $ratings = glsr(Tables::class)->table('ratings');
417 if ($this->isOrderbyWithIsNull($column)) {
418 $clause['clauses'][] = "NULLIF({$ratings}.{$column}, '') IS NULL, {$orderby}";
419 } else {
420 $clause['clauses'][] = "{$ratings}.{$column} {$query->get('order')}";
421 }
422 return $clause;
423 }
424
425 protected function modifyClauseWhere(string $where, \WP_Query $query): array
426 {
427 $clause = glsr(SqlClauseDefaults::class)->restrict([
428 'clauses' => [],
429 'replace' => false,
430 ]);
431 $ratings = glsr(Tables::class)->table('ratings');
432 $posts = glsr(Tables::class)->table('posts');
433 foreach ($this->filterByValues() as $key => $value) {
434 if (in_array($key, ['assigned_post', 'assigned_user'])) {
435 $assignedTable = glsr(Tables::class)->table($key.'s');
436 $column = Str::suffix(Str::removePrefix($key, 'assigned_'), '_id');
437 $value = Cast::toInt($value);
438 if (0 === $value) {
439 $clause['clauses'][] = "AND {$assignedTable}.{$column} IS NULL";
440 } else {
441 $clause['clauses'][] = "AND {$assignedTable}.{$column} = {$value}";
442 }
443 } elseif (in_array($key, ['rating', 'terms', 'type'])) {
444 $clause['clauses'][] = "AND {$ratings}.{$key} = '{$value}'";
445 } elseif ('author' === $key && '0' === $value) {
446 // Filtering by the "author" URL parameter is automatically done
447 // by WordPress when the value is not empty
448 $clause['clauses'][] = "AND {$posts}.post_author IN (0)";
449 }
450 }
451 return $clause;
452 }
453 }
454