ListTableController::setQueryForTable()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 18
ccs 0
cts 10
cp 0
rs 9.5222
cc 5
nc 7
nop 1
crap 30
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_userdata($userId);
33
            if (!$user) {
34
                continue;
35
            }
36
            $name = glsr(Sanitizer::class)->sanitizeUserName(
37
                $user->display_name,
38
                $user->user_nicename
39
            );
40
            if (!glsr()->can('edit_post', $postId) && glsr()->can('respond_to_post', $postId)) {
41
                $send = [
42
                    'text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name),
43
                ];
44
                if (get_option('show_avatars')) {
45
                    $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
46
                    $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
47
                }
48
                $checked[$key] = $send;
49
            }
50
            if (glsr()->can('edit_post', $postId)) {
51
                continue;
52
            }
53
            if (!glsr()->can('respond_to_post', $postId)) {
54
                continue;
55
            }
56
            $send = ['text' => sprintf(_x('%s is currently editing', 'admin-text', 'site-reviews'), $name)];
57
            if (get_option('show_avatars')) {
58
                $send['avatar_src'] = get_avatar_url($user->ID, ['size' => 18]);
59
                $send['avatar_src_2x'] = get_avatar_url($user->ID, ['size' => 36]);
60
            }
61
            $checked[$key] = $send;
62
        }
63
        if (!empty($checked)) {
64
            $response['wp-check-locked-posts'] = $checked;
65
        }
66
        return $response;
67
    }
68
69
    /**
70
     * @param string[] $columns
71
     *
72
     * @filter manage_{glsr()->post_type}_posts_columns
73
     */
74
    public function filterColumnsForPostType(array $columns): array
75
    {
76
        $postTypeColumns = glsr()->retrieveAs('array', 'columns.'.glsr()->post_type, []);
77
        foreach ($postTypeColumns as $key => &$value) {
78
            if (array_key_exists($key, $columns) && empty($value)) {
79
                $value = $columns[$key];
80
            }
81
        }
82
        return array_filter($postTypeColumns, 'strlen'); // @phpstan-ignore-line
83
    }
84
85
    /**
86
     * @filter post_date_column_status
87
     */
88
    public function filterDateColumnStatus(string $status, \WP_Post $post): string
89
    {
90
        if (glsr()->post_type === $post->post_type) {
91
            return _x('Submitted', 'admin-text', 'site-reviews');
92
        }
93
        return $status;
94
    }
95
96
    /**
97
     * @param string[] $hidden
98
     *
99
     * @filter default_hidden_columns
100
     */
101
    public function filterDefaultHiddenColumns(array $hidden, \WP_Screen $screen): array
102
    {
103
        if ('edit-'.glsr()->post_type === $screen->id) {
104
            $hiddenColumns = glsr()->retrieveAs('array', 'columns_hidden.'.glsr()->post_type, []);
105
            return array_unique(array_merge($hidden, $hiddenColumns));
106
        }
107
        return $hidden;
108
    }
109
110
    /**
111
     * @filter wp_list_table_class_name
112
     */
113
    public function filterListTableClass(string $className): string
114
    {
115
        $screen = glsr_current_screen();
116
        if (glsr()->post_type !== $screen->post_type) {
117
            return $className;
118
        }
119
        if ('edit' !== $screen->base) {
120
            return $className;
121
        }
122
        return ReviewsListTable::class;
123
    }
124
125
    /**
126
     * @filter posts_clauses
127
     */
128
    public function filterPostClauses(array $postClauses, \WP_Query $query): array
129
    {
130
        if (!$this->hasQueryPermission($query)) {
131
            return $postClauses;
132
        }
133
        $clauses = array_fill_keys(array_keys($postClauses), []);
134
        if ($this->isListFiltered() || $this->isListOrdered()) {
135
            foreach ($postClauses as $key => $clause) {
136
                $method = Helper::buildMethodName('modifyClause', $key);
137
                if (method_exists($this, $method)) {
138
                    $clauses[$key] = call_user_func([$this, $method], $clause, $query);
139
                }
140
            }
141
        }
142
        $clauses = glsr()->filterArray('review-table/clauses', $clauses, $postClauses, $query);
143
        foreach ($clauses as $key => $clause) {
144
            $clause = glsr(SqlClauseDefaults::class)->restrict($clause);
145
            if (empty($clause['clauses'])) {
146
                continue;
147
            }
148
            $values = array_values(array_unique($clause['clauses']));
149
            $values = implode(' ', $values);
150
            if (!$clause['replace']) {
151
                $values = trim($postClauses[$key])." {$values}";
152
            }
153
            $postClauses[$key] = " {$values} ";
154
        }
155
        return $postClauses;
156
    }
157
158
    /**
159
     * @param string[] $actions
160
     *
161
     * @filter post_row_actions
162
     */
163
    public function filterRowActions(array $actions, \WP_Post $post): array
164
    {
165
        if (glsr()->post_type !== Arr::get($post, 'post_type') || 'trash' === $post->post_status) {
166
            return $actions;
167
        }
168
        unset($actions['inline hide-if-no-js']);
169
        $baseurl = admin_url("post.php?post={$post->ID}&plugin=".glsr()->id);
170
        $newActions = ['id' => sprintf('<span>ID: %d</span>', $post->ID)];
171
        if (glsr()->can('publish_post', $post->ID)) {
172
            $newActions['approve'] = glsr(Builder::class)->a([
173
                'aria-label' => esc_attr(_x('Approve this review', 'admin-text', 'site-reviews')),
174
                'class' => 'glsr-toggle-status',
175
                'href' => wp_nonce_url(add_query_arg('action', 'approve', $baseurl), "approve-review_{$post->ID}"),
176
                'text' => _x('Approve', 'admin-text', 'site-reviews'),
177
            ]);
178
            $newActions['unapprove'] = glsr(Builder::class)->a([
179
                'aria-label' => esc_attr(_x('Unapprove this review', 'admin-text', 'site-reviews')),
180
                'class' => 'glsr-toggle-status',
181
                'href' => wp_nonce_url(add_query_arg('action', 'unapprove', $baseurl), "unapprove-review_{$post->ID}"),
182
                'text' => _x('Unapprove', 'admin-text', 'site-reviews'),
183
            ]);
184
        }
185
        if (glsr()->can('respond_to_post', $post->ID)) {
186
            $newActions['respond hide-if-no-js'] = glsr(Builder::class)->button([
187
                'aria-expanded' => false,
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_userdata($last);
277
            if ($user) {
278
                $name = glsr(Sanitizer::class)->sanitizeUserName(
279
                    $user->display_name,
280
                    $user->user_nicename
281
                );
282
            }
283
            $message = esc_html_x('Saving is disabled: %s is currently editing this review.', 'admin-text', 'site-reviews');
284
            printf($message, $name);
285
            wp_die();
286
        }
287
        $response = (string) filter_input(INPUT_POST, '_response');
288
        glsr(ReviewManager::class)->updateResponse($postId, compact('response'));
289
        glsr()->action('cache/flush', glsr_get_review($postId));
290
        global $mode;
291
        $mode = Str::restrictTo(['excerpt', 'list'], (string) filter_input(INPUT_POST, 'post_view'), 'list');
292
        $table = new ReviewsListTable(['screen' => convert_to_screen($screen)]);
293
        $table->display_rows([get_post($postId)], 0);
294
        wp_die();
295
    }
296
297
    /**
298
     * @action restrict_manage_posts
299
     */
300
    public function renderColumnFilters(string $postType): void
301
    {
302
        if (glsr()->post_type === $postType) {
303
            $filters = glsr(ListtableFiltersDefaults::class)->defaults();
304
            $enabledFilters = Arr::consolidate(
305
                get_user_meta(get_current_user_id(), 'edit_'.glsr()->post_type.'_filters', true)
306
            );
307
            foreach ($filters as $filter) {
308
                echo Cast::toString(glsr()->runIf($filter, $enabledFilters));
309
            }
310
        }
311
    }
312
313
    /**
314
     * @action manage_{glsr()->post_type}_posts_custom_column
315
     */
316
    public function renderColumnValues(string $column, int $postId): void
317
    {
318
        $review = glsr(ReviewManager::class)->get((int) $postId);
319
        if (!$review->isValid()) {
320
            glsr(Migrate::class)->reset(); // looks like a migration is needed!
321
            return;
322 28
        }
323
        $className = Helper::buildClassName(['ColumnValue', $column], 'Controllers\ListTableColumns');
324 28
        $className = glsr()->filterString("column/{$column}", $className);
325 28
        $value = glsr()->runIf($className, $review);
326
        $value = glsr()->filterString("columns/{$column}", $value, $postId);
327
        echo Helper::ifEmpty($value, '&mdash;');
328
    }
329
330
    /**
331
     * @action pre_get_posts
332
     */
333
    public function setQueryForTable(\WP_Query $query): void
334
    {
335
        if (!$this->hasQueryPermission($query)) {
336
            return;
337
        }
338
        $orderby = $query->get('orderby');
339
        if ('response' === $orderby) {
340
            $query->set('meta_key', Str::prefix($orderby, '_'));
341
            $query->set('orderby', 'meta_value');
342
        }
343
        if ($termId = filter_input(INPUT_GET, 'category', FILTER_SANITIZE_NUMBER_INT)) {
344
            $taxQuery = ['taxonomy' => glsr()->taxonomy];
345
            if (-1 === Cast::toInt($termId)) {
346
                $taxQuery['operator'] = 'NOT EXISTS';
347
            } else {
348
                $taxQuery['terms'] = $termId;
349
            }
350
            $query->set('tax_query', [$taxQuery]);
351
        }
352
    }
353
354
    protected function filterByValues(): array
355
    {
356
        $filterBy = glsr(ColumnFilterbyDefaults::class)->defaults();
357
        $filterBy = filter_input_array(INPUT_GET, $filterBy);
358
        return Arr::removeEmptyValues(Arr::consolidate($filterBy));
359
    }
360
361
    protected function isListFiltered(): bool
362
    {
363
        return !empty($this->filterByValues());
364
    }
365
366
    protected function isListOrdered(): bool
367
    {
368
        $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
369
        $column = Cast::toString(get_query_var('orderby')); // get_query_var output is unpredictable
370
        return array_key_exists($column, $columns);
371
    }
372
373
    protected function isOrderbyWithIsNull(string $column): bool
374
    {
375
        $columns = [
376
            'email', 'name', 'ip_address', 'type',
377
        ];
378
        $columns = glsr()->filterArray('columns/orderby-is-null', $columns);
379
        return in_array($column, $columns);
380
    }
381
382
    protected function modifyClauseJoin(string $join, \WP_Query $query): array
383
    {
384
        $clause = glsr(SqlClauseDefaults::class)->restrict([
385
            'clauses' => [],
386
            'replace' => false,
387
        ]);
388
        $posts = glsr(Tables::class)->table('posts');
389
        $ratings = glsr(Tables::class)->table('ratings');
390
        $clause['clauses'][] = "INNER JOIN {$ratings} ON ({$ratings}.review_id = {$posts}.ID)";
391
        foreach ($this->filterByValues() as $key => $value) {
392
            if (!in_array($key, ['assigned_post', 'assigned_user'])) {
393
                continue;
394
            }
395
            $assignedTable = glsr(Tables::class)->table($key.'s');
396
            $value = Cast::toInt($value);
397
            if (0 === $value) {
398
                $clause['clauses'][] = "LEFT JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
399
            } else {
400
                $clause['clauses'][] = "INNER JOIN {$assignedTable} ON ({$assignedTable}.rating_id = {$ratings}.ID)";
401
            }
402
        }
403
        return $clause;
404
    }
405
406
    protected function modifyClauseOrderby(string $orderby, \WP_Query $query): array
407
    {
408
        $clause = glsr(SqlClauseDefaults::class)->restrict([
409
            'clauses' => [],
410
            'replace' => true,
411
        ]);
412
        $columns = glsr(ColumnOrderbyDefaults::class)->defaults();
413
        $column = Arr::get($columns, $query->get('orderby'));
414
        if (empty($column)) {
415
            return $clause;
416
        }
417
        $ratings = glsr(Tables::class)->table('ratings');
418
        if ($this->isOrderbyWithIsNull($column)) {
419
            $clause['clauses'][] = "NULLIF({$ratings}.{$column}, '') IS NULL, {$orderby}";
420
        } else {
421
            $clause['clauses'][] = "{$ratings}.{$column} {$query->get('order')}";
422
        }
423
        return $clause;
424
    }
425
426
    protected function modifyClauseWhere(string $where, \WP_Query $query): array
427
    {
428
        $clause = glsr(SqlClauseDefaults::class)->restrict([
429
            'clauses' => [],
430
            'replace' => false,
431
        ]);
432
        $ratings = glsr(Tables::class)->table('ratings');
433
        $posts = glsr(Tables::class)->table('posts');
434
        foreach ($this->filterByValues() as $key => $value) {
435
            if (in_array($key, ['assigned_post', 'assigned_user'])) {
436
                $assignedTable = glsr(Tables::class)->table($key.'s');
437
                $column = Str::suffix(Str::removePrefix($key, 'assigned_'), '_id');
438
                $value = Cast::toInt($value);
439
                if (0 === $value) {
440
                    $clause['clauses'][] = "AND {$assignedTable}.{$column} IS NULL";
441
                } else {
442
                    $clause['clauses'][] = "AND {$assignedTable}.{$column} = {$value}";
443
                }
444
            } elseif (in_array($key, ['rating', 'terms', 'type'])) {
445
                $clause['clauses'][] = "AND {$ratings}.{$key} = '{$value}'";
446
            } elseif ('author' === $key && '0' === $value) {
447
                // Filtering by the "author" URL parameter is automatically done
448
                // by WordPress when the value is not empty
449
                $clause['clauses'][] = "AND {$posts}.post_author IN (0)";
450
            }
451
        }
452
        return $clause;
453
    }
454
}
455