• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / duplicate-post / 14725616456

29 Apr 2025 07:21AM UTC coverage: 45.469% (-4.7%) from 50.122%
14725616456

push

github

web-flow
Merge pull request #402 from Yoast/feature/drop-php-7.2-7.3

Drop support for Php 7.2 and 7.3

1164 of 2560 relevant lines covered (45.47%)

1.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.29
/src/admin/options-form-generator.php
1
<?php
2

3
namespace Yoast\WP\Duplicate_Post\Admin;
4

5
use WP_Taxonomy;
6
use Yoast\WP\Duplicate_Post\Utils;
7

8
/**
9
 * Class Options_Form_Generator.
10
 */
11
class Options_Form_Generator {
12

13
        /**
14
         * The Options_Inputs instance.
15
         *
16
         * @var Options_Inputs
17
         */
18
        protected $options_inputs;
19

20
        /**
21
         * Options_Form_Generator constructor.
22
         *
23
         * @param Options_Inputs $inputs The Options_Inputs instance.
24
         */
25
        public function __construct( Options_Inputs $inputs ) {
2✔
26
                $this->options_inputs = $inputs;
2✔
27
        }
28

29
        /**
30
         * Generates the HTML output of an input control, based on the passed options.
31
         *
32
         * @param array  $options       The options to base the input on.
33
         * @param string $parent_option The parent option, used for grouped inputs. Optional.
34
         *
35
         * @return string The HTML output.
36
         */
37
        public function generate_options_input( array $options, $parent_option = '' ) {
10✔
38
                $output = '';
10✔
39

40
                foreach ( $options as $option => $option_values ) {
10✔
41
                        // Skip empty options.
42
                        if ( empty( $option_values ) ) {
10✔
43
                                continue;
2✔
44
                        }
45

46
                        // Check for support of the current WordPress version.
47
                        if ( \array_key_exists( 'version', $option_values ) && \version_compare( \get_bloginfo( 'version' ), $option_values['version'] ) < 0 ) {
8✔
48
                                continue;
2✔
49
                        }
50

51
                        if ( \array_key_exists( 'sub_options', $option_values ) ) {
8✔
52
                                $output .= $this->generate_options_input( $option_values['sub_options'], $option );
2✔
53

54
                                continue;
2✔
55
                        }
56

57
                        // If callback, call it.
58
                        if ( \array_key_exists( 'callback', $option_values ) ) {
8✔
59
                                $output .= $this->{$option_values['callback']}();
2✔
60

61
                                continue;
2✔
62
                        }
63

64
                        if ( ! \array_key_exists( 'type', $option_values ) ) {
8✔
65
                                continue;
×
66
                        }
67

68
                        $id = ( \array_key_exists( 'id', $option_values ) ? $option_values['id'] : $this->prepare_input_id( $option ) );
8✔
69

70
                        if ( $parent_option !== '' ) {
8✔
71
                                $id     = \sprintf( '%s-%s', $this->prepare_input_id( $parent_option ), $id );
2✔
72
                                $option = \sprintf( '%s[%s]', $parent_option, $option );
2✔
73
                        }
74

75
                        switch ( $option_values['type'] ) {
8✔
76
                                case 'checkbox':
8✔
77
                                        $output .= $this->options_inputs->checkbox(
6✔
78
                                                $option,
6✔
79
                                                $option_values['value'],
6✔
80
                                                $id,
6✔
81
                                                $this->is_checked( $option, $option_values, $parent_option )
6✔
82
                                        );
6✔
83

84
                                        $output .= \sprintf( '<label for="%s">%s</label>', $id, \esc_html( $option_values['label'] ) );
6✔
85
                                        break;
6✔
86
                                case 'text':
6✔
87
                                        $output .= $this->options_inputs->text( $option, $option_values['value'], $id );
6✔
88
                                        break;
6✔
89
                                case 'number':
2✔
90
                                        $output .= $this->options_inputs->number( $option, $option_values['value'], $id );
2✔
91

92
                                        break;
2✔
93
                        }
94

95
                        if ( \array_key_exists( 'description', $option_values ) ) {
8✔
96
                                $output .= ' ' . $this->extract_description( $option_values['description'], $id );
4✔
97
                        }
98

99
                        $output .= '<br />';
8✔
100
                }
101

102
                return $output;
10✔
103
        }
104

105
        /**
106
         * Sorts taxonomy objects based on being public, followed by being private
107
         * and when the visibility is equal, on the taxonomy public name (case-sensitive).
108
         *
109
         * @param WP_Taxonomy $taxonomy1 First taxonomy object.
110
         * @param WP_Taxonomy $taxonomy2 Second taxonomy object.
111
         *
112
         * @return int An integer less than, equal to, or greater than zero indicating respectively
113
         *             the first taxonomy should be sorted before, at the same level or after the second taxonomy.
114
         */
115
        public function sort_taxonomy_objects( $taxonomy1, $taxonomy2 ) {
×
116
                if ( $taxonomy1->public === true && $taxonomy2->public === false ) {
×
117
                        return -1;
×
118
                }
119
                elseif ( $taxonomy1->public === false && $taxonomy2->public === true ) {
×
120
                        return 1;
×
121
                }
122

123
                // Same visibility, sort by name.
124
                return \strnatcmp( $taxonomy1->labels->name, $taxonomy2->labels->name );
×
125
        }
126

127
        /**
128
         * Extracts and formats the description associated with the input field.
129
         *
130
         * @param string|array<string> $description The description string. Can be an array of strings.
131
         * @param string               $id          The ID of the input field.
132
         *
133
         * @return string The description HTML for the input.
134
         */
135
        public function extract_description( $description, $id ) {
2✔
136
                if ( ! \is_array( $description ) ) {
2✔
137
                        return \sprintf( '<span id="%s-description">(%s)</span>', $id, \esc_html( $description ) );
2✔
138
                }
139

140
                return \sprintf( '<p id="%s-description">%s</p>', $id, \implode( '<br />', \array_map( '\esc_html', $description ) ) );
2✔
141
        }
142

143
        /**
144
         * Generates a list of checkboxes for registered taxonomies.
145
         *
146
         * @return string The generated taxonomies list.
147
         */
148
        public function generate_taxonomy_exclusion_list() {
2✔
149
                $taxonomies = \get_taxonomies( [], 'objects' );
2✔
150

151
                \usort( $taxonomies, [ $this, 'sort_taxonomy_objects' ] );
2✔
152

153
                $taxonomies_blacklist = \get_option( 'duplicate_post_taxonomies_blacklist' );
2✔
154

155
                if ( ! \is_array( $taxonomies_blacklist ) ) {
2✔
156
                        $taxonomies_blacklist = [];
×
157
                }
158

159
                $output = '';
2✔
160

161
                foreach ( $taxonomies as $taxonomy ) {
2✔
162
                        if ( $taxonomy->name === 'post_format' ) {
2✔
163
                                continue;
×
164
                        }
165

166
                        $is_public = ( $taxonomy->public ) ? 'public' : 'private';
2✔
167
                        $name      = \esc_attr( $taxonomy->name );
2✔
168

169
                        $output .= \sprintf( '<div class="taxonomy_%s">', $is_public );
2✔
170
                        $output .= $this->generate_options_input(
2✔
171
                                [
2✔
172
                                        'duplicate_post_taxonomies_blacklist[]' => [
2✔
173
                                                'type'    => 'checkbox',
2✔
174
                                                'id'      => 'duplicate-post-' . $this->prepare_input_id( $name ),
2✔
175
                                                'value'   => $name,
2✔
176
                                                'checked' => \in_array( $taxonomy->name, $taxonomies_blacklist, true ),
2✔
177
                                                'label'   => $taxonomy->labels->name . ' [' . $taxonomy->name . ']',
2✔
178
                                        ],
2✔
179
                                ]
2✔
180
                        );
2✔
181
                        $output .= '</div>';
2✔
182
                }
183

184
                return $output;
2✔
185
        }
186

187
        /**
188
         * Generates a list of checkboxes for registered roles.
189
         *
190
         * @return string The generated roles list.
191
         */
192
        public function generate_roles_permission_list() {
2✔
193
                $post_types        = \get_post_types( [ 'show_ui' => true ], 'objects' );
2✔
194
                $edit_capabilities = [ 'edit_posts' => true ];
2✔
195

196
                foreach ( $post_types as $post_type ) {
2✔
197
                        $edit_capabilities[ $post_type->cap->edit_posts ] = true;
2✔
198
                }
199

200
                $output = '';
2✔
201

202
                foreach ( Utils::get_roles() as $name => $display_name ) {
2✔
203
                        $role = \get_role( $name );
2✔
204

205
                        if ( \count( \array_intersect_key( $role->capabilities, $edit_capabilities ) ) > 0 ) {
2✔
206
                                $output .= $this->generate_options_input(
2✔
207
                                        [
2✔
208
                                                'duplicate_post_roles[]' => [
2✔
209
                                                        'type'    => 'checkbox',
2✔
210
                                                        'id'      => 'duplicate-post-' . $this->prepare_input_id( $name ),
2✔
211
                                                        'value'   => $name,
2✔
212
                                                        'checked' => $role->has_cap( 'copy_posts' ),
2✔
213
                                                        'label'   => \translate_user_role( $display_name ),
2✔
214
                                                ],
2✔
215
                                        ]
2✔
216
                                );
2✔
217
                        }
218
                }
219

220
                return $output;
2✔
221
        }
222

223
        /**
224
         * Generates a list of checkboxes for registered post types.
225
         *
226
         * @return string The generated post types list.
227
         */
228
        public function generate_post_types_list() {
2✔
229
                $post_types        = \get_post_types( [ 'show_ui' => true ], 'objects' );
2✔
230
                $hidden_post_types = $this->get_hidden_post_types();
2✔
231
                $output            = '';
2✔
232

233
                foreach ( $post_types as $post_type_object ) {
2✔
234
                        if ( \in_array( $post_type_object->name, $hidden_post_types, true ) ) {
2✔
235
                                continue;
×
236
                        }
237

238
                        $name = \esc_attr( $post_type_object->name );
2✔
239

240
                        $output .= $this->generate_options_input(
2✔
241
                                [
2✔
242
                                        'duplicate_post_types_enabled[]' => [
2✔
243
                                                'type'    => 'checkbox',
2✔
244
                                                'id'      => 'duplicate-post-' . $this->prepare_input_id( $name ),
2✔
245
                                                'value'   => $name,
2✔
246
                                                'checked' => $this->is_post_type_enabled( $post_type_object->name ),
2✔
247
                                                'label'   => $post_type_object->labels->name,
2✔
248
                                        ],
2✔
249
                                ]
2✔
250
                        );
2✔
251
                }
252

253
                return $output;
2✔
254
        }
255

256
        /**
257
         * Determines whether the passed option should result in a checked checkbox or not.
258
         *
259
         * @param string $option        The option to search for.
260
         * @param array  $option_values The option's values.
261
         * @param string $parent_option The parent option. Optional.
262
         *
263
         * @return bool Whether or not the checkbox should be checked.
264
         */
265
        public function is_checked( $option, $option_values, $parent_option = '' ) {
6✔
266
                if ( \array_key_exists( 'checked', $option_values ) ) {
6✔
267
                        return $option_values['checked'];
4✔
268
                }
269

270
                // Check for serialized options.
271
                $saved_option = ! empty( $parent_option ) ? \get_option( $parent_option ) : \get_option( $option );
2✔
272

273
                if ( ! \is_array( $saved_option ) ) {
2✔
274
                        return (int) $saved_option === 1;
2✔
275
                }
276

277
                // Clean up the sub-option's name.
278
                $cleaned_option = \trim( \str_replace( $parent_option, '', $option ), '[]' );
×
279

280
                return \array_key_exists( $cleaned_option, $saved_option ) && (int) $saved_option[ $cleaned_option ] === 1;
×
281
        }
282

283
        /**
284
         * Prepares the passed ID so it's properly formatted.
285
         *
286
         * @param string $id The ID to prepare.
287
         *
288
         * @return string The prepared input ID.
289
         */
290
        public function prepare_input_id( $id ) {
2✔
291
                return \str_replace( '_', '-', $id );
2✔
292
        }
293

294
        /**
295
         * Checks whether or not a post type is enabled.
296
         *
297
         * @codeCoverageIgnore As this is a simple wrapper for a function that is also being used elsewhere, we can skip testing for now.
298
         *
299
         * @param string $post_type The post type.
300
         *
301
         * @return bool Whether or not the post type is enabled.
302
         */
303
        public function is_post_type_enabled( $post_type ) {
304
                $duplicate_post_types_enabled = \get_option( 'duplicate_post_types_enabled', [ 'post', 'page' ] );
305
                if ( ! \is_array( $duplicate_post_types_enabled ) ) {
306
                        $duplicate_post_types_enabled = [ $duplicate_post_types_enabled ];
307
                }
308
                return \in_array( $post_type, $duplicate_post_types_enabled, true );
309
        }
310

311
        /**
312
         * Generates a list of post types that should be hidden from the options page.
313
         *
314
         * @return array The array of names of the post types to hide.
315
         */
316
        public function get_hidden_post_types() {
×
317
                $hidden_post_types = [
×
318
                        'attachment',
×
319
                        'wp_block',
×
320
                ];
×
321

322
                if ( Utils::is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
×
323
                        $hidden_post_types[] = 'product';
×
324
                }
325

326
                return $hidden_post_types;
×
327
        }
328
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc