* @since 5.0.0 */ namespace Beyondwords\Wordpress\Component\Settings; use Beyondwords\Wordpress\Core\ApiClient; use Beyondwords\Wordpress\Core\Environment; use Symfony\Component\PropertyAccess\PropertyAccess; /** * Sync * * @since 5.0.0 * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ class Sync { /** * Map settings. * * @since 5.0.0 */ public const MAP_SETTINGS = [ // Player 'beyondwords_player_style' => '[player_settings][player_style]', 'beyondwords_player_theme' => '[player_settings][theme]', 'beyondwords_player_theme_dark' => '[player_settings][dark_theme]', 'beyondwords_player_theme_light' => '[player_settings][light_theme]', 'beyondwords_player_theme_video' => '[player_settings][video_theme]', 'beyondwords_player_call_to_action' => '[player_settings][call_to_action]', 'beyondwords_player_widget_style' => '[player_settings][widget_style]', 'beyondwords_player_widget_position' => '[player_settings][widget_position]', 'beyondwords_player_skip_button_style' => '[player_settings][skip_button_style]', 'beyondwords_player_clickable_sections' => '[player_settings][segment_playback_enabled]', // Project 'beyondwords_project_auto_publish_enabled' => '[project][auto_publish_enabled]', 'beyondwords_project_language_code' => '[project][language]', 'beyondwords_project_body_voice_id' => '[project][body][voice][id]', 'beyondwords_project_body_voice_speaking_rate' => '[project][body][voice][speaking_rate]', 'beyondwords_project_title_enabled' => '[project][title][enabled]', 'beyondwords_project_title_voice_id' => '[project][title][voice][id]', 'beyondwords_project_title_voice_speaking_rate' => '[project][title][voice][speaking_rate]', // Video 'beyondwords_video_enabled' => '[video_settings][enabled]', ]; /** * Init. * * @since 5.0.0 * @since 6.0.0 Make static. */ public static function init() { add_action('load-settings_page_beyondwords', [self::class, 'syncToWordPress'], 30); if (Environment::hasAutoSyncSettings()) { add_action('load-settings_page_beyondwords', [self::class, 'scheduleSyncs'], 20); add_action('shutdown', [self::class, 'syncToDashboard']); } } /** * Should we schedule a sync on the current settings tab? * * @since 5.0.0 * @since 5.2.0 Remove API creds validation. * @since 6.0.0 Make static. * * @return void */ public static function scheduleSyncs() { $tab = Settings::getActiveTab(); $endpoints = []; switch ($tab) { case 'content': case 'voices': $endpoints = ['project']; break; case 'player': $endpoints = ['player_settings', 'video_settings']; break; } if (count($endpoints)) { wp_cache_set('beyondwords_sync_to_wordpress', $endpoints, 'beyondwords', 60); } } /** * Sync from the dashboard/BeyondWords REST API to WordPress. * * @since 5.0.0 Introduced. * @since 5.4.0 Stop saving language ID – we only need the ISO code now. * @since 6.0.0 Make static. * * @return void **/ public static function syncToWordPress() { $sync_to_wordpress = wp_cache_get('beyondwords_sync_to_wordpress', 'beyondwords'); wp_cache_delete('beyondwords_sync_to_wordpress', 'beyondwords'); if (empty($sync_to_wordpress) || ! is_array($sync_to_wordpress)) { return; } $responses = []; if (! empty(array_intersect($sync_to_wordpress, ['all', 'project']))) { $project = ApiClient::getProject(); if (! empty($project)) { $responses['project'] = $project; } } if (! empty(array_intersect($sync_to_wordpress, ['all', 'player_settings']))) { $player_settings = ApiClient::getPlayerSettings(); if (! empty($player_settings)) { $responses['player_settings'] = $player_settings; } } if (! empty(array_intersect($sync_to_wordpress, ['all', 'video_settings']))) { $video_settings = ApiClient::getVideoSettings(); if (! empty($video_settings)) { $responses['video_settings'] = $video_settings; } } // Update WordPress options using the REST API response data. self::updateOptionsFromResponses($responses); } /** * Update WordPress options from REST API responses. * * @since 5.0.0 * @since 6.0.0 Make static. * @since 6.0.3 Check path is readable before getting value. * * @return boolean **/ public static function updateOptionsFromResponses($responses) { if (empty($responses)) { add_settings_error( 'beyondwords_settings', 'beyondwords_settings', ' Unexpected BeyondWords REST API response.', 'error' ); return false; } $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->disableExceptionOnInvalidPropertyPath() ->getPropertyAccessor(); $updated = false; foreach (self::MAP_SETTINGS as $optionName => $path) { if ($propertyAccessor->isReadable($responses, $path)) { $value = $propertyAccessor->getValue($responses, $path); if ($value !== null) { update_option($optionName, $value, false); $updated = true; } } } return $updated; } /** * Sync from WordPress to the dashboard/BeyondWords REST API. * * @since 5.0.0 * @since 6.0.0 Make static. * * @return void **/ public static function syncToDashboard() { $options = wp_cache_get('beyondwords_sync_to_dashboard', 'beyondwords'); wp_cache_delete('beyondwords_sync_to_dashboard', 'beyondwords'); if (empty($options) || ! is_array($options)) { return; } $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->disableExceptionOnInvalidPropertyPath() ->getPropertyAccessor(); $settings = []; foreach ($options as $option) { if (self::shouldSyncOptionToDashboard($option)) { $propertyAccessor->setValue( $settings, self::MAP_SETTINGS[$option], get_option($option) ); } } // Sync player settings back to API if (isset($settings['player_settings'])) { ApiClient::updatePlayerSettings($settings['player_settings']); add_settings_error( 'beyondwords_settings', 'beyondwords_settings', ' Player settings synced from WordPress to the BeyondWords dashboard.', // phpcs:ignore Generic.Files.LineLength.TooLong 'success' ); } // Sync title voice back to API if (in_array('beyondwords_project_title_voice_speaking_rate', $options)) { $value = $propertyAccessor->getValue( $settings, self::MAP_SETTINGS['beyondwords_project_title_voice_speaking_rate'] ); if ($value !== null) { $titleVoiceId = get_option('beyondwords_project_title_voice_id'); ApiClient::updateVoice($titleVoiceId, [ 'speaking_rate' => (int)$value, ]); } } // Sync body voice back to API if (in_array('beyondwords_project_body_voice_speaking_rate', $options)) { $value = $propertyAccessor->getValue( $settings, self::MAP_SETTINGS['beyondwords_project_body_voice_speaking_rate'] ); if ($value !== null) { $bodyVoiceId = get_option('beyondwords_project_body_voice_id'); ApiClient::updateVoice($bodyVoiceId, [ 'speaking_rate' => (int)$value, ]); } } // Sync project settings back to API if (isset($settings['project'])) { // Don't send speaking rates back to /project endpoint $titleSpeakingRate = $propertyAccessor->getValue( $settings, self::MAP_SETTINGS['beyondwords_project_title_voice_speaking_rate'] ); if ($titleSpeakingRate) { unset($settings['project']['title']['voice']['speaking_rate']); } // Don't send speaking rates back to /project endpoint $bodySpeakingRate = $propertyAccessor->getValue( $settings, self::MAP_SETTINGS['beyondwords_project_body_voice_speaking_rate'] ); if ($bodySpeakingRate) { unset($settings['project']['body']['voice']['speaking_rate']); } ApiClient::updateProject($settings['project']); add_settings_error( 'beyondwords_settings', 'beyondwords_settings', ' Project settings synced from WordPress to the BeyondWords dashboard.', // phpcs:ignore Generic.Files.LineLength.TooLong 'success' ); } } /** * Should we sync this option to the dashboard? * * @since 5.0.0 * @since 6.0.0 Make static. * * @param string $option_name Option name. * * @return void **/ public static function shouldSyncOptionToDashboard($option_name) { if (! array_key_exists($option_name, self::MAP_SETTINGS)) { return false; } // Check the option was updated without error $hasErrors = get_settings_errors($option_name); return is_array($hasErrors) && count($hasErrors) === 0; } /** * Sync an option to the WordPress dashboard. * * Note that this DOES NOT make the API call, it instead flags the field * as one to sync so that we can group fields and send them in a single * request to the BeyondWords REST API. * * @since 5.0.0 * * @return void **/ public static function syncOptionToDashboard($optionName) { $options = wp_cache_get('beyondwords_sync_to_dashboard', 'beyondwords'); if (! is_array($options)) { $options = []; } $options[] = $optionName; $options = array_unique($options); wp_cache_set('beyondwords_sync_to_dashboard', $options, 'beyondwords', 60); } }