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