Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.54% covered (danger)
58.54%
48 / 82
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SettingsUtils
58.54% covered (danger)
58.54%
48 / 82
62.50% covered (warning)
62.50%
5 / 8
31.04
0.00% covered (danger)
0.00%
0 / 1
 getConsideredPostTypes
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 getCompatiblePostTypes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getIncompatiblePostTypes
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 hasApiCreds
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasValidApiConnection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateApiConnection
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 colorInput
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addSettingsErrorMessage
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
1<?php
2// phpcs:disable Generic.Files.LineLength.TooLong
3
4declare(strict_types=1);
5
6namespace Beyondwords\Wordpress\Component\Settings;
7
8use Beyondwords\Wordpress\Core\ApiClient;
9use Beyondwords\Wordpress\Core\Environment;
10use Beyondwords\Wordpress\Core\Request;
11
12/**
13 * BeyondWords Settings Utilities.
14 *
15 * @package    Beyondwords
16 * @subpackage Beyondwords/includes
17 * @author     Stuart McAlpine <stu@beyondwords.io>
18 * @since      3.5.0
19 */
20class SettingsUtils
21{
22    /**
23     * Get the post types BeyondWords will consider for compatibility.
24     *
25     * We don't consider many of the core built-in post types for compatibity
26     * because they don't support the features we need such as titles, body,
27     * custom fields, etc.
28     *
29     * @since 3.7.0
30     * @since 4.5.0 Renamed from getAllowedPostTypes to getConsideredPostTypes.
31     * @since 4.6.2 Exclude wp_font_face and wp_font_family from considered post types.
32     *
33     * @static
34     *
35     * @return string[] Array of post type names.
36     **/
37    public static function getConsideredPostTypes(): array
38    {
39        $postTypes = get_post_types();
40
41        $skip = [
42            'attachment',
43            'custom_css',
44            'customize_changeset',
45            'nav_menu_item',
46            'oembed_cache',
47            'revision',
48            'user_request',
49            'wp_block',
50            'wp_font_face',
51            'wp_font_family',
52            'wp_template',
53            'wp_template_part',
54            'wp_global_styles',
55            'wp_navigation',
56        ];
57
58        // Remove the skipped post types
59        $postTypes = array_diff($postTypes, $skip);
60
61        return array_values($postTypes);
62    }
63
64    /**
65     * Get the post types that are compatible with BeyondWords.
66     *
67     * - Start with the considered post types
68     * - Allow publishers to filter the list
69     * - Filter again, removing any that are incompatible
70     *
71     * @since 3.0.0
72     * @since 3.2.0 Removed $output parameter to always output names, never objects.
73     * @since 3.2.0 Added `beyondwords_post_types` filter.
74     * @since 3.5.0 Moved from Core\Utils to Component\Settings\SettingsUtils.
75     * @since 3.7.0 Refactored forbidden/allowed/supported post type methods to improve site health debugging info.
76     * @since 4.5.0 Renamed from getSupportedPostTypes to getCompatiblePostTypes.
77     * @since 5.0.0 Remove beyondwords_post_types filter.
78     *
79     * @static
80     *
81     * @return string[] Array of post type names.
82     **/
83    public static function getCompatiblePostTypes(): array
84    {
85        $postTypes = SettingsUtils::getConsideredPostTypes();
86
87        /**
88         * Filters the post types supported by BeyondWords.
89         *
90         * This defaults to all registered post types with 'custom-fields' in their 'supports' array.
91         *
92         * If any of the supplied post types do not support custom fields then they will be removed
93         * from the array.
94         *
95         * @since 3.3.3 Introduced as beyondwords_post_types
96         * @since 4.3.0 Renamed from beyondwords_post_types to beyondwords_settings_post_types.
97         *
98         * @param string[] The post types supported by BeyondWords.
99         */
100        $postTypes = apply_filters('beyondwords_settings_post_types', $postTypes);
101
102        // Remove incompatible post types
103        $postTypes = array_diff($postTypes, SettingsUtils::getIncompatiblePostTypes());
104
105        return array_values($postTypes);
106    }
107
108    /**
109     * Get the post types that are incompatible with BeyondWords.
110     *
111     * The requirements are:
112     * - Must support Custom Fields.
113     *
114     * @since 4.5.0
115     *
116     * @static
117     *
118     * @return string[] Array of post type names.
119     **/
120    public static function getIncompatiblePostTypes(): array
121    {
122        $postTypes = SettingsUtils::getConsideredPostTypes();
123
124        // Filter the array, removing unsupported post types
125        $postTypes = array_filter($postTypes, function ($postType) {
126            if (post_type_supports($postType, 'custom-fields')) {
127                return false;
128            }
129
130            return true;
131        });
132
133        return array_values($postTypes);
134    }
135
136    /**
137     * Do we have both an API Key and Project ID?
138     *
139     * @since  5.2.0
140     * @static
141     */
142    public static function hasApiCreds(): bool
143    {
144        $projectId = trim(strval(get_option('beyondwords_project_id')));
145        $apiKey    = trim(strval(get_option('beyondwords_api_key')));
146
147        return strlen($projectId) && strlen($apiKey);
148    }
149
150    /**
151     * Do we have a valid REST API connection for the BeyondWords REST API?
152     *
153     * Note that this only whether a valid REST API connection was made when
154     * the API Key was supplied. The API connection may be invalidated at a later
155     * date e.g. if the API Key is revoked.
156     *
157     * @since  5.2.0
158     * @static
159     */
160    public static function hasValidApiConnection(): bool
161    {
162        return boolval(get_option('beyondwords_valid_api_connection'));
163    }
164
165    /**
166     * Validate the BeyondWords REST API connection.
167     *
168     * @since 5.0.0
169     * @since 5.2.0 Moved from Sync class into SettingsUtils class.
170     * @static
171     **/
172    public static function validateApiConnection(): bool
173    {
174        // This may have been left over from previous versions
175        delete_transient('beyondwords_validate_api_connection');
176
177        // Assume invalid connection
178        delete_option('beyondwords_valid_api_connection');
179
180        $projectId = get_option('beyondwords_project_id');
181        $apiKey    = get_option('beyondwords_api_key');
182
183        if (! $projectId || ! $apiKey) {
184            return false;
185        }
186
187        $url = sprintf('%s/projects/%d', Environment::getApiUrl(), $projectId);
188
189        $response = ApiClient::callApi(
190            new Request('GET', $url)
191        );
192
193        $statusCode = wp_remote_retrieve_response_code($response);
194
195        if ($statusCode === 200) {
196            update_option('beyondwords_valid_api_connection', gmdate(\DateTime::ATOM), false);
197            wp_cache_set('beyondwords_sync_to_wordpress', ['all'], 'beyondwords', 60);
198            return true;
199        }
200
201        // Cancel any syncs
202        wp_cache_delete('beyondwords_sync_to_wordpress', 'beyondwords');
203
204        $debug = sprintf(
205            '<code>%s</code>: <code>%s</code>',
206            wp_remote_retrieve_response_code($response),
207            wp_remote_retrieve_body($response)
208        );
209
210        self::addSettingsErrorMessage(
211            sprintf(
212                /* translators: %s is replaced with the BeyondWords REST API response debug data */
213                __(
214                    'We were unable to validate your BeyondWords REST API connection.<br />Please check your project ID and API key, save changes, and contact us for support if this message remains.<br /><br />BeyondWords REST API Response:<br />%s', // phpcs:ignore Generic.Files.LineLength.TooLong
215                    'speechkit'
216                ),
217                $debug,
218            ),
219            'Settings/ValidApiConnection'
220        );
221
222        return false;
223    }
224
225    /**
226     * A color input.
227     *
228     * @since  5.0.0
229     * @static
230     *
231     * @param string $label Content for the `<label>`
232     * @param string $name  `name` attribute for the `<input />`
233     * @param string $value `value` attribute for the `<input />`
234     */
235    public static function colorInput(string $label, string $name, string $value): void
236    {
237        ?>
238        <div class="color-input">
239            <label>
240                    <?php echo esc_attr($label); ?>
241            </label>
242            <output
243                for="<?php echo esc_attr($name); ?>"
244                style="background: <?php echo esc_attr($value); ?>; margin-right: 0.25rem;"
245            ></output>
246            <input
247                name="<?php echo esc_attr($name); ?>"
248                type="text"
249                value="<?php echo esc_attr($value); ?>"
250                class="small-text"
251                oninput="this.previousElementSibling.style.background = 'transparent'; this.previousElementSibling.style.background = `${this.value}`"
252            />
253        </div>
254        <?php
255    }
256
257    /**
258     * Add settings error message.
259     *
260     * @since 5.2.0
261     * @static
262     *
263     * @param string $message The error message.
264     * @param string $errorId The error ID.
265     **/
266    public static function addSettingsErrorMessage(string $message, string $errorId = ''): void
267    {
268        $errors = wp_cache_get('beyondwords_settings_errors', 'beyondwords');
269
270        if (empty($errors)) {
271            $errors = [];
272        }
273
274        if (empty($errorId)) {
275            $errorId = bin2hex(random_bytes(8));
276        }
277
278        $errors[$errorId] = $message;
279
280        wp_cache_set('beyondwords_settings_errors', $errors, 'beyondwords');
281    }
282}