PluginProbe
WANotifier for Forms and Actions / 3.0.1
WANotifier for Forms and Actions v3.0.1
3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 1.0.3 All 66 releases
notifier / includes / classes / class-notifier-migration.php

class-notifier-migration.php in WANotifier for Forms and Actions 3.0.1, at includes/classes/class-notifier-migration.php

346 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 /**
7 * Migration class for WANotifier plugin v3
8 *
9 * Migration flow (batch — all slugs in one redirect):
10 *
11 * 1. Migration page shows a list of legacy triggers grouped by integration slug.
12 * 2. A single "Connect & Migrate" button redirects the user's browser to the SaaS
13 * batch migration page at /integrations/migrate/, passing all slugs, their connect
14 * tokens, the old API key, and a return_url.
15 * 3. SaaS: shows a single authorization page listing all integrations. The WA account
16 * is resolved automatically from the old API key — no account selector needed.
17 * 4. User clicks "Connect & Migrate All". SaaS loops over each slug:
18 * - Creates a connection row (save_connection).
19 * - Calls the plugin's /wp-json/notifier/v1/{slug}/connect to deliver credentials.
20 * - Fetches legacy trigger definitions from /wp-json/notifier/v1/legacy-triggers?slug={slug}.
21 * - Runs process_migrate_trigger() for each trigger, which saves the new trigger config
22 * and updates the corresponding Notifications' trigger references from the old format
23 * (e.g. wp_abc12_woo_order_new) to the new format ({connection_id}:{trigger_slug}).
24 * 5. SaaS redirects back to the plugin's migration page (return_url).
25 * 6. Plugin detects all slugs are connected and shows "Finalize Migration" button.
26 * 7. User clicks "Finalize Migration" (AJAX):
27 * - Calls /api/v1/disconnect (old API key) to remove legacy trigger schema from SaaS.
28 * - Permanently deletes all wa_notifier_trigger CPT posts.
29 * - Marks migration complete.
30 *
31 * Migration state is tracked via the `notifier_migration_status` option:
32 * - '' / not set : migration not started
33 * - 'complete' : migration done
34 *
35 * @package Notifier
36 */
37 class Notifier_Migration {
38
39 const STATUS_OPTION = 'notifier_migration_status';
40
41 /**
42 * Integration slugs that belong to the standalone WANotifier for WooCommerce plugin.
43 */
44 const WOO_SLUGS = array( 'woocommerce', 'wcar' );
45
46 // ========================================
47 // 1. INITIALIZATION
48 // ========================================
49
50 public static function init() {
51 add_action( 'admin_notices', array( __CLASS__, 'show_migration_notice' ) );
52 add_action( 'wp_ajax_notifier_dismiss_migration_notice', array( __CLASS__, 'ajax_dismiss_notice' ) );
53 add_action( 'current_screen', array( __CLASS__, 'maybe_capture_used_meta' ) );
54 }
55
56 // ========================================
57 // 2. MIGRATION STATUS HELPERS
58 // ========================================
59
60 public static function is_migration_complete() {
61 return 'complete' === get_option( self::STATUS_OPTION, '' );
62 }
63
64 public static function is_migration_dismissed() {
65 return 'dismissed' === get_option( 'notifier_migration_notice_dismissed', '' );
66 }
67
68 /**
69 * Check whether there are any legacy CPT triggers to migrate.
70 *
71 * @return bool
72 */
73 public static function has_legacy_triggers() {
74 $count = wp_count_posts( 'wa_notifier_trigger' );
75 return ! empty( $count->publish ) && (int) $count->publish > 0;
76 }
77
78 /**
79 * Get all unique integration slugs that have legacy triggers.
80 *
81 * @return string[]
82 */
83 public static function get_slugs_with_legacy_triggers() {
84 $posts = get_posts( array(
85 'post_type' => 'wa_notifier_trigger',
86 'post_status' => 'publish',
87 'numberposts' => -1,
88 ) );
89
90 $slugs = array();
91 foreach ( $posts as $post ) {
92 $trigger_id = get_post_meta( $post->ID, NOTIFIER_PREFIX . 'trigger', true );
93 $base_trigger = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
94 $slug = Notifier_Notification_Triggers::get_integration_slug_for_trigger( $base_trigger );
95 if ( $slug && ! in_array( $slug, $slugs, true ) ) {
96 $slugs[] = $slug;
97 }
98 }
99
100 return $slugs;
101 }
102
103 /**
104 * Get only non-Woo/WCAR slugs that have legacy triggers.
105 *
106 * @return string[]
107 */
108 public static function get_non_woo_slugs_with_legacy_triggers() {
109 return array_values( array_diff( self::get_slugs_with_legacy_triggers(), self::WOO_SLUGS ) );
110 }
111
112 /**
113 * Determine the trigger scenario for migration UI.
114 *
115 * @return string 'woo_only' | 'mixed' | 'non_woo_only'
116 */
117 public static function get_trigger_scenario() {
118 $all_slugs = self::get_slugs_with_legacy_triggers();
119 $non_woo_slugs = array_diff( $all_slugs, self::WOO_SLUGS );
120 $woo_slugs = array_intersect( $all_slugs, self::WOO_SLUGS );
121
122 if ( ! empty( $woo_slugs ) && empty( $non_woo_slugs ) ) {
123 return 'woo_only';
124 }
125
126 if ( ! empty( $woo_slugs ) && ! empty( $non_woo_slugs ) ) {
127 return 'mixed';
128 }
129
130 return 'non_woo_only';
131 }
132
133 /**
134 * Check if all non-Woo slugs with legacy triggers are now connected.
135 *
136 * @return bool
137 */
138 public static function all_non_woo_slugs_connected() {
139 $slugs = self::get_non_woo_slugs_with_legacy_triggers();
140 if ( empty( $slugs ) ) {
141 return true;
142 }
143 foreach ( $slugs as $slug ) {
144 if ( ! Notifier_Connection::is_connected( $slug ) ) {
145 return false;
146 }
147 }
148 return true;
149 }
150
151 // ========================================
152 // 3. CONNECT URL FOR MIGRATION
153 // ========================================
154
155 /**
156 * Build the SaaS batch migration URL for all slugs with legacy triggers.
157 *
158 * Redirects to /integrations/migrate/ with all slugs and a base64-encoded
159 * payload containing the connect tokens, old API key, and return URL.
160 * Base64 encoding keeps the URL clean and avoids query-string escaping issues
161 * with JSON and special characters in URLs.
162 *
163 * @return string
164 */
165 public static function get_migrate_connect_url() {
166 $slugs = self::get_non_woo_slugs_with_legacy_triggers();
167 $api_key = trim( get_option( 'notifier_api_key', '' ) );
168
169 $connect_tokens = array();
170 foreach ( $slugs as $slug ) {
171 $connect_tokens[ $slug ] = Notifier_Connection::get_or_create_connect_token( $slug );
172 }
173
174 $return_url = admin_url( 'admin.php?page=notifier-migration' );
175 $saas_base = untrailingslashit( NOTIFIER_APP_BASE_URL );
176
177 $payload = base64_encode( wp_json_encode( array(
178 'store_url' => site_url(),
179 'slugs' => $slugs,
180 'connect_tokens' => $connect_tokens,
181 'return_url' => $return_url,
182 'api_key' => $api_key,
183 ) ) );
184
185 return $saas_base . '/integrations/migrate/?payload=' . rawurlencode( $payload );
186 }
187
188 // ========================================
189 // 4. ADMIN NOTICE
190 // ========================================
191
192 public static function show_migration_notice() {
193 if ( self::is_migration_complete() ) {
194 return;
195 }
196
197 if ( self::is_migration_dismissed() ) {
198 return;
199 }
200
201 if ( ! self::has_legacy_triggers() ) {
202 return;
203 }
204
205 if ( ! current_user_can( 'manage_options' ) ) {
206 return;
207 }
208
209 // Don't show on the migration page itself.
210 $screen = get_current_screen();
211 if ( $screen && false !== strpos( $screen->id, 'notifier-migration' ) ) {
212 return;
213 }
214
215 $migration_url = admin_url( 'admin.php?page=notifier-migration' );
216 ?>
217 <div class="notice notice-warning is-dismissible" id="notifier-migration-notice">
218 <p>
219 <strong><?php esc_html_e( 'WANotifier v3: Action Required', 'notifier' ); ?></strong>
220 &mdash;
221 <?php esc_html_e( 'Your existing triggers need to be migrated to the new integration system. They will continue to work until you migrate.', 'notifier' ); ?>
222 <a href="<?php echo esc_url( $migration_url ); ?>" class="button button-primary" style="margin-left:8px;">
223 <?php esc_html_e( 'Migrate Now', 'notifier' ); ?>
224 </a>
225 </p>
226 </div>
227 <?php
228 }
229
230 public static function ajax_dismiss_notice() {
231 check_ajax_referer( 'notifier_dismiss_migration', 'nonce' );
232 update_option( 'notifier_migration_notice_dismissed', 'dismissed' );
233 wp_send_json_success();
234 }
235
236 // ========================================
237 // 5. POST-MIGRATION CLEANUP
238 // ========================================
239
240 /**
241 * Delete legacy trigger CPT posts, skipping Woo/WCAR triggers
242 * (those are handled by the standalone plugin's own migration).
243 */
244 public static function delete_legacy_trigger_posts() {
245 $posts = get_posts( array(
246 'post_type' => 'wa_notifier_trigger',
247 'post_status' => array( 'publish', 'draft', 'trash' ),
248 'numberposts' => -1,
249 ) );
250
251 foreach ( $posts as $post ) {
252 $trigger_id = get_post_meta( $post->ID, NOTIFIER_PREFIX . 'trigger', true );
253 $base_trigger = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
254 $slug = Notifier_Notification_Triggers::get_integration_slug_for_trigger( $base_trigger );
255
256 if ( in_array( $slug, self::WOO_SLUGS, true ) ) {
257 continue;
258 }
259
260 wp_delete_post( $post->ID, true );
261 }
262 }
263
264 /**
265 * Remove the site's legacy trigger schema from the SaaS.
266 *
267 * The legacy system stored all triggers for a site under source='wp', so one
268 * call removes the entire site entry from wa_notifier_api_sources_and_triggers_*
269 * user meta.
270 *
271 * @param string $api_key Old WANotifier API key.
272 */
273 public static function disconnect_legacy_on_saas( $api_key ) {
274 $saas_url = untrailingslashit( NOTIFIER_APP_BASE_URL );
275
276 $response = wp_remote_post( $saas_url . '/' . NOTIFIER_APP_API_PATH . '/disconnect?key=' . rawurlencode( $api_key ), array(
277 'timeout' => 15,
278 'headers' => array( 'Content-Type' => 'application/json' ),
279 'body' => wp_json_encode( array(
280 'site_url' => site_url(),
281 'source' => 'wp',
282 ) ),
283 'sslverify' => true,
284 ) );
285
286 if ( is_wp_error( $response ) ) {
287 error_log( 'WANotifier migration: disconnect_legacy_on_saas failed: ' . $response->get_error_message() );
288 }
289 }
290
291 // ========================================
292 // 6. META FIELD SELECTION CAPTURE
293 // ========================================
294
295 /**
296 * On page load, check for ?used_meta= query param and store the decoded
297 * merge tags and recipient fields in options for the WordPress meta field selection.
298 *
299 * The SaaS redirects back with a base64-encoded JSON payload in the used_meta
300 * query parameter containing the merge tags and recipient fields used in Notifications.
301 */
302 public static function maybe_capture_used_meta() {
303 if ( ! current_user_can( 'manage_options' ) ) {
304 return;
305 }
306
307 $screen = get_current_screen();
308 if ( ! $screen || false === strpos( $screen->id, 'notifier' ) ) {
309 return;
310 }
311
312 if ( ! isset( $_GET['used_meta'] ) ) {
313 return;
314 }
315
316 $raw = wp_unslash( $_GET['used_meta'] );
317 $decoded = json_decode( base64_decode( $raw ), true );
318
319 if ( ! is_array( $decoded ) ) {
320 return;
321 }
322
323 // Include WP meta (post_meta_*, user_meta_*) and hook-added fields; exclude Woo meta.
324 $wp_pattern = '/^((?!woo_).+_meta_|user_meta_)/';
325 $is_wp_field = function( $id ) use ( $wp_pattern ) {
326 $id = is_string( $id ) ? $id : '';
327 return $id !== '' && ( preg_match( $wp_pattern, $id ) || strpos( $id, 'woo_' ) !== 0 );
328 };
329
330 if ( isset( $decoded['merge_tags'] ) && is_array( $decoded['merge_tags'] ) ) {
331 $wp_tags = array_values( array_filter( $decoded['merge_tags'], $is_wp_field ) );
332 if ( ! empty( $wp_tags ) ) {
333 update_option( 'notifier_wp_enabled_merge_tags', array_map( 'sanitize_text_field', $wp_tags ) );
334 }
335 }
336
337 if ( isset( $decoded['recipient_fields'] ) && is_array( $decoded['recipient_fields'] ) ) {
338 $wp_fields = array_values( array_filter( $decoded['recipient_fields'], $is_wp_field ) );
339 if ( ! empty( $wp_fields ) ) {
340 update_option( 'notifier_wp_enabled_recipient_fields', array_map( 'sanitize_text_field', $wp_fields ) );
341 }
342 }
343 }
344
345 }
346