Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.62% covered (warning)
67.62%
71 / 105
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
BulkEdit
67.31% covered (warning)
67.31%
70 / 104
50.00% covered (danger)
50.00%
4 / 8
74.39
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 bulkEditCustomBox
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 saveBulkEdit
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
9.03
 generateAudioForPosts
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 deleteAudioForPosts
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 bulkActionsEdit
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 handleBulkGenerateAction
88.00% covered (success)
88.00%
22 / 25
0.00% covered (danger)
0.00%
0 / 1
6.06
 handleBulkDeleteAction
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5/**
6 * BeyondWords Bulk Edit.
7 *
8 * Text Domain: beyondwords
9 *
10 * @package Beyondwords\Wordpress
11 * @author  Stuart McAlpine <stu@beyondwords.io>
12 * @since   3.0.0
13 */
14
15namespace Beyondwords\Wordpress\Component\Posts\BulkEdit;
16
17use Beyondwords\Wordpress\Core\Core;
18use Beyondwords\Wordpress\Core\CoreUtils;
19use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
20use Beyondwords\Wordpress\Plugin;
21
22/**
23 * BulkEdit
24 *
25 * @since 3.0.0
26 */
27defined('ABSPATH') || exit;
28
29class BulkEdit
30{
31    /**
32     * Init.
33     *
34     * @since 4.0.0
35     * @since 6.0.0 Make static.
36     */
37    public static function init()
38    {
39        add_action('bulk_edit_custom_box', [self::class, 'bulkEditCustomBox'], 10, 2);
40        add_action('wp_ajax_save_bulk_edit_beyondwords', [self::class, 'saveBulkEdit']);
41
42        add_action('wp_loaded', function (): void {
43            $postTypes = SettingsUtils::getCompatiblePostTypes();
44
45            if (is_array($postTypes)) {
46                foreach ($postTypes as $postType) {
47                    add_filter("bulk_actions-edit-{$postType}", [self::class, 'bulkActionsEdit'], 10, 1);
48                    add_filter("handle_bulk_actions-edit-{$postType}", [self::class, 'handleBulkDeleteAction'], 10, 3); // phpcs:ignore Generic.Files.LineLength.TooLong
49                    add_filter("handle_bulk_actions-edit-{$postType}", [self::class, 'handleBulkGenerateAction'], 10, 3); // phpcs:ignore Generic.Files.LineLength.TooLong
50                }
51            }
52        });
53    }
54
55    /**
56     * Adds the meta box container.
57     *
58     * @since 6.0.0 Make static.
59     */
60    public static function bulkEditCustomBox($columnName, $postType)
61    {
62        if ($columnName !== 'beyondwords') {
63            return;
64        }
65
66        $postTypes = SettingsUtils::getCompatiblePostTypes();
67
68        if (! in_array($postType, $postTypes)) {
69            return;
70        }
71
72        wp_nonce_field('beyondwords_bulk_edit_nonce', 'beyondwords_bulk_edit');
73
74        ?>
75        <fieldset class="inline-edit-col-right">
76            <div class="inline-edit-col">
77                <div class="inline-edit-group wp-clearfix">
78                    <label class="alignleft">
79                        <span class="title"><?php esc_html_e('BeyondWords', 'speechkit'); ?></span>
80                        <select name="beyondwords_generate_audio">
81                            <option value="-1"><?php esc_html_e('— No change —', 'speechkit'); ?></option>
82                            <option value="generate"><?php esc_html_e('Generate audio', 'speechkit'); ?></option>
83                            <option value="delete"><?php esc_html_e('Delete audio', 'speechkit'); ?></option>
84                        </select>
85                    </label>
86                </div>
87            </div>
88        </fieldset>
89        <?php
90    }
91
92    /**
93     * Save Bulk Edit updates.
94     *
95     * @link https://rudrastyh.com/wordpress/bulk-edit.html
96     *
97     * @since 6.0.0 Make static.
98     */
99    public static function saveBulkEdit()
100    {
101        /*
102         * We need to verify this came from the our screen and with proper authorization,
103         * because save_post can be triggered at other times.
104         */
105        if (
106            ! isset($_POST['beyondwords_bulk_edit_nonce']) ||
107            ! wp_verify_nonce(sanitize_key($_POST['beyondwords_bulk_edit_nonce']), 'beyondwords_bulk_edit')
108        ) {
109            wp_nonce_ays('');
110        }
111
112        if (! isset($_POST['beyondwords_bulk_edit']) || ! isset($_POST['post_ids'])) {
113            return [];
114        }
115
116        if (is_array($_POST['post_ids']) && count($_POST['post_ids'])) {
117            $postIds = array_map('intval', $_POST['post_ids']);
118            $postIds = array_filter($postIds);
119
120            switch ($_POST['beyondwords_bulk_edit']) {
121                case 'generate':
122                    return self::generateAudioForPosts($postIds);
123                case 'delete':
124                    return self::deleteAudioForPosts($postIds);
125            }
126        }
127
128        return [];
129    }
130
131    /**
132     * Generate audio for posts.
133     *
134     * @since 6.0.0 Make static.
135     */
136    public static function generateAudioForPosts(array|null $postIds): array
137    {
138        if (! is_array($postIds)) {
139            return [];
140        }
141
142        $updatedPostIds = [];
143
144        foreach ($postIds as $postId) {
145            if (! get_post_meta($postId, 'beyondwords_content_id', true)) {
146                update_post_meta($postId, 'beyondwords_generate_audio', '1');
147            }
148            $updatedPostIds[] = $postId;
149        }
150
151        return $updatedPostIds;
152    }
153
154    /**
155     * Delete audio for posts.
156     *
157     * @since 6.0.0 Make static.
158     */
159    public static function deleteAudioForPosts(array|null $postIds): array
160    {
161        if (! is_array($postIds)) {
162            return [];
163        }
164
165        $updatedPostIds = [];
166
167        $response = Core::batchDeleteAudioForPosts($postIds);
168
169        if (! $response) {
170            throw new \Exception(esc_html__('Error while bulk deleting audio. Please contact support with reference BULK-NO-RESPONSE.', 'speechkit')); // phpcs:ignore Generic.Files.LineLength.TooLong
171        }
172
173        // Now process all posts
174        $keys = CoreUtils::getPostMetaKeys('all');
175
176        foreach ($response as $postId) {
177            foreach ($keys as $key) {
178                delete_post_meta($postId, $key);
179            }
180            $updatedPostIds[] = $postId;
181        }
182
183        return $updatedPostIds;
184    }
185
186    /**
187     * Add custom bulk actions to the posts list table.
188     *
189     * @since 6.0.0 Make static.
190     */
191    public static function bulkActionsEdit($bulk_array)
192    {
193        $bulk_array['beyondwords_generate_audio'] = __('Generate audio', 'speechkit');
194        $bulk_array['beyondwords_delete_audio']   = __('Delete audio', 'speechkit');
195
196        return $bulk_array;
197    }
198
199    /**
200     * Handle the "Generate audio" bulk action.
201     *
202     * @since 6.0.0 Make static.
203     */
204    public static function handleBulkGenerateAction($redirect, $doaction, $objectIds)
205    {
206        if ($doaction !== 'beyondwords_generate_audio') {
207            return $redirect;
208        }
209
210        // Remove query args
211        $args = [
212            'beyondwords_bulk_generated',
213            'beyondwords_bulk_deleted',
214            'beyondwords_bulk_failed',
215            'beyondwords_bulk_error',
216        ];
217
218        $redirect = remove_query_arg($args, $redirect);
219
220        // Order batch by Post ID asc
221        sort($objectIds);
222
223        $generated = 0;
224        $failed    = 0;
225
226        try {
227            // Update all custom fields before attempting any processing
228            foreach ($objectIds as $postId) {
229                update_post_meta($postId, 'beyondwords_generate_audio', '1');
230            }
231
232            // Now process all posts
233            foreach ($objectIds as $postId) {
234                $response = Core::generateAudioForPost($postId);
235
236                if ($response) {
237                    $generated++;
238                } else {
239                    $failed++;
240                }
241            }
242        } catch (\Exception $e) {
243            $redirect = add_query_arg('beyondwords_bulk_error', $e->getMessage(), $redirect);
244        }
245
246        // Add $generated & $failed query args into redirect
247        $redirect = add_query_arg('beyondwords_bulk_generated', $generated, $redirect);
248        $redirect = add_query_arg('beyondwords_bulk_failed', $failed, $redirect);
249
250        // Add nonce to redirect url
251        $nonce = wp_create_nonce('beyondwords_bulk_edit_result');
252
253        return add_query_arg('beyondwords_bulk_edit_result_nonce', $nonce, $redirect);
254    }
255
256    /**
257     * Handle the "Delete audio" bulk action.
258     *
259     * @since 6.0.0 Make static.
260     */
261    public static function handleBulkDeleteAction($redirect, $doaction, $objectIds)
262    {
263        if ($doaction !== 'beyondwords_delete_audio') {
264            return $redirect;
265        }
266
267        // Remove query args
268        $args = [
269            'beyondwords_bulk_generated',
270            'beyondwords_bulk_deleted',
271            'beyondwords_bulk_failed',
272            'beyondwords_bulk_error',
273        ];
274
275        $redirect = remove_query_arg($args, $redirect);
276
277        // Order batch by Post ID asc
278        sort($objectIds);
279
280        $deleted = 0;
281
282        // Handle "Delete audio" bulk action
283        try {
284            $result = self::deleteAudioForPosts($objectIds);
285
286            $deleted = count($result);
287
288            // Add $deleted query arg into redirect
289            $redirect = add_query_arg('beyondwords_bulk_deleted', $deleted, $redirect);
290        } catch (\Exception $e) {
291            $redirect = add_query_arg('beyondwords_bulk_error', $e->getMessage(), $redirect);
292        }
293
294        // Add $nonce query arg into redirect
295        $nonce = wp_create_nonce('beyondwords_bulk_edit_result');
296
297        return add_query_arg('beyondwords_bulk_edit_result_nonce', $nonce, $redirect);
298    }
299}