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