PluginProbe
WANotifier for Forms and Actions / 3.1.1
WANotifier for Forms and Actions v3.1.1
3.1.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 All 67 releases
notifier / includes / classes / class-notifier-migration.php

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

347 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 'rest_url' => rest_url(),
180 'slugs' => $slugs,
181 'connect_tokens' => $connect_tokens,
182 'return_url' => $return_url,
183 'api_key' => $api_key,
184 ) ) );
185
186 return $saas_base . '/integrations/migrate/?payload=' . rawurlencode( $payload );
187 }
188
189 // ========================================
190 // 4. ADMIN NOTICE
191 // ========================================
192
193 public static function show_migration_notice() {
194 if ( self::is_migration_complete() ) {
195 return;
196 }
197
198 if ( self::is_migration_dismissed() ) {
199 return;
200 }
201
202 if ( ! self::has_legacy_triggers() ) {
203 return;
204 }
205
206 if ( ! current_user_can( 'manage_options' ) ) {
207 return;
208 }
209
210 // Don't show on the migration page itself.
211 $screen = get_current_screen();
212 if ( $screen && false !== strpos( $screen->id, 'notifier-migration' ) ) {
213 return;
214 }
215
216 $migration_url = admin_url( 'admin.php?page=notifier-migration' );
217 ?>
218 <div class="notice notice-warning is-dismissible" id="notifier-migration-notice">
219 <p>
220 <strong><?php esc_html_e( 'WANotifier v3: Action Required', 'notifier' ); ?></strong>
221 &mdash;
222 <?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' ); ?>
223 <a href="<?php echo esc_url( $migration_url ); ?>" class="button button-primary" style="margin-left:8px;">
224 <?php esc_html_e( 'Migrate Now', 'notifier' ); ?>
225 </a>
226 </p>
227 </div>
228 <?php
229 }
230
231 public static function ajax_dismiss_notice() {
232 check_ajax_referer( 'notifier_dismiss_migration', 'nonce' );
233 update_option( 'notifier_migration_notice_dismissed', 'dismissed' );
234 wp_send_json_success();
235 }
236
237 // ========================================
238 // 5. POST-MIGRATION CLEANUP
239 // ========================================
240
241 /**
242 * Delete legacy trigger CPT posts, skipping Woo/WCAR triggers
243 * (those are handled by the standalone plugin's own migration).
244 */
245 public static function delete_legacy_trigger_posts() {
246 $posts = get_posts( array(
247 'post_type' => 'wa_notifier_trigger',
248 'post_status' => array( 'publish', 'draft', 'trash' ),
249 'numberposts' => -1,
250 ) );
251
252 foreach ( $posts as $post ) {
253 $trigger_id = get_post_meta( $post->ID, NOTIFIER_PREFIX . 'trigger', true );
254 $base_trigger = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
255 $slug = Notifier_Notification_Triggers::get_integration_slug_for_trigger( $base_trigger );
256
257 if ( in_array( $slug, self::WOO_SLUGS, true ) ) {
258 continue;
259 }
260
261 wp_delete_post( $post->ID, true );
262 }
263 }
264
265 /**
266 * Remove the site's legacy trigger schema from the SaaS.
267 *
268 * The legacy system stored all triggers for a site under source='wp', so one
269 * call removes the entire site entry from wa_notifier_api_sources_and_triggers_*
270 * user meta.
271 *
272 * @param string $api_key Old WANotifier API key.
273 */
274 public static function disconnect_legacy_on_saas( $api_key ) {
275 $saas_url = untrailingslashit( NOTIFIER_APP_BASE_URL );
276
277 $response = wp_remote_post( $saas_url . '/' . NOTIFIER_APP_API_PATH . '/disconnect?key=' . rawurlencode( $api_key ), array(
278 'timeout' => 15,
279 'headers' => array( 'Content-Type' => 'application/json' ),
280 'body' => wp_json_encode( array(
281 'site_url' => site_url(),
282 'source' => 'wp',
283 ) ),
284 'sslverify' => true,
285 ) );
286
287 if ( is_wp_error( $response ) ) {
288 error_log( 'WANotifier migration: disconnect_legacy_on_saas failed: ' . $response->get_error_message() );
289 }
290 }
291
292 // ========================================
293 // 6. META FIELD SELECTION CAPTURE
294 // ========================================
295
296 /**
297 * On page load, check for ?used_meta= query param and store the decoded
298 * merge tags and recipient fields in options for the WordPress meta field selection.
299 *
300 * The SaaS redirects back with a base64-encoded JSON payload in the used_meta
301 * query parameter containing the merge tags and recipient fields used in Notifications.
302 */
303 public static function maybe_capture_used_meta() {
304 if ( ! current_user_can( 'manage_options' ) ) {
305 return;
306 }
307
308 $screen = get_current_screen();
309 if ( ! $screen || false === strpos( $screen->id, 'notifier' ) ) {
310 return;
311 }
312
313 if ( ! isset( $_GET['used_meta'] ) ) {
314 return;
315 }
316
317 $raw = wp_unslash( $_GET['used_meta'] );
318 $decoded = json_decode( base64_decode( $raw ), true );
319
320 if ( ! is_array( $decoded ) ) {
321 return;
322 }
323
324 // Include WP meta (post_meta_*, user_meta_*) and hook-added fields; exclude Woo meta.
325 $wp_pattern = '/^((?!woo_).+_meta_|user_meta_)/';
326 $is_wp_field = function( $id ) use ( $wp_pattern ) {
327 $id = is_string( $id ) ? $id : '';
328 return $id !== '' && ( preg_match( $wp_pattern, $id ) || strpos( $id, 'woo_' ) !== 0 );
329 };
330
331 if ( isset( $decoded['merge_tags'] ) && is_array( $decoded['merge_tags'] ) ) {
332 $wp_tags = array_values( array_filter( $decoded['merge_tags'], $is_wp_field ) );
333 if ( ! empty( $wp_tags ) ) {
334 update_option( 'notifier_wp_enabled_merge_tags', array_map( 'sanitize_text_field', $wp_tags ) );
335 }
336 }
337
338 if ( isset( $decoded['recipient_fields'] ) && is_array( $decoded['recipient_fields'] ) ) {
339 $wp_fields = array_values( array_filter( $decoded['recipient_fields'], $is_wp_field ) );
340 if ( ! empty( $wp_fields ) ) {
341 update_option( 'notifier_wp_enabled_recipient_fields', array_map( 'sanitize_text_field', $wp_fields ) );
342 }
343 }
344 }
345
346 }
347