PluginProbe
BeyondWords – AI audio for publishers / 7.0.0
BeyondWords – AI audio for publishers v7.0.0
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / core / class-updater.php

class-updater.php in BeyondWords – AI audio for publishers 7.0.0, at src/core/class-updater.php

310 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin update routines: version-gated migrations run on every page load.
4 *
5 * @package BeyondWords\Core
6 * @since 3.0.0
7 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
8 */
9
10 declare( strict_types = 1 );
11
12 namespace BeyondWords\Core;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Version-gated migrations.
18 *
19 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
20 */
21 class Updater {
22
23 /**
24 * Run any pending migrations and update the recorded plugin version.
25 *
26 * The exact-version bail — not the `version_compare` gates — keeps the common
27 * path cheap: a pre-release like `7.0.0-beta.1` compares `< 7.0.0` forever.
28 */
29 public static function run(): void {
30 $version = get_option( 'beyondwords_version', '1.0.0' );
31
32 if ( BEYONDWORDS__PLUGIN_VERSION === $version ) {
33 return;
34 }
35
36 if ( version_compare( $version, '3.0.0', '<' ) ) {
37 self::migrate_settings();
38 }
39
40 if ( version_compare( $version, '3.7.0', '<' ) ) {
41 self::rename_plugin_settings();
42 }
43
44 if ( version_compare( $version, '7.0.0', '<' ) ) {
45 self::migrate_preselect_format();
46 self::delete_deprecated_options();
47 self::migrate_disabled_to_embed_none();
48 self::migrate_source_script_to_post_and_script();
49 }
50
51 // Record the activation timestamp the first time we run.
52 add_option( 'beyondwords_date_activated', gmdate( \DateTime::ATOM ), '', false );
53
54 // Always update the plugin version so FTP uploads and similar bypasses still register.
55 update_option( 'beyondwords_version', BEYONDWORDS__PLUGIN_VERSION );
56 }
57
58 /**
59 * Convert the legacy preselect option to the 7.0.0 mode-based format.
60 *
61 * Reuses the tolerant readers on `Preselect`, so it is idempotent — which
62 * keeps the re-runs pre-release builds trigger (`< 7.0.0` forever) safe.
63 *
64 * @since 7.0.0
65 */
66 public static function migrate_preselect_format(): void {
67 $preselect = get_option( 'beyondwords_preselect' );
68
69 if ( ! is_array( $preselect ) ) {
70 return;
71 }
72
73 $migrated = [];
74
75 foreach ( $preselect as $post_type => $value ) {
76 $post_type = (string) $post_type;
77 $single = [ $post_type => $value ];
78
79 $mode = \BeyondWords\Settings\Preselect::get_mode( $post_type, $single );
80
81 if ( \BeyondWords\Settings\Preselect::MODE_ALL === $mode ) {
82 $migrated[ $post_type ] = [ 'mode' => \BeyondWords\Settings\Preselect::MODE_ALL ];
83 } elseif ( \BeyondWords\Settings\Preselect::MODE_TERMS === $mode ) {
84 $terms = \BeyondWords\Settings\Preselect::get_selected_terms( $post_type, $single );
85
86 if ( ! empty( $terms ) ) {
87 $migrated[ $post_type ] = [
88 'mode' => \BeyondWords\Settings\Preselect::MODE_TERMS,
89 'terms' => $terms,
90 ];
91 }
92 }
93 // MODE_OFF (empty / unrecognised) → dropped.
94 }
95
96 update_option( 'beyondwords_preselect', $migrated, false );
97 }
98
99 /**
100 * v7.0.0: remove every option marked deprecated in `Utils::get_options()`.
101 *
102 * Multisite-aware because legacy installs may have stored these as site options.
103 *
104 * @since 7.0.0
105 */
106 public static function delete_deprecated_options(): void {
107 foreach ( Utils::get_options( 'deprecated' ) as $option ) {
108 if ( is_multisite() ) {
109 delete_site_option( $option );
110 } else {
111 delete_option( $option );
112 }
113 }
114 }
115
116 /**
117 * v7.0.0: migrate the legacy `beyondwords_disabled` opt-out to Embed "None".
118 *
119 * Carries the flag forward so previously-hidden players stay hidden; never
120 * overwrites an existing Embed value, and batches to keep memory bounded.
121 *
122 * @since 7.0.0
123 */
124 public static function migrate_disabled_to_embed_none(): void {
125 $batch = 100;
126
127 do {
128 $post_ids = get_posts(
129 [
130 'post_type' => 'any',
131 'post_status' => 'any',
132 'numberposts' => $batch,
133 'fields' => 'ids',
134 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
135 'meta_query' => [
136 [
137 'key' => 'beyondwords_disabled',
138 'value' => '1',
139 ],
140 ],
141 ]
142 );
143
144 foreach ( $post_ids as $post_id ) {
145 if ( '' === (string) get_post_meta( $post_id, 'beyondwords_embed', true ) ) {
146 update_post_meta(
147 $post_id,
148 'beyondwords_embed',
149 \BeyondWords\Editor\Components\SettingsFields::EMBED_NONE
150 );
151 }
152
153 delete_post_meta( $post_id, 'beyondwords_disabled' );
154 }
155
156 $found = count( $post_ids );
157 } while ( $found === $batch );
158 }
159
160 /**
161 * v7.0.0: normalise the removed script-only Source to Post + script.
162 *
163 * See doc/legacy-meta-migration.md. Batched to keep memory bounded, and
164 * idempotent because a migrated post no longer matches the query.
165 *
166 * @since 7.0.0
167 */
168 public static function migrate_source_script_to_post_and_script(): void {
169 $batch = 100;
170
171 do {
172 $post_ids = get_posts(
173 [
174 'post_type' => 'any',
175 'post_status' => 'any',
176 'numberposts' => $batch,
177 'fields' => 'ids',
178 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
179 'meta_query' => [
180 [
181 'key' => 'beyondwords_source',
182 'value' => \BeyondWords\Editor\Components\SettingsFields::LEGACY_SOURCE_SCRIPT,
183 ],
184 ],
185 ]
186 );
187
188 foreach ( $post_ids as $post_id ) {
189 // Pin the asset script-only was showing, so the player doesn't switch.
190 if ( '' === (string) get_post_meta( $post_id, 'beyondwords_embed', true ) ) {
191 $output = (string) get_post_meta( $post_id, 'beyondwords_output', true );
192
193 $audio = '' === $output
194 || \BeyondWords\Editor\Components\SettingsFields::output_includes_audio( $output );
195
196 update_post_meta(
197 $post_id,
198 'beyondwords_embed',
199 $audio
200 ? \BeyondWords\Editor\Components\SettingsFields::EMBED_AUDIO_SCRIPT
201 : \BeyondWords\Editor\Components\SettingsFields::EMBED_VIDEO_SCRIPT
202 );
203 }
204
205 update_post_meta(
206 $post_id,
207 'beyondwords_source',
208 \BeyondWords\Editor\Components\SettingsFields::SOURCE_POST_AND_SCRIPT
209 );
210 }
211
212 $found = count( $post_ids );
213 } while ( $found === $batch );
214 }
215
216 /**
217 * v3.0.0: migrate `speechkit_settings.*` array values to top-level options.
218 */
219 public static function migrate_settings(): void {
220 $old_settings = get_option( 'speechkit_settings', [] );
221
222 if ( ! is_array( $old_settings ) || empty( $old_settings ) ) {
223 return;
224 }
225
226 $settings_map = [
227 'speechkit_api_key' => 'speechkit_api_key',
228 'speechkit_id' => 'speechkit_project_id',
229 'speechkit_merge_excerpt' => 'speechkit_prepend_excerpt',
230 ];
231
232 foreach ( $settings_map as $old_key => $new_key ) {
233 if ( array_key_exists( $old_key, $old_settings ) && ! get_option( $new_key ) ) {
234 add_option( $new_key, $old_settings[ $old_key ] );
235 }
236 }
237
238 if ( false === get_option( 'speechkit_preselect' ) ) {
239 add_option( 'speechkit_preselect', self::construct_preselect_setting() );
240 }
241 }
242
243 /**
244 * Build a v3 preselect array from the v2 post-type + category settings.
245 *
246 * @return array<string,mixed>|false `false` when the legacy `speechkit_settings` option is missing.
247 */
248 public static function construct_preselect_setting(): array|false {
249 $old_settings = get_option( 'speechkit_settings', [] );
250
251 if ( ! is_array( $old_settings ) || empty( $old_settings ) ) {
252 return false;
253 }
254
255 $preselect = [];
256
257 if (
258 array_key_exists( 'speechkit_select_post_types', $old_settings )
259 && ! empty( $old_settings['speechkit_select_post_types'] )
260 ) {
261 $preselect = array_fill_keys( $old_settings['speechkit_select_post_types'], '1' );
262 }
263
264 if (
265 array_key_exists( 'speechkit_selected_categories', $old_settings )
266 && ! empty( $old_settings['speechkit_selected_categories'] )
267 ) {
268 $taxonomy = get_taxonomy( 'category' );
269
270 if ( $taxonomy && is_array( $taxonomy->object_type ) ) {
271 foreach ( $taxonomy->object_type as $post_type ) {
272 $preselect[ $post_type ] = [
273 'category' => $old_settings['speechkit_selected_categories'],
274 ];
275 }
276 }
277 }
278
279 return $preselect;
280 }
281
282 /**
283 * v3.7.0: copy `speechkit_*` option keys to `beyondwords_*`.
284 *
285 * Originals are left in place so plugin downgrades remain safe.
286 */
287 public static function rename_plugin_settings(): void {
288 $api_key = get_option( 'speechkit_api_key' );
289 $project_id = get_option( 'speechkit_project_id' );
290 $prepend_excerpt = get_option( 'speechkit_prepend_excerpt' );
291 $preselect = get_option( 'speechkit_preselect' );
292
293 if ( $api_key ) {
294 update_option( 'beyondwords_api_key', $api_key, false );
295 }
296
297 if ( $project_id ) {
298 update_option( 'beyondwords_project_id', $project_id, false );
299 }
300
301 if ( $prepend_excerpt ) {
302 update_option( 'beyondwords_prepend_excerpt', $prepend_excerpt, false );
303 }
304
305 if ( $preselect ) {
306 update_option( 'beyondwords_preselect', $preselect, false );
307 }
308 }
309 }
310