From bdba262e460e962f195b22601ea14485a3f7574f Mon Sep 17 00:00:00 2001 From: Stuart McAlpine Date: Fri, 17 Oct 2025 10:25:11 +0100 Subject: [PATCH 1/6] Type hints --- src/Component/Post/AddPlayer/AddPlayer.php | 10 +- .../Post/AddPlayer/AddPlayer.php.bak | 187 ++++++ src/Component/Post/PostContentUtils.php | 42 +- src/Component/Post/PostContentUtils.php.bak | 590 ++++++++++++++++++ src/Component/Post/PostMetaUtils.php | 44 +- src/Component/Posts/BulkEdit/BulkEdit.php | 16 +- src/Component/Posts/BulkEdit/BulkEdit.php.bak | 301 +++++++++ src/Component/Settings/Settings.php | 22 +- src/Component/Settings/Settings.php.bak | 547 ++++++++++++++++ src/Component/Settings/SettingsUtils.php | 16 +- src/Component/Settings/SettingsUtils.php.bak | 292 +++++++++ src/Component/SiteHealth/SiteHealth.php | 16 +- src/Component/SiteHealth/SiteHealth.php.bak | 481 ++++++++++++++ src/Core/ApiClient.php | 40 +- src/Core/Core.php | 32 +- src/Core/CoreUtils.php | 8 +- src/Core/Environment.php | 14 +- src/Core/Player/ConfigBuilder.php | 2 + src/Core/Response.php | 22 +- .../Component/Posts/BulkEdit/BulkEditTest.php | 2 +- uninstall.php | 2 + 21 files changed, 2543 insertions(+), 143 deletions(-) create mode 100644 src/Component/Post/AddPlayer/AddPlayer.php.bak create mode 100755 src/Component/Post/PostContentUtils.php.bak create mode 100644 src/Component/Posts/BulkEdit/BulkEdit.php.bak create mode 100644 src/Component/Settings/Settings.php.bak create mode 100755 src/Component/Settings/SettingsUtils.php.bak create mode 100644 src/Component/SiteHealth/SiteHealth.php.bak diff --git a/src/Component/Post/AddPlayer/AddPlayer.php b/src/Component/Post/AddPlayer/AddPlayer.php index a8ebda15..a024b8e7 100644 --- a/src/Component/Post/AddPlayer/AddPlayer.php +++ b/src/Component/Post/AddPlayer/AddPlayer.php @@ -63,7 +63,7 @@ public static function registerBlock() * * @return array */ - public static function addPlugin($plugin_array) + public static function addPlugin(array $plugin_array): array { $plugin_array['beyondwords_player'] = BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/AddPlayer/tinymce.js'; return $plugin_array; @@ -78,7 +78,7 @@ public static function addPlugin($plugin_array) * * @return array */ - public static function addButton($buttons) + public static function addButton(array $buttons): array { $advIndex = array_search('wp_adv', $buttons); @@ -100,7 +100,7 @@ public static function addButton($buttons) * * @return string Comma-delimited list of stylesheets with the "Add Player" CSS appended. */ - public static function addStylesheet($stylesheets) + public static function addStylesheet(string $stylesheets): string { return $stylesheets . ',' . BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/AddPlayer/AddPlayer.css'; } @@ -135,7 +135,7 @@ public static function playerPreviewI18nStyles() * * @return mixed[] An array with TinyMCE config. */ - public static function filterTinyMceSettings($settings) + public static function filterTinyMceSettings(array $settings): array { if (isset($settings['content_style'])) { $settings['content_style'] .= ' ' . self::playerPreviewI18nStyles() . ' '; @@ -171,7 +171,7 @@ public static function addEditorStyles() * * @since 6.0.0 Make static. */ - public static function addBlockEditorStylesheet($hook) + public static function addBlockEditorStylesheet(string $hook): void { // Only enqueue for Gutenberg/Post screens if (CoreUtils::isGutenbergPage() || $hook === 'post.php' || $hook === 'post-new.php') { diff --git a/src/Component/Post/AddPlayer/AddPlayer.php.bak b/src/Component/Post/AddPlayer/AddPlayer.php.bak new file mode 100644 index 00000000..a8ebda15 --- /dev/null +++ b/src/Component/Post/AddPlayer/AddPlayer.php.bak @@ -0,0 +1,187 @@ + + * @since 3.2.0 + */ + +namespace Beyondwords\Wordpress\Component\Post\AddPlayer; + +use Beyondwords\Wordpress\Core\CoreUtils; + +/** + * AddPlayer + * + * @since 3.2.0 + */ +class AddPlayer +{ + // The CSS declaration block for the player preview in both Classic Editor and Block Editor. + public const PLAYER_PREVIEW_STYLE_FORMAT = "iframe [data-beyondwords-player]:empty:after, .edit-post-visual-editor [data-beyondwords-player]:empty:after { content: '%s'; }"; // phpcs:ignore Generic.Files.LineLength.TooLong + + /** + * Init. + * + * @since 4.0.0 + * @since 6.0.0 Make static. + */ + public static function init() + { + add_action('init', array(__CLASS__, 'registerBlock')); + add_action('enqueue_block_editor_assets', array(__CLASS__, 'addBlockEditorStylesheet')); + + add_action('admin_head', array(__CLASS__, 'addEditorStyles')); + add_filter('tiny_mce_before_init', array(__CLASS__, 'filterTinyMceSettings')); + + add_filter('mce_external_plugins', array(__CLASS__, 'addPlugin')); + add_filter('mce_buttons', array(__CLASS__, 'addButton')); + add_filter('mce_css', array(__CLASS__, 'addStylesheet')); + } + + /** + * Register Block. + * + * @since 3.2.0 + * @since 6.0.0 Make static. + */ + public static function registerBlock() + { + \register_block_type(__DIR__); + } + + /** + * Add TinyMCE buttons. + * + * @since 6.0.0 Make static. + * + * @param array TinyMCE plugin array + * + * @return array + */ + public static function addPlugin($plugin_array) + { + $plugin_array['beyondwords_player'] = BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/AddPlayer/tinymce.js'; + return $plugin_array; + } + + /** + * Register TinyMCE buttons. + * + * @since 6.0.0 Make static. + * + * @param array TinyMCE buttons array + * + * @return array + */ + public static function addButton($buttons) + { + $advIndex = array_search('wp_adv', $buttons); + + if ($advIndex === false) { + $advIndex = count($buttons); + } + + array_splice($buttons, $advIndex, 0, ['beyondwords_player']); + + return $buttons; + } + + /** + * Filters the comma-delimited list of stylesheets to load in TinyMCE. + * + * @since 6.0.0 Make static. + * + * @param string $stylesheets Comma-delimited list of stylesheets. + * + * @return string Comma-delimited list of stylesheets with the "Add Player" CSS appended. + */ + public static function addStylesheet($stylesheets) + { + return $stylesheets . ',' . BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/AddPlayer/AddPlayer.css'; + } + + /** + * "Player Preview" i18n styles. + * + * Player preview uses the CSS :after to set the content so we pass the CSS through WordPress i18n functions here. + * + * @since 3.3.0 + * @since 6.0.0 Make static. + * + * @return string CSS Block for player preview i18n delcerations. + */ + public static function playerPreviewI18nStyles() + { + return sprintf( + self::PLAYER_PREVIEW_STYLE_FORMAT, + esc_attr__('Player placeholder: The position of the audio player.', 'speechkit') + ); + } + + /** + * Tiny MCE before init. + * + * Adds i18n-compatible TinyMCE Classic Editor CSS for the player placeholder. + * + * @since 3.3.0 + * @since 6.0.0 Make static. + * + * @param mixed[] $setings An array with TinyMCE config. + * + * @return mixed[] An array with TinyMCE config. + */ + public static function filterTinyMceSettings($settings) + { + if (isset($settings['content_style'])) { + $settings['content_style'] .= ' ' . self::playerPreviewI18nStyles() . ' '; + } else { + $settings['content_style'] = self::playerPreviewI18nStyles() . ' '; + } + + return $settings; + } + + /** + * Add editor styles. + * + * Adds i18n-compatible Block Editor CSS for the player placeholder. + * + * @since 3.3.0 + * @since 6.0.0 Make static. + */ + public static function addEditorStyles() + { + $allowed_html = array( + 'style' => array(), + ); + + echo wp_kses( + sprintf('', self::playerPreviewI18nStyles()), + $allowed_html + ); + } + + /** + * Add Block Editor Stylesheet. + * + * @since 6.0.0 Make static. + */ + public static function addBlockEditorStylesheet($hook) + { + // Only enqueue for Gutenberg/Post screens + if (CoreUtils::isGutenbergPage() || $hook === 'post.php' || $hook === 'post-new.php') { + // Register the Classic/Block Editor "Add Player" CSS + wp_enqueue_style( + 'beyondwords-AddPlayer', + BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/AddPlayer/AddPlayer.css', + array(), + BEYONDWORDS__PLUGIN_VERSION + ); + } + } +} diff --git a/src/Component/Post/PostContentUtils.php b/src/Component/Post/PostContentUtils.php index d56a8cfc..d1950a15 100755 --- a/src/Component/Post/PostContentUtils.php +++ b/src/Component/Post/PostContentUtils.php @@ -23,13 +23,13 @@ class PostContentUtils * From API version 1.1 the "summary" param is going to be used differently, * so for WordPress we now prepend the WordPress excerpt to the "body" param. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.6.0 * * @return string The content body param. */ - public static function getContentBody($post) + public static function getContentBody(int|\WP_Post $post): string|null { $post = get_post($post); @@ -61,11 +61,11 @@ public static function getContentBody($post) * @since 5.0.0 Remove SpeechKit-Start shortcode. * @since 5.0.0 Remove beyondwords_content filter. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @return string The body (the processed $post->post_content). */ - public static function getPostBody($post) + public static function getPostBody(int|\WP_Post $post): string|null { $post = get_post($post); @@ -96,13 +96,13 @@ public static function getPostBody($post) * This is a
with optional attributes depending on the BeyondWords * data of the post. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.6.0 * * @return string The summary wrapper
. */ - public static function getPostSummaryWrapperFormat($post) + public static function getPostSummaryWrapperFormat(int|\WP_Post $post): string { $post = get_post($post); @@ -122,14 +122,14 @@ public static function getPostSummaryWrapperFormat($post) /** * Get the post summary for the audio content. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.0.0 * @since 4.6.0 Renamed from PostContentUtils::getSummary() to PostContentUtils::getPostSummary() * * @return string The summary. */ - public static function getPostSummary($post) + public static function getPostSummary(int|\WP_Post $post): string|null { $post = get_post($post); @@ -162,13 +162,13 @@ public static function getPostSummary($post) * formatting tags such as and so we do not pass segments, we pass * a HTML string as the body param instead. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.0.0 * * @return array|null The segments. */ - public static function getSegments($post) + public static function getSegments(int|\WP_Post $post): array { if (! has_blocks($post)) { return null; @@ -219,14 +219,14 @@ public static function getSegments($post) * * This method filters all blocks, removing any which have been excluded. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 3.8.0 * @since 4.0.0 Replace for loop with array_reduce * * @return string The post body without excluded blocks. */ - public static function getContentWithoutExcludedBlocks($post) + public static function getContentWithoutExcludedBlocks(int|\WP_Post $post): string { if (! has_blocks($post)) { return trim($post->post_content); @@ -252,14 +252,14 @@ public static function getContentWithoutExcludedBlocks($post) /** * Get audio-enabled blocks. * - * @param int|WP_Post $post The WordPress post ID, or post object. + * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.0.0 * @since 5.0.0 Remove beyondwords_post_audio_enabled_blocks filter. * * @return array The blocks. */ - public static function getAudioEnabledBlocks($post) + public static function getAudioEnabledBlocks(int|\WP_Post $post): array { $post = get_post($post); @@ -305,7 +305,7 @@ public static function getAudioEnabledBlocks($post) * * @return string JSON endoded params. **/ - public static function getContentParams($postId) + public static function getContentParams(int $postId): array|string { $body = [ 'type' => 'auto_segment', @@ -394,7 +394,7 @@ public static function getContentParams($postId) * * @return object The metadata object (empty if no metadata). */ - public static function getMetadata($postId) + public static function getMetadata(int $postId): array|object { $metadata = new \stdClass(); @@ -426,7 +426,7 @@ public static function getMetadata($postId) * * @return object The taxonomies object (empty if no taxonomies). */ - public static function getAllTaxonomiesAndTerms($postId) + public static function getAllTaxonomiesAndTerms(int $postId): array|object { $postType = get_post_type($postId); @@ -454,7 +454,7 @@ public static function getAllTaxonomiesAndTerms($postId) * * @return string */ - public static function getAuthorName($postId) + public static function getAuthorName(int $postId): string { $authorId = get_post_field('post_author', $postId); @@ -475,7 +475,7 @@ public static function getAuthorName($postId) * * @return string HTML. */ - public static function addMarkerAttribute($html, $marker) + public static function addMarkerAttribute(string $html, string $marker): string { if (! $marker) { return $html; @@ -503,7 +503,7 @@ public static function addMarkerAttribute($html, $marker) * * @return string HTML. */ - public static function addMarkerAttributeWithHTMLTagProcessor($html, $marker) + public static function addMarkerAttributeWithHTMLTagProcessor(string $html, string $marker): string { if (! $marker) { return $html; @@ -545,7 +545,7 @@ public static function addMarkerAttributeWithHTMLTagProcessor($html, $marker) * * @return string HTML. */ - public static function addMarkerAttributeWithDOMDocument($html, $marker) + public static function addMarkerAttributeWithDOMDocument(string $html, string $marker): string { if (! $marker) { return $html; diff --git a/src/Component/Post/PostContentUtils.php.bak b/src/Component/Post/PostContentUtils.php.bak new file mode 100755 index 00000000..d56a8cfc --- /dev/null +++ b/src/Component/Post/PostContentUtils.php.bak @@ -0,0 +1,590 @@ + + * @since 3.5.0 + */ +class PostContentUtils +{ + public const DATE_FORMAT = 'Y-m-d\TH:i:s\Z'; + + /** + * Get the content "body" param for the audio, ready to be sent to the + * BeyondWords API. + * + * From API version 1.1 the "summary" param is going to be used differently, + * so for WordPress we now prepend the WordPress excerpt to the "body" param. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 4.6.0 + * + * @return string The content body param. + */ + public static function getContentBody($post) + { + $post = get_post($post); + + if (!($post instanceof \WP_Post)) { + throw new \Exception(esc_html__('Post Not Found', 'speechkit')); + } + + $summary = PostContentUtils::getPostSummary($post); + $body = PostContentUtils::getPostBody($post); + + if ($summary) { + $format = PostContentUtils::getPostSummaryWrapperFormat($post); + + $body = sprintf($format, $summary) . $body; + } + + return $body; + } + + /** + * Get the post body for the audio content. + * + * @since 3.0.0 + * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils + * @since 3.8.0 Exclude Gutenberg blocks with attribute { beyondwordsAudio: false } + * @since 4.0.0 Renamed from PostContentUtils::getSourceTextForAudio() to PostContentUtils::getBody() + * @since 4.6.0 Renamed from PostContentUtils::getBody() to PostContentUtils::getPostBody() + * @since 4.7.0 Remove wpautop filter for block editor API requests. + * @since 5.0.0 Remove SpeechKit-Start shortcode. + * @since 5.0.0 Remove beyondwords_content filter. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @return string The body (the processed $post->post_content). + */ + public static function getPostBody($post) + { + $post = get_post($post); + + if (!($post instanceof \WP_Post)) { + throw new \Exception(esc_html__('Post Not Found', 'speechkit')); + } + + $content = PostContentUtils::getContentWithoutExcludedBlocks($post); + + if (has_blocks($post)) { + // wpautop breaks our HTML markup when block editor paragraphs are empty + remove_filter('the_content', 'wpautop'); + + // But we still want to remove empty lines + $content = preg_replace('/^\h*\v+/m', '', $content); + } + + // Apply the_content filters to handle shortcodes etc + $content = apply_filters('the_content', $content); + + // Trim to remove trailing newlines – common for WordPress content + return trim($content); + } + + /** + * Get the post summary wrapper format. + * + * This is a
with optional attributes depending on the BeyondWords + * data of the post. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 4.6.0 + * + * @return string The summary wrapper
. + */ + public static function getPostSummaryWrapperFormat($post) + { + $post = get_post($post); + + if (!($post instanceof \WP_Post)) { + throw new \Exception(esc_html__('Post Not Found', 'speechkit')); + } + + $summaryVoiceId = intval(get_post_meta($post->ID, 'beyondwords_summary_voice_id', true)); + + if ($summaryVoiceId > 0) { + return '
%s
'; + } + + return '
%s
'; + } + + /** + * Get the post summary for the audio content. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 4.0.0 + * @since 4.6.0 Renamed from PostContentUtils::getSummary() to PostContentUtils::getPostSummary() + * + * @return string The summary. + */ + public static function getPostSummary($post) + { + $post = get_post($post); + + if (!($post instanceof \WP_Post)) { + throw new \Exception(esc_html__('Post Not Found', 'speechkit')); + } + + $summary = null; + + // Optionally send the excerpt to the REST API, if the plugin setting has been checked + $prependExcerpt = get_option('beyondwords_prepend_excerpt'); + + if ($prependExcerpt && has_excerpt($post)) { + // Escape characters + $summary = htmlentities($post->post_excerpt, ENT_QUOTES | ENT_XHTML); + // Apply WordPress filters + $summary = apply_filters('get_the_excerpt', $summary); + // Convert line breaks into paragraphs + $summary = trim(wpautop($summary)); + } + + return $summary; + } + + /** + * Get the segments for the audio content, ready to be sent to the BeyondWords API. + * + * @codeCoverageIgnore + * THIS METHOD IS CURRENTLY NOT IN USE. Segments cannot currently include HTML + * formatting tags such as and so we do not pass segments, we pass + * a HTML string as the body param instead. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 4.0.0 + * + * @return array|null The segments. + */ + public static function getSegments($post) + { + if (! has_blocks($post)) { + return null; + } + + $titleSegment = (object) [ + 'section' => 'title', + 'text' => get_the_title($post), + ]; + + $summarySegment = (object) [ + 'section' => 'summary', + 'text' => PostContentUtils::getPostSummary($post), + ]; + + $blocks = PostContentUtils::getAudioEnabledBlocks($post); + + $bodySegments = array_map(function ($block) { + $marker = null; + + if (isset($block['attrs']) && isset($block['attrs']['beyondwordsMarker'])) { + $marker = $block['attrs']['beyondwordsMarker']; + } + + return (object) [ + 'section' => 'body', + 'marker' => $marker, + 'text' => trim(render_block($block)), + ]; + }, $blocks); + + // Merge title, summary and body segments + $segments = array_values(array_merge([$titleSegment], [$summarySegment], $bodySegments)); + + // Remove any segments with empty text + $segments = array_values(array_filter($segments, function ($segment) { + return (! empty($segment::text)); + })); + + return $segments; + } + + /** + * Get the post content without blocks which have been filtered. + * + * We have added buttons into the Gutenberg editor to optionally exclude selected + * blocks from the source text for audio. + * + * This method filters all blocks, removing any which have been excluded. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 3.8.0 + * @since 4.0.0 Replace for loop with array_reduce + * + * @return string The post body without excluded blocks. + */ + public static function getContentWithoutExcludedBlocks($post) + { + if (! has_blocks($post)) { + return trim($post->post_content); + } + + $blocks = parse_blocks($post->post_content); + $output = ''; + + $blocks = PostContentUtils::getAudioEnabledBlocks($post); + + foreach ($blocks as $block) { + $marker = $block['attrs']['beyondwordsMarker'] ?? ''; + + $output .= PostContentUtils::addMarkerAttribute( + render_block($block), + $marker + ); + } + + return $output; + } + + /** + * Get audio-enabled blocks. + * + * @param int|WP_Post $post The WordPress post ID, or post object. + * + * @since 4.0.0 + * @since 5.0.0 Remove beyondwords_post_audio_enabled_blocks filter. + * + * @return array The blocks. + */ + public static function getAudioEnabledBlocks($post) + { + $post = get_post($post); + + if (! ($post instanceof \WP_Post)) { + return []; + } + + if (! has_blocks($post)) { + return []; + } + + $allBlocks = parse_blocks($post->post_content); + + $blocks = array_filter($allBlocks, function ($block) { + $enabled = true; + + if (is_array($block['attrs']) && isset($block['attrs']['beyondwordsAudio'])) { + $enabled = (bool) $block['attrs']['beyondwordsAudio']; + } + + return $enabled; + }); + + return $blocks; + } + + /** + * Get the body param we pass to the API. + * + * @since 3.0.0 Introduced as getBodyJson. + * @since 3.3.0 Added metadata to aid custom playlist generation. + * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils. + * @since 3.10.4 Rename `published_at` API param to `publish_date`. + * @since 4.0.0 Use new API params. + * @since 4.0.3 Ensure `image_url` is always a string. + * @since 4.3.0 Rename from getBodyJson to getContentParams. + * @since 4.6.0 Remove summary param & prepend body with summary. + * @since 5.0.0 Remove beyondwords_body_params filter. + * @since 6.0.0 Cast return value to string. + * + * @static + * @param int $postId WordPress Post ID. + * + * @return string JSON endoded params. + **/ + public static function getContentParams($postId) + { + $body = [ + 'type' => 'auto_segment', + 'title' => get_the_title($postId), + 'body' => PostContentUtils::getContentBody($postId), + 'source_url' => get_the_permalink($postId), + 'source_id' => strval($postId), + 'author' => PostContentUtils::getAuthorName($postId), + 'image_url' => strval(wp_get_original_image_url(get_post_thumbnail_id($postId))), + 'metadata' => PostContentUtils::getMetadata($postId), + 'publish_date' => get_post_time(PostContentUtils::DATE_FORMAT, true, $postId), + ]; + + $status = get_post_status($postId); + + /* + * If the post status is draft/pending then we explicity send + * { published: false } to the BeyondWords API, to prevent the + * generated audio from being published in playlists. + * + * We also omit { publish_date } because get_post_time() returns `false` + * for posts which are "Pending Review". + */ + if (in_array($status, ['draft', 'pending'])) { + $body['published'] = false; + unset($body['publish_date']); + } elseif (get_option('beyondwords_project_auto_publish_enabled')) { + $body['published'] = true; + } + + $languageCode = get_post_meta($postId, 'beyondwords_language_code', true); + + if ($languageCode) { + $body['language'] = $languageCode; + } + + $bodyVoiceId = intval(get_post_meta($postId, 'beyondwords_body_voice_id', true)); + + if ($bodyVoiceId > 0) { + $body['body_voice_id'] = $bodyVoiceId; + } + + $titleVoiceId = intval(get_post_meta($postId, 'beyondwords_title_voice_id', true)); + + if ($titleVoiceId > 0) { + $body['title_voice_id'] = $titleVoiceId; + } + + $summaryVoiceId = intval(get_post_meta($postId, 'beyondwords_summary_voice_id', true)); + + if ($summaryVoiceId > 0) { + $body['summary_voice_id'] = $summaryVoiceId; + } + + /** + * Filters the params we send to the BeyondWords API 'content' endpoint. + * + * @since 4.0.0 Introduced as beyondwords_body_params + * @since 4.3.0 Renamed from beyondwords_body_params to beyondwords_content_params + * + * @param array $body The params we send to the BeyondWords API. + * @param array $postId WordPress post ID. + */ + $body = apply_filters('beyondwords_content_params', $body, $postId); + + return (string) wp_json_encode($body); + } + + /** + * Get the post metadata to send with BeyondWords API requests. + * + * The metadata key is defined by the BeyondWords API as "A custom object + * for storing meta information". + * + * The metadata values are used to create filters for playlists in the + * BeyondWords dashboard. + * + * We currently only include taxonomies by default, and the output of this + * method can be filtered using the `beyondwords_post_metadata` filter. + * + * @since 3.3.0 + * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils. + * @since 5.0.0 Remove beyondwords_post_metadata filter. + * + * @param int $postId Post ID. + * + * @return object The metadata object (empty if no metadata). + */ + public static function getMetadata($postId) + { + $metadata = new \stdClass(); + + $taxonomy = PostContentUtils::getAllTaxonomiesAndTerms($postId); + + if (count((array)$taxonomy)) { + $metadata->taxonomy = $taxonomy; + } + + return $metadata; + } + + /** + * Get all taxonomies, and their selected terms, for a post. + * + * Returns an associative array of taxonomy names and terms. + * + * For example: + * + * array( + * "categories" => array("Category 1"), + * "post_tag" => array("Tag 1", "Tag 2", "Tag 3"), + * ) + * + * @since 3.3.0 + * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils + * + * @param int $postId Post ID. + * + * @return object The taxonomies object (empty if no taxonomies). + */ + public static function getAllTaxonomiesAndTerms($postId) + { + $postType = get_post_type($postId); + + $postTypeTaxonomies = get_object_taxonomies($postType); + + $taxonomies = new \stdClass(); + + foreach ($postTypeTaxonomies as $postTypeTaxonomy) { + $terms = get_the_terms($postId, $postTypeTaxonomy); + + if (! empty($terms) && ! is_wp_error($terms)) { + $taxonomies->{(string)$postTypeTaxonomy} = wp_list_pluck($terms, 'name'); + } + } + + return $taxonomies; + } + + /** + * Get author name for a post. + * + * @since 3.10.4 + * + * @param int $postId Post ID. + * + * @return string + */ + public static function getAuthorName($postId) + { + $authorId = get_post_field('post_author', $postId); + + return get_the_author_meta('display_name', $authorId); + } + + /** + * Add data-beyondwords-marker attribute to the root elements in a HTML + * string (typically the rendered HTML of a single block). + * + * Checks to see whether we can use WP_HTML_Tag_Processor, or whether we + * fall back to using DOMDocument to add the marker. + * + * @since 4.2.2 + * + * @param string $html HTML. + * @param string $marker Marker UUID. + * + * @return string HTML. + */ + public static function addMarkerAttribute($html, $marker) + { + if (! $marker) { + return $html; + } + + // Prefer WP_HTML_Tag_Processor, introduced in WordPress 6.2 + if (class_exists('WP_HTML_Tag_Processor')) { + return PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker); + } else { + return PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker); + } + } + + /** + * Add data-beyondwords-marker attribute to the root elements in a HTML + * string using WP_HTML_Tag_Processor. + * + * @since 4.0.0 + * @since 4.2.2 Moved from src/Component/Post/BlockAttributes/BlockAttributes.php + * to src/Component/Post/PostContentUtils.php + * @since 4.7.0 Prevent empty data-beyondwords-marker attributes. + * + * @param string $html HTML. + * @param string $marker Marker UUID. + * + * @return string HTML. + */ + public static function addMarkerAttributeWithHTMLTagProcessor($html, $marker) + { + if (! $marker) { + return $html; + } + + // https://github.com/WordPress/gutenberg/pull/42485 + $tags = new \WP_HTML_Tag_Processor($html); + + if ($tags->next_tag()) { + $tags->set_attribute('data-beyondwords-marker', $marker); + } + + return strval($tags); + } + + /** + * Add data-beyondwords-marker attribute to the root elements in a HTML + * string using DOMDocument. + * + * This is a fallback, since WP_HTML_Tag_Processor was only shipped with + * WordPress 6.2 on 19 April 2023. + * + * https://make.wordpress.org/core/2022/10/13/whats-new-in-gutenberg-14-3-12-october/ + * + * Note: It is not ideal to do all the $bodyElement/$fullHtml processing + * in this method, but without it DOMDocument does not work as expected if + * there is more than 1 root element. The approach here has been taken from + * some historic Gutenberg code before they implemented WP_HTML_Tag_Processor: + * + * https://github.com/WordPress/gutenberg/blob/6671cef1179412a2bbd4969cbbc82705c7f69bac/lib/block-supports/index.php + * + * @since 4.0.0 + * @since 4.2.2 Moved from src/Component/Post/BlockAttributes/BlockAttributes.php + * to src/Component/Post/PostContentUtils.php + * @since 4.7.0 Prevent empty data-beyondwords-marker attributes. + * + * @param string $html HTML. + * @param string $marker Marker UUID. + * + * @return string HTML. + */ + public static function addMarkerAttributeWithDOMDocument($html, $marker) + { + if (! $marker) { + return $html; + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + + $wrappedHtml = + '' + . $html + . ''; + + $success = $dom->loadHTML($wrappedHtml, LIBXML_HTML_NODEFDTD | LIBXML_COMPACT); + + if (! $success) { + return $html; + } + + // Structure is like ``, so body is the `lastChild` of our document. + $bodyElement = $dom->documentElement->lastChild; + + $xpath = new \DOMXPath($dom); + $blockRoot = $xpath->query('./*', $bodyElement)[0]; + + if (empty($blockRoot)) { + return $html; + } + + $blockRoot->setAttribute('data-beyondwords-marker', $marker); + + // Avoid using `$dom->saveHtml( $node )` because the node results may not produce consistent + // whitespace. Saving the root HTML `$dom->saveHtml()` prevents this behavior. + $fullHtml = $dom->saveHtml(); + + // Find the open/close tags. The open tag needs to be adjusted so we get inside the tag + // and not the tag itself. + $start = strpos($fullHtml, '', 0) + strlen(''); + $end = strpos($fullHtml, '', $start); + + return trim(substr($fullHtml, $start, $end - $start)); + } +} diff --git a/src/Component/Post/PostMetaUtils.php b/src/Component/Post/PostMetaUtils.php index d7a7b6cd..6f5f3094 100755 --- a/src/Component/Post/PostMetaUtils.php +++ b/src/Component/Post/PostMetaUtils.php @@ -36,7 +36,7 @@ class PostMetaUtils * * @return string */ - public static function getRenamedPostMeta($postId, $name) + public static function getRenamedPostMeta(int $postId, string $name): mixed { if (metadata_exists('post', $postId, 'beyondwords_' . $name)) { return get_post_meta($postId, 'beyondwords_' . $name, true); @@ -59,7 +59,7 @@ public static function getRenamedPostMeta($postId, $name) * * @since 4.1.0 Append 'beyondwords_version' and 'wordpress_version'. */ - public static function getAllBeyondwordsMetadata($postId) + public static function getAllBeyondwordsMetadata(int $postId): array { global $wp_version; @@ -99,7 +99,7 @@ public static function getAllBeyondwordsMetadata($postId) /** * Remove the BeyondWords metadata for a Post. */ - public static function removeAllBeyondwordsMetadata($postId) + public static function removeAllBeyondwordsMetadata(int $postId): void { $keysToCheck = [ 'beyondwords_generate_audio', @@ -136,8 +136,6 @@ public static function removeAllBeyondwordsMetadata($postId) foreach ($keysToCheck as $key) { delete_post_meta($postId, $key, null); } - - return true; } /** @@ -149,7 +147,7 @@ public static function removeAllBeyondwordsMetadata($postId) * * @return bool True if the post should have BeyondWords content, false otherwise. */ - public static function hasContent($postId) + public static function hasContent(int $postId): bool { $contentId = PostMetaUtils::getContentId($postId); $integrationMethod = get_post_meta($postId, 'beyondwords_integration_method', true); @@ -190,7 +188,7 @@ public static function hasContent($postId) * * @return string|false Content ID, or false */ - public static function getContentId($postId, $fallback = false) + public static function getContentId(int $postId, bool $fallback = false): string|int|false { $contentId = get_post_meta($postId, 'beyondwords_content_id', true); if (! empty($contentId)) { @@ -223,7 +221,7 @@ public static function getContentId($postId, $fallback = false) * * @return int|false Podcast ID, or false */ - public static function getPodcastId($postId) + public static function getPodcastId(int $postId): string|int|false { // Check for "Podcast ID" custom field (number, or string for > 4.x) $podcastId = PostMetaUtils::getRenamedPostMeta($postId, 'podcast_id'); @@ -294,11 +292,11 @@ public static function getPodcastId($postId) * * @return string Preview token */ - public static function getPreviewToken($postId) + public static function getPreviewToken(int $postId): string|false { $previewToken = get_post_meta($postId, 'beyondwords_preview_token', true); - return $previewToken; + return $previewToken ?: false; } /** @@ -312,7 +310,7 @@ public static function getPreviewToken($postId) * * @return bool */ - public static function hasGenerateAudio($postId) + public static function hasGenerateAudio(int $postId): bool { $generateAudio = PostMetaUtils::getRenamedPostMeta($postId, 'generate_audio'); @@ -348,7 +346,7 @@ public static function hasGenerateAudio($postId) * * @return int|false Project ID, or false */ - public static function getProjectId($postId, $strict = false) + public static function getProjectId(int $postId, bool $strict = false): int|string|false { // If strict is true, we only check the custom field and do not fall back to the plugin setting. if ($strict) { @@ -396,11 +394,11 @@ public static function getProjectId($postId, $strict = false) * * @return int|false Body Voice ID, or false */ - public static function getBodyVoiceId($postId) + public static function getBodyVoiceId(int $postId): int|string|false { $voiceId = get_post_meta($postId, 'beyondwords_body_voice_id', true); - return $voiceId; + return $voiceId ?: false; } /** @@ -415,11 +413,11 @@ public static function getBodyVoiceId($postId) * * @return int|false Title Voice ID, or false */ - public static function getTitleVoiceId($postId) + public static function getTitleVoiceId(int $postId): int|string|false { $voiceId = get_post_meta($postId, 'beyondwords_title_voice_id', true); - return $voiceId; + return $voiceId ?: false; } /** @@ -434,11 +432,11 @@ public static function getTitleVoiceId($postId) * * @return int|false Summary Voice ID, or false */ - public static function getSummaryVoiceId($postId) + public static function getSummaryVoiceId(int $postId): int|string|false { $voiceId = get_post_meta($postId, 'beyondwords_summary_voice_id', true); - return $voiceId; + return $voiceId ?: false; } /** @@ -452,7 +450,7 @@ public static function getSummaryVoiceId($postId) * * @return string Player style. */ - public static function getPlayerStyle($postId) + public static function getPlayerStyle(int $postId): string { $playerStyle = get_post_meta($postId, 'beyondwords_player_style', true); @@ -476,7 +474,7 @@ public static function getPlayerStyle($postId) * * @return string */ - public static function getErrorMessage($postId) + public static function getErrorMessage(int $postId): string|false { return PostMetaUtils::getRenamedPostMeta($postId, 'error_message'); } @@ -492,9 +490,9 @@ public static function getErrorMessage($postId) * * @return string */ - public static function getDisabled($postId) + public static function getDisabled(int $postId): bool { - return PostMetaUtils::getRenamedPostMeta($postId, 'disabled'); + return (bool) PostMetaUtils::getRenamedPostMeta($postId, 'disabled'); } /** @@ -515,7 +513,7 @@ public static function getDisabled($postId) * * @return string */ - public static function getHttpResponseBodyFromPostMeta($postId, $metaName) + public static function getHttpResponseBodyFromPostMeta(int $postId, string $metaName): array|string|false { $postMeta = get_post_meta($postId, $metaName, true); diff --git a/src/Component/Posts/BulkEdit/BulkEdit.php b/src/Component/Posts/BulkEdit/BulkEdit.php index ebbf161e..90c394e1 100644 --- a/src/Component/Posts/BulkEdit/BulkEdit.php +++ b/src/Component/Posts/BulkEdit/BulkEdit.php @@ -55,7 +55,7 @@ public static function init() * * @since 6.0.0 Make static. */ - public static function bulkEditCustomBox($columnName, $postType) + public static function bulkEditCustomBox(string $columnName, string $postType): void { if ($columnName !== 'beyondwords') { return; @@ -133,10 +133,10 @@ public static function saveBulkEdit() * * @since 6.0.0 Make static. */ - public static function generateAudioForPosts($postIds) + public static function generateAudioForPosts(array|null $postIds): array { if (! is_array($postIds)) { - return false; + return []; } $updatedPostIds = []; @@ -156,10 +156,10 @@ public static function generateAudioForPosts($postIds) * * @since 6.0.0 Make static. */ - public static function deleteAudioForPosts($postIds) + public static function deleteAudioForPosts(array|null $postIds): array { if (! is_array($postIds)) { - return false; + return []; } $updatedPostIds = []; @@ -188,7 +188,7 @@ public static function deleteAudioForPosts($postIds) * * @since 6.0.0 Make static. */ - public static function bulkActionsEdit($bulk_array) + public static function bulkActionsEdit(array $bulk_array): array { $bulk_array['beyondwords_generate_audio'] = __('Generate audio', 'speechkit'); $bulk_array['beyondwords_delete_audio'] = __('Delete audio', 'speechkit'); @@ -201,7 +201,7 @@ public static function bulkActionsEdit($bulk_array) * * @since 6.0.0 Make static. */ - public static function handleBulkGenerateAction($redirect, $doaction, $objectIds) + public static function handleBulkGenerateAction(string $redirect, string $doaction, array $objectIds): string { if ($doaction !== 'beyondwords_generate_audio') { return $redirect; @@ -259,7 +259,7 @@ public static function handleBulkGenerateAction($redirect, $doaction, $objectIds * * @since 6.0.0 Make static. */ - public static function handleBulkDeleteAction($redirect, $doaction, $objectIds) + public static function handleBulkDeleteAction(string $redirect, string $doaction, array $objectIds): string { if ($doaction !== 'beyondwords_delete_audio') { return $redirect; diff --git a/src/Component/Posts/BulkEdit/BulkEdit.php.bak b/src/Component/Posts/BulkEdit/BulkEdit.php.bak new file mode 100644 index 00000000..ebbf161e --- /dev/null +++ b/src/Component/Posts/BulkEdit/BulkEdit.php.bak @@ -0,0 +1,301 @@ + + * @since 3.0.0 + */ + +namespace Beyondwords\Wordpress\Component\Posts\BulkEdit; + +use Beyondwords\Wordpress\Core\Core; +use Beyondwords\Wordpress\Core\CoreUtils; +use Beyondwords\Wordpress\Component\Settings\SettingsUtils; +use Beyondwords\Wordpress\Plugin; + +/** + * BulkEdit + * + * @since 3.0.0 + */ +class BulkEdit +{ + /** + * Init. + * + * @since 4.0.0 + * @since 6.0.0 Make static. + */ + public static function init() + { + add_action('bulk_edit_custom_box', array(__CLASS__, 'bulkEditCustomBox'), 10, 2); + add_action('wp_ajax_save_bulk_edit_beyondwords', array(__CLASS__, 'saveBulkEdit')); + + add_action('wp_loaded', function () { + $postTypes = SettingsUtils::getCompatiblePostTypes(); + + if (is_array($postTypes)) { + foreach ($postTypes as $postType) { + add_filter("bulk_actions-edit-{$postType}", array(__CLASS__, 'bulkActionsEdit'), 10, 1); + add_filter("handle_bulk_actions-edit-{$postType}", array(__CLASS__, 'handleBulkDeleteAction'), 10, 3); // phpcs:ignore Generic.Files.LineLength.TooLong + add_filter("handle_bulk_actions-edit-{$postType}", array(__CLASS__, 'handleBulkGenerateAction'), 10, 3); // phpcs:ignore Generic.Files.LineLength.TooLong + } + } + }); + } + + /** + * Adds the meta box container. + * + * @since 6.0.0 Make static. + */ + public static function bulkEditCustomBox($columnName, $postType) + { + if ($columnName !== 'beyondwords') { + return; + } + + $postTypes = SettingsUtils::getCompatiblePostTypes(); + + if (! in_array($postType, $postTypes)) { + return; + } + + wp_nonce_field('beyondwords_bulk_edit_nonce', 'beyondwords_bulk_edit'); + + ?> +
+
+
+ +
+
+
+ getMessage(), $redirect); + } + + // Add $generated & $failed query args into redirect + $redirect = add_query_arg('beyondwords_bulk_generated', $generated, $redirect); + $redirect = add_query_arg('beyondwords_bulk_failed', $failed, $redirect); + + // Add nonce to redirect url + $nonce = wp_create_nonce('beyondwords_bulk_edit_result'); + $redirect = add_query_arg('beyondwords_bulk_edit_result_nonce', $nonce, $redirect); + + return $redirect; + } + + /** + * Handle the "Delete audio" bulk action. + * + * @since 6.0.0 Make static. + */ + public static function handleBulkDeleteAction($redirect, $doaction, $objectIds) + { + if ($doaction !== 'beyondwords_delete_audio') { + return $redirect; + } + + // Remove query args + $args = [ + 'beyondwords_bulk_generated', + 'beyondwords_bulk_deleted', + 'beyondwords_bulk_failed', + 'beyondwords_bulk_error', + ]; + + $redirect = remove_query_arg($args, $redirect); + + // Order batch by Post ID asc + sort($objectIds); + + $deleted = 0; + + // Handle "Delete audio" bulk action + try { + $result = self::deleteAudioForPosts($objectIds); + + $deleted = count($result); + + // Add $deleted query arg into redirect + $redirect = add_query_arg('beyondwords_bulk_deleted', $deleted, $redirect); + } catch (\Exception $e) { + $redirect = add_query_arg('beyondwords_bulk_error', $e->getMessage(), $redirect); + } + + // Add $nonce query arg into redirect + $nonce = wp_create_nonce('beyondwords_bulk_edit_result'); + $redirect = add_query_arg('beyondwords_bulk_edit_result_nonce', $nonce, $redirect); + + return $redirect; + } +} diff --git a/src/Component/Settings/Settings.php b/src/Component/Settings/Settings.php index 5f0fe019..c37d516e 100644 --- a/src/Component/Settings/Settings.php +++ b/src/Component/Settings/Settings.php @@ -76,7 +76,7 @@ public static function init() * * @return void */ - public static function addOptionsPage() + public static function addOptionsPage(): void { // Settings > BeyondWords add_options_page( @@ -96,7 +96,7 @@ public static function addOptionsPage() * * @return void */ - public static function maybeValidateApiCreds() + public static function maybeValidateApiCreds(): void { $activeTab = self::getActiveTab(); @@ -114,7 +114,7 @@ public static function maybeValidateApiCreds() * * @return void */ - public static function createAdminInterface() + public static function createAdminInterface(): void { $tabs = self::getTabs(); $activeTab = self::getActiveTab(); @@ -177,7 +177,7 @@ class="nav-tab " * @since 4.7.0 Prepend custom links instead of appending them. * @since 6.0.0 Make static. */ - public static function addSettingsLinkToPluginPage($links) + public static function addSettingsLinkToPluginPage(array $links): array { $settingsLink = 'id) { @@ -353,7 +353,7 @@ public static function maybePrintPluginReviewNotice() * * @return void */ - public static function printSettingsErrors() + public static function printSettingsErrors(): void { $settingsErrors = wp_cache_get('beyondwords_settings_errors', 'beyondwords'); wp_cache_delete('beyondwords_settings_errors', 'beyondwords'); @@ -399,7 +399,7 @@ public static function printSettingsErrors() * * @return void */ - public static function restApiInit() + public static function restApiInit(): void { // settings endpoint register_rest_route('beyondwords/v1', '/settings', array( @@ -487,7 +487,7 @@ public static function dismissReviewNotice() * * @return void */ - public static function enqueueScripts($hook) + public static function enqueueScripts(string $hook): void { if ($hook === 'settings_page_beyondwords') { // jQuery UI JS diff --git a/src/Component/Settings/Settings.php.bak b/src/Component/Settings/Settings.php.bak new file mode 100644 index 00000000..5f0fe019 --- /dev/null +++ b/src/Component/Settings/Settings.php.bak @@ -0,0 +1,547 @@ + + * @since 3.0.0 + */ + +namespace Beyondwords\Wordpress\Component\Settings; + +use Beyondwords\Wordpress\Component\Settings\Fields\IntegrationMethod\IntegrationMethod; +use Beyondwords\Wordpress\Component\Settings\Fields\Languages\Languages; +use Beyondwords\Wordpress\Component\Settings\Fields\PreselectGenerateAudio\PreselectGenerateAudio; +use Beyondwords\Wordpress\Component\Settings\Tabs\Content\Content; +use Beyondwords\Wordpress\Component\Settings\Tabs\Credentials\Credentials; +use Beyondwords\Wordpress\Component\Settings\Tabs\Player\Player; +use Beyondwords\Wordpress\Component\Settings\Tabs\Pronunciations\Pronunciations; +use Beyondwords\Wordpress\Component\Settings\Tabs\Summarization\Summarization; +use Beyondwords\Wordpress\Component\Settings\Tabs\Voices\Voices; +use Beyondwords\Wordpress\Component\Settings\SettingsUtils; +use Beyondwords\Wordpress\Component\Settings\Sync; +use Beyondwords\Wordpress\Core\Environment; + +/** + * Settings + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @since 3.0.0 + */ +class Settings +{ + public const REVIEW_NOTICE_TIME_FORMAT = '-14 days'; + + /** + * Init + * + * @since 3.0.0 Introduced. + * @since 5.4.0 Add plugin review notice. + * @since 6.0.0 Make static. + */ + public static function init() + { + (new Credentials())::init(); + (new Sync())::init(); + + if (SettingsUtils::hasValidApiConnection()) { + (new Voices())::init(); + (new Content())::init(); + (new Player())::init(); + (new Summarization())::init(); + (new Pronunciations())::init(); + } + + add_action('admin_menu', array(__CLASS__, 'addOptionsPage'), 1); + add_action('admin_notices', array(__CLASS__, 'printMissingApiCredsWarning'), 100); + add_action('admin_notices', array(__CLASS__, 'printSettingsErrors'), 200); + add_action('admin_notices', array(__CLASS__, 'maybePrintPluginReviewNotice')); + add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueueScripts')); + add_action('load-settings_page_beyondwords', array(__CLASS__, 'maybeValidateApiCreds')); + + add_action('rest_api_init', array(__CLASS__, 'restApiInit')); + + add_filter('plugin_action_links_speechkit/speechkit.php', array(__CLASS__, 'addSettingsLinkToPluginPage')); + } + + /** + * Add items to the WordPress admin menu. + * + * @since 3.0.0 + * @since 6.0.0 Make static. + * + * @return void + */ + public static function addOptionsPage() + { + // Settings > BeyondWords + add_options_page( + __('BeyondWords Settings', 'speechkit'), + __('BeyondWords', 'speechkit'), + 'manage_options', + 'beyondwords', + array(__CLASS__, 'createAdminInterface') + ); + } + + /** + * Validate API creds if we are on the credentials tab. + * + * @since 5.4.0 + * @since 6.0.0 Make static. + * + * @return void + */ + public static function maybeValidateApiCreds() + { + $activeTab = self::getActiveTab(); + + if ($activeTab === 'credentials') { + SettingsUtils::validateApiConnection(); + } + } + + /** + * Prints the admin interface for plugin settings. + * + * @since 3.0.0 + * @since 4.7.0 Added tabs. + * @since 6.0.0 Make static. + * + * @return void + */ + public static function createAdminInterface() + { + $tabs = self::getTabs(); + $activeTab = self::getActiveTab(); + ?> + + ' . __('Settings', 'speechkit') . ''; + + array_unshift($links, $settingsLink); + + return $links; + } + + /** + * Get tabs. + * + * @since 4.7.0 + * @since 5.2.0 Make static. + * + * @return array Tabs + */ + public static function getTabs() + { + $tabs = array( + 'credentials' => __('Credentials', 'speechkit'), + 'content' => __('Content', 'speechkit'), + 'voices' => __('Voices', 'speechkit'), + 'player' => __('Player', 'speechkit'), + 'summarization' => __('Summarization', 'speechkit'), + 'pronunciations' => __('Pronunciations', 'speechkit'), + ); + + if (! SettingsUtils::hasValidApiConnection()) { + $tabs = array_splice($tabs, 0, 1); + } + + return $tabs; + } + + /** + * Get active tab. + * + * @since 4.7.0 + * @since 5.2.0 Make static. + * + * @return string Active tab + */ + public static function getActiveTab() + { + $tabs = self::getTabs(); + + if (! count($tabs)) { + return ''; + } + + $defaultTab = array_key_first($tabs); + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if (isset($_GET['tab'])) { + $tab = sanitize_text_field(wp_unslash($_GET['tab'])); + } else { + $tab = $defaultTab; + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if (!empty($tab) && array_key_exists($tab, $tabs)) { + $activeTab = $tab; + } else { + $activeTab = $defaultTab; + } + + return $activeTab; + } + + /** + * Print missing API creds warning. + * + * @since 5.2.0 + * @since 6.0.0 Make static. + * + * @return void + */ + public static function printMissingApiCredsWarning() + { + if (! SettingsUtils::hasApiCreds()) : + ?> +
+

+ + %s', + esc_url(admin_url('options-general.php?page=beyondwords')), + esc_html__('plugin settings', 'speechkit') + ) + ); + ?> + +

+

+ +

+

+ + + +

+
+ id) { + return; + } + + $dateActivated = get_option('beyondwords_date_activated', '2025-03-01'); + $dateNoticeDismissed = get_option('beyondwords_notice_review_dismissed', ''); + + $showNotice = false; + + if (empty($dateNoticeDismissed)) { + $dateActivated = strtotime($dateActivated); + + if ($dateActivated < strtotime(self::REVIEW_NOTICE_TIME_FORMAT)) { + $showNotice = true; + } + } + + if ($showNotice) : + ?> +
+

+ + %s', + 'https://wordpress.org/support/plugin/speechkit/reviews/', + esc_html__('WordPress Plugin Repo', 'speechkit') + ) + ); + ?> + +

+
+ +
+
    + %s', + // Only allow links with href and target attributes + wp_kses( + $error, + array( + 'a' => array( + 'href' => array(), + 'target' => array(), + ), + 'b' => array(), + 'strong' => array(), + 'i' => array(), + 'em' => array(), + 'br' => array(), + 'code' => array(), + ) + ) + ); + } + ?> +
+
+ \WP_REST_Server::READABLE, + 'callback' => array(__CLASS__, 'restApiResponse'), + 'permission_callback' => function () { + return current_user_can('edit_posts'); + }, + )); + + // settings endpoint + register_rest_route('beyondwords/v1', '/settings', array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array(__CLASS__, 'restApiResponse'), + 'permission_callback' => function () { + return current_user_can('edit_posts'); + }, + )); + + // dismiss review notice endpoint + register_rest_route('beyondwords/v1', '/settings/notices/review/dismiss', array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array(__CLASS__, 'dismissReviewNotice'), + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + )); + } + + /** + * WP REST API response (required for the Gutenberg editor). + * + * DO NOT expose ALL settings e.g. be sure to never expose the API key. + * + * @since 3.0.0 + * @since 3.4.0 Add pluginVersion and wpVersion. + * @since 6.0.0 Make static and add integrationMethod. + * + * @return \WP_REST_Response + */ + public static function restApiResponse() + { + global $wp_version; + + return new \WP_REST_Response([ + 'apiKey' => get_option('beyondwords_api_key', ''), + 'integrationMethod' => IntegrationMethod::getIntegrationMethod(), + 'pluginVersion' => BEYONDWORDS__PLUGIN_VERSION, + 'projectId' => get_option('beyondwords_project_id', ''), + 'preselect' => get_option(PreselectGenerateAudio::OPTION_NAME, PreselectGenerateAudio::DEFAULT_PRESELECT), // phpcs:ignore Generic.Files.LineLength.TooLong + 'projectLanguageCode' => get_option('beyondwords_project_language_code', ''), + 'projectBodyVoiceId' => get_option('beyondwords_project_body_voice_id', ''), + 'restUrl' => get_rest_url(), + 'wpVersion' => $wp_version, + ]); + } + + /** + * Dismiss review notice. + * + * @since 5.4.0 + * @since 6.0.0 Make static. + * + * @return \WP_REST_Response + */ + public static function dismissReviewNotice() + { + $success = update_option('beyondwords_notice_review_dismissed', gmdate(\DateTime::ATOM)); + + return new \WP_REST_Response( + [ + 'success' => $success + ], + $success ? 200 : 500 + ); + } + + /** + * Register the settings script. + * + * @since 5.0.0 + * @since 6.0.0 Make static. + * + * @param string $hook Page hook + * + * @return void + */ + public static function enqueueScripts($hook) + { + if ($hook === 'settings_page_beyondwords') { + // jQuery UI JS + wp_enqueue_script('jquery-ui-core');// enqueue jQuery UI Core + wp_enqueue_script('jquery-ui-tabs');// enqueue jQuery UI Tabs + + // Plugin settings JS + wp_register_script( + 'beyondwords-settings', + BEYONDWORDS__PLUGIN_URI . 'build/settings.js', + ['jquery', 'jquery-ui-core', 'jquery-ui-tabs', 'underscore', 'tom-select'], + BEYONDWORDS__PLUGIN_VERSION, + true + ); + + // Tom Select JS + wp_enqueue_script( + 'tom-select', + 'https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js', // phpcs:ignore + [], + '2.2.2', + true + ); + + // Plugin settings CSS + wp_enqueue_style( + 'beyondwords-settings', + BEYONDWORDS__PLUGIN_URI . 'src/Component/Settings/settings.css', + 'forms', + BEYONDWORDS__PLUGIN_VERSION + ); + + // Tom Select CSS + wp_enqueue_style( + 'tom-select', + 'https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css', // phpcs:ignore + false, + BEYONDWORDS__PLUGIN_VERSION + ); + + /** + * Localize the script to handle ajax requests + */ + wp_add_inline_script( + 'beyondwords-settings', + ' + var beyondwordsData = beyondwordsData || {}; + beyondwordsData.nonce = "' . wp_create_nonce('wp_rest') . '"; + beyondwordsData.root = "' . esc_url_raw(rest_url()) . '"; + ', + 'before', + ); + + wp_enqueue_script('beyondwords-settings'); + } + } +} \ No newline at end of file diff --git a/src/Component/Settings/SettingsUtils.php b/src/Component/Settings/SettingsUtils.php index caf6d680..fbb8fe14 100755 --- a/src/Component/Settings/SettingsUtils.php +++ b/src/Component/Settings/SettingsUtils.php @@ -34,7 +34,7 @@ class SettingsUtils * * @return string[] Array of post type names. **/ - public static function getConsideredPostTypes() + public static function getConsideredPostTypes(): array { $postTypes = get_post_types(); @@ -80,7 +80,7 @@ public static function getConsideredPostTypes() * * @return string[] Array of post type names. **/ - public static function getCompatiblePostTypes() + public static function getCompatiblePostTypes(): array { $postTypes = SettingsUtils::getConsideredPostTypes(); @@ -117,7 +117,7 @@ public static function getCompatiblePostTypes() * * @return string[] Array of post type names. **/ - public static function getIncompatiblePostTypes() + public static function getIncompatiblePostTypes(): array { $postTypes = SettingsUtils::getConsideredPostTypes(); @@ -141,7 +141,7 @@ public static function getIncompatiblePostTypes() * * @return boolean */ - public static function hasApiCreds() + public static function hasApiCreds(): bool { $projectId = trim(strval(get_option('beyondwords_project_id'))); $apiKey = trim(strval(get_option('beyondwords_api_key'))); @@ -161,7 +161,7 @@ public static function hasApiCreds() * * @return boolean */ - public static function hasValidApiConnection() + public static function hasValidApiConnection(): bool { return boolval(get_option('beyondwords_valid_api_connection')); } @@ -175,7 +175,7 @@ public static function hasValidApiConnection() * * @return boolean **/ - public static function validateApiConnection() + public static function validateApiConnection(): bool { // This may have been left over from previous versions delete_transient('beyondwords_validate_api_connection'); @@ -240,7 +240,7 @@ public static function validateApiConnection() * * @return string */ - public static function colorInput($label, $name, $value) + public static function colorInput(string $label, string $name, string $value): void { ?>
@@ -273,7 +273,7 @@ class="small-text" * * @return void **/ - public static function addSettingsErrorMessage($message, $errorId = '') + public static function addSettingsErrorMessage(string $message, string $errorId = ''): void { $errors = wp_cache_get('beyondwords_settings_errors', 'beyondwords'); diff --git a/src/Component/Settings/SettingsUtils.php.bak b/src/Component/Settings/SettingsUtils.php.bak new file mode 100755 index 00000000..caf6d680 --- /dev/null +++ b/src/Component/Settings/SettingsUtils.php.bak @@ -0,0 +1,292 @@ + + * @since 3.5.0 + */ +class SettingsUtils +{ + /** + * Get the post types BeyondWords will consider for compatibility. + * + * We don't consider many of the core built-in post types for compatibity + * because they don't support the features we need such as titles, body, + * custom fields, etc. + * + * @since 3.7.0 + * @since 4.5.0 Renamed from getAllowedPostTypes to getConsideredPostTypes. + * @since 4.6.2 Exclude wp_font_face and wp_font_family from considered post types. + * + * @static + * + * @return string[] Array of post type names. + **/ + public static function getConsideredPostTypes() + { + $postTypes = get_post_types(); + + $skip = [ + 'attachment', + 'custom_css', + 'customize_changeset', + 'nav_menu_item', + 'oembed_cache', + 'revision', + 'user_request', + 'wp_block', + 'wp_font_face', + 'wp_font_family', + 'wp_template', + 'wp_template_part', + 'wp_global_styles', + 'wp_navigation', + ]; + + // Remove the skipped post types + $postTypes = array_diff($postTypes, $skip); + + return array_values($postTypes); + } + + /** + * Get the post types that are compatible with BeyondWords. + * + * - Start with the considered post types + * - Allow publishers to filter the list + * - Filter again, removing any that are incompatible + * + * @since 3.0.0 + * @since 3.2.0 Removed $output parameter to always output names, never objects. + * @since 3.2.0 Added `beyondwords_post_types` filter. + * @since 3.5.0 Moved from Core\Utils to Component\Settings\SettingsUtils. + * @since 3.7.0 Refactored forbidden/allowed/supported post type methods to improve site health debugging info. + * @since 4.5.0 Renamed from getSupportedPostTypes to getCompatiblePostTypes. + * @since 5.0.0 Remove beyondwords_post_types filter. + * + * @static + * + * @return string[] Array of post type names. + **/ + public static function getCompatiblePostTypes() + { + $postTypes = SettingsUtils::getConsideredPostTypes(); + + /** + * Filters the post types supported by BeyondWords. + * + * This defaults to all registered post types with 'custom-fields' in their 'supports' array. + * + * If any of the supplied post types do not support custom fields then they will be removed + * from the array. + * + * @since 3.3.3 Introduced as beyondwords_post_types + * @since 4.3.0 Renamed from beyondwords_post_types to beyondwords_settings_post_types. + * + * @param string[] The post types supported by BeyondWords. + */ + $postTypes = apply_filters('beyondwords_settings_post_types', $postTypes); + + // Remove incompatible post types + $postTypes = array_diff($postTypes, SettingsUtils::getIncompatiblePostTypes()); + + return array_values($postTypes); + } + + /** + * Get the post types that are incompatible with BeyondWords. + * + * The requirements are: + * - Must support Custom Fields. + * + * @since 4.5.0 + * + * @static + * + * @return string[] Array of post type names. + **/ + public static function getIncompatiblePostTypes() + { + $postTypes = SettingsUtils::getConsideredPostTypes(); + + // Filter the array, removing unsupported post types + $postTypes = array_filter($postTypes, function ($postType) { + if (post_type_supports($postType, 'custom-fields')) { + return false; + } + + return true; + }); + + return array_values($postTypes); + } + + /** + * Do we have both an API Key and Project ID? + * + * @since 5.2.0 + * @static + * + * @return boolean + */ + public static function hasApiCreds() + { + $projectId = trim(strval(get_option('beyondwords_project_id'))); + $apiKey = trim(strval(get_option('beyondwords_api_key'))); + + return strlen($projectId) && strlen($apiKey); + } + + /** + * Do we have a valid REST API connection for the BeyondWords REST API? + * + * Note that this only whether a valid REST API connection was made when + * the API Key was supplied. The API connection may be invalidated at a later + * date e.g. if the API Key is revoked. + * + * @since 5.2.0 + * @static + * + * @return boolean + */ + public static function hasValidApiConnection() + { + return boolval(get_option('beyondwords_valid_api_connection')); + } + + /** + * Validate the BeyondWords REST API connection. + * + * @since 5.0.0 + * @since 5.2.0 Moved from Sync class into SettingsUtils class. + * @static + * + * @return boolean + **/ + public static function validateApiConnection() + { + // This may have been left over from previous versions + delete_transient('beyondwords_validate_api_connection'); + + // Assume invalid connection + delete_option('beyondwords_valid_api_connection'); + + $projectId = get_option('beyondwords_project_id'); + $apiKey = get_option('beyondwords_api_key'); + + if (! $projectId || ! $apiKey) { + return false; + } + + $url = sprintf('%s/projects/%d', Environment::getApiUrl(), $projectId); + + $response = ApiClient::callApi( + new Request('GET', $url) + ); + + $statusCode = wp_remote_retrieve_response_code($response); + + if ($statusCode === 200) { + update_option('beyondwords_valid_api_connection', gmdate(\DateTime::ATOM), false); + wp_cache_set('beyondwords_sync_to_wordpress', ['all'], 'beyondwords', 60); + return true; + } + + // Cancel any syncs + wp_cache_delete('beyondwords_sync_to_wordpress', 'beyondwords'); + + $debug = sprintf( + '%s: %s', + wp_remote_retrieve_response_code($response), + wp_remote_retrieve_body($response) + ); + + self::addSettingsErrorMessage( + sprintf( + /* translators: %s is replaced with the BeyondWords REST API response debug data */ + __( + 'We were unable to validate your BeyondWords REST API connection.
Please check your project ID and API key, save changes, and contact us for support if this message remains.

BeyondWords REST API Response:
%s', // phpcs:ignore Generic.Files.LineLength.TooLong + 'speechkit' + ), + $debug, + ), + 'Settings/ValidApiConnection' + ); + + return false; + } + + /** + * A color input. + * + * @since 5.0.0 + * @static + * + * @param string $label Content for the `