PluginProbe ʕ •ᴥ•ʔ
Admin Help Docs / 2.0.3
Admin Help Docs v2.0.3
2.0.3 2.0.2 2.0.1.1 trunk 1.4.3.2 2.0.0 2.0.0.1 2.0.0.2 2.0.1
admin-help-docs / inc / tabs / migrate.php
admin-help-docs / inc / tabs Last commit date
css 1 day ago js 1 day ago admin-menu.php 1 day ago documentation.php 1 day ago faq.php 1 day ago import.php 1 day ago migrate.php 1 day ago settings.php 1 day ago support.php 1 day ago
migrate.php
480 lines
1 <?php
2 /**
3 * Migrate Tab Loader
4 */
5
6 namespace PluginRx\AdminHelpDocs;
7
8 if ( ! defined( 'ABSPATH' ) ) exit;
9
10 class Migrate {
11
12 /**
13 * The single instance of the class
14 *
15 * @var self|null
16 */
17 private static ?Migrate $instance = null;
18
19
20 /**
21 * Get the singleton instance
22 *
23 * @return self
24 */
25 public static function instance() : self {
26 return self::$instance ??= new self();
27 } // End instance()
28
29
30 /**
31 * Migrate constructor.
32 *
33 * Private to enforce singleton pattern.
34 */
35 private function __construct() {
36
37 add_action( 'wp_ajax_helpdocs_migrate_list_posts', [ $this, 'ajax_list_posts' ] );
38 add_action( 'wp_ajax_helpdocs_migrate_import_posts', [ $this, 'ajax_import_posts' ] );
39 add_action( 'wp_ajax_helpdocs_dismiss_migrate_notice', [ $this, 'ajax_dismiss_notice' ] );
40 add_action( 'admin_notices', [ $this, 'render_migration_notices' ] );
41 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_notice_script' ] );
42
43 } // End __construct()
44
45
46 /**
47 * Known source plugins eligible for a migration notice.
48 *
49 * Each entry:
50 * - post_type The source plugin's post type slug
51 * - plugin_label The plugin's display name, used in the notice sentence
52 * - doc_label The label to use for this post type in the migrate dropdown
53 * - pages admin.php ?page= slugs where the notice should render
54 * - screens get_current_screen()->id values where the notice should render
55 *
56 * @return array
57 */
58 public static function known_sources() : array {
59 $sources = [
60 'wp-help' => [
61 'post_type' => 'wp-help',
62 'plugin_label' => __( 'WP Help', 'admin-help-docs' ),
63 'doc_label' => __( 'WP Help Documents', 'admin-help-docs' ),
64 'pages' => [ 'wp-help-documents' ],
65 'screens' => [ 'edit-wp-help' ],
66 'notice_message' => __( 'WP Help has not been updated in a couple of years. You can migrate your existing docs to Admin Help Docs in a couple of clicks, no need to recreate them.', 'admin-help-docs' ),
67 ],
68 ];
69
70 return apply_filters( 'helpdocs_migrate_known_sources', $sources );
71 } // End known_sources()
72
73
74 /**
75 * Get post types eligible as a migration source (excludes our own types and non-public built-ins)
76 *
77 * @return array Associative array of post_type => label
78 */
79 public static function eligible_post_types() : array {
80 $excluded = [
81 HelpDocs::$post_type,
82 Imports::$post_type,
83 'attachment',
84 'revision',
85 'nav_menu_item',
86 'wp_block',
87 'wp_template',
88 'wp_template_part',
89 'wp_navigation',
90 'wp_font_family',
91 'wp_font_face',
92 'wp_global_styles',
93 'customize_changeset',
94 'oembed_cache',
95 'user_request',
96 ];
97
98 $label_overrides = [];
99 foreach ( self::known_sources() as $source ) {
100 if ( ! empty( $source[ 'doc_label' ] ) ) {
101 $label_overrides[ $source[ 'post_type' ] ] = $source[ 'doc_label' ];
102 }
103 }
104
105 $post_types = get_post_types( [ 'show_ui' => true ], 'objects' );
106 $eligible = [];
107
108 foreach ( $post_types as $post_type ) {
109 if ( in_array( $post_type->name, $excluded, true ) ) {
110 continue;
111 }
112 $eligible[ $post_type->name ] = $label_overrides[ $post_type->name ] ?? $post_type->label;
113 }
114
115 return apply_filters( 'helpdocs_migrate_eligible_post_types', $eligible );
116 } // End eligible_post_types()
117
118
119 /**
120 * Render migration notices for any known source plugin that's active and on a matching screen
121 *
122 * @return void
123 */
124 public function render_migration_notices() {
125 $current_page = isset( $_GET[ 'page' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'page' ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
126 $screen = get_current_screen();
127 $screen_id = $screen ? $screen->id : '';
128
129 foreach ( self::known_sources() as $key => $source ) {
130 if ( ! post_type_exists( $source[ 'post_type' ] ) ) {
131 continue;
132 }
133
134 $matches_page = in_array( $current_page, $source[ 'pages' ] ?? [], true );
135 $matches_screen = in_array( $screen_id, $source[ 'screens' ] ?? [], true );
136
137 if ( ! $matches_page && ! $matches_screen ) {
138 continue;
139 }
140
141 if ( get_user_meta( get_current_user_id(), 'helpdocs_dismissed_migrate_notice_' . $key, true ) ) {
142 continue;
143 }
144
145 $migrate_url = add_query_arg( [ 'source_post_type' => $source[ 'post_type' ] ], Bootstrap::tab_url( 'migrate' ) );
146
147 $message = $source[ 'notice_message' ] ?? sprintf(
148 /* translators: %s: plugin name */
149 __( '%s docs can be migrated to Admin Help Docs in a couple of clicks, no need to recreate them.', 'admin-help-docs' ),
150 $source[ 'plugin_label' ]
151 );
152
153 echo '<div class="notice notice-info is-dismissible helpdocs-migrate-notice" data-source-key="' . esc_attr( $key ) . '">
154 <p>' . esc_html( $message ) . '</p>
155 <p><a href="' . esc_url( $migrate_url ) . '" class="button button-primary">' . esc_html__( 'Migrate to Admin Help Docs', 'admin-help-docs' ) . '</a></p>
156 </div>';
157 }
158 } // End render_migration_notices()
159
160
161 /**
162 * Enqueue the notice-dismiss script on screens where a migration notice may render
163 *
164 * @return void
165 */
166 public function enqueue_notice_script() {
167 if ( ! self::current_screen_has_notice() ) {
168 return;
169 }
170
171 $text_domain = Bootstrap::textdomain();
172 $version = Bootstrap::script_version();
173
174 wp_enqueue_style( $text_domain . '-migrate', Bootstrap::url( 'inc/tabs/css/migrate.css' ), [ $text_domain . '-docs' ], $version );
175 wp_enqueue_script( $text_domain . '-migrate-notice', Bootstrap::url( 'inc/tabs/js/migrate-notice.js' ), [ 'jquery' ], $version, true );
176 wp_localize_script( $text_domain . '-migrate-notice', 'helpdocs_migrate_notice', [
177 'nonce' => wp_create_nonce( 'helpdocs_migrate_nonce' ),
178 ] );
179 } // End enqueue_notice_script()
180
181
182 /**
183 * Whether the current screen matches any known source's notice targets
184 *
185 * @return bool
186 */
187 public static function current_screen_has_notice() : bool {
188 $current_page = isset( $_GET[ 'page' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'page' ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
189 $screen = get_current_screen();
190 $screen_id = $screen ? $screen->id : '';
191
192 foreach ( self::known_sources() as $source ) {
193 if ( ! post_type_exists( $source[ 'post_type' ] ) ) {
194 continue;
195 }
196
197 if ( in_array( $current_page, $source[ 'pages' ] ?? [], true ) || in_array( $screen_id, $source[ 'screens' ] ?? [], true ) ) {
198 return true;
199 }
200 }
201
202 return false;
203 } // End current_screen_has_notice()
204
205
206 /**
207 * AJAX: dismiss a migration notice for the current user
208 *
209 * @return void
210 */
211 public function ajax_dismiss_notice() {
212 check_ajax_referer( 'helpdocs_migrate_nonce', 'nonce' );
213
214 $key = isset( $_POST[ 'source_key' ] ) ? sanitize_key( wp_unslash( $_POST[ 'source_key' ] ) ) : '';
215
216 if ( $key ) {
217 update_user_meta( get_current_user_id(), 'helpdocs_dismissed_migrate_notice_' . $key, 1 );
218 }
219
220 wp_send_json_success();
221 } // End ajax_dismiss_notice()
222
223
224 /**
225 * Render the tab
226 */
227 public function render_tab() {
228 $text_domain = Bootstrap::textdomain();
229 $version = Bootstrap::script_version();
230
231 wp_enqueue_style( $text_domain . '-migrate', Bootstrap::url( 'inc/tabs/css/migrate.css' ), [ $text_domain . '-docs' ], $version );
232 wp_enqueue_script( $text_domain . '-migrate', Bootstrap::url( 'inc/tabs/js/migrate.js' ), [ 'jquery' ], $version, true );
233 wp_localize_script( $text_domain . '-migrate', 'helpdocs_migrate', [
234 'ajax_url' => admin_url( 'admin-ajax.php' ),
235 'nonce' => wp_create_nonce( 'helpdocs_migrate_nonce' ),
236 'docs_url' => Bootstrap::tab_url( 'documentation' ),
237 'i18n' => [
238 'no_posts' => __( 'No posts found for this post type.', 'admin-help-docs' ),
239 'already_migrated' => __( 'Migrated', 'admin-help-docs' ),
240 'migrating' => __( 'Migrating…', 'admin-help-docs' ),
241 'loading' => __( 'Loading posts…', 'admin-help-docs' ),
242 'error' => __( 'Something went wrong loading these posts. Please try again.', 'admin-help-docs' ),
243 'done' => __( 'Migration completed successfully. Migrated %migrated%, skipped %skipped%.', 'admin-help-docs' ),
244 'go_to_docs' => __( 'Go to Main Documentation Page!', 'admin-help-docs' ),
245 'select_all' => __( 'Select All', 'admin-help-docs' ),
246 'deselect_all' => __( 'Deselect All', 'admin-help-docs' ),
247 ],
248 ] );
249
250 $preselect = isset( $_GET[ 'source_post_type' ] ) ? sanitize_key( wp_unslash( $_GET[ 'source_post_type' ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
251 $post_types = self::eligible_post_types();
252 ?>
253 <div class="helpdocs-full-width-box" id="helpdocs-migrate-app" data-preselect="<?php echo esc_attr( $preselect ); ?>">
254 <p><?php esc_html_e( 'Copy documents from another post type into Admin Help Docs. Originals are left untouched. Migrated docs are published to the Main Documentation Page and filed into a folder named after the source post type.', 'admin-help-docs' ); ?></p>
255
256 <div class="helpdocs-migrate-source-row">
257 <label for="helpdocs-migrate-source"><?php esc_html_e( 'Source Post Type', 'admin-help-docs' ); ?></label>
258 <select id="helpdocs-migrate-source" <?php disabled( empty( $preselect ), false ); ?>>
259 <option value=""><?php esc_html_e( '— Select —', 'admin-help-docs' ); ?></option>
260 <?php foreach ( $post_types as $slug => $label ) : ?>
261 <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( $preselect, $slug ); ?>><?php echo esc_html( $label ); ?></option>
262 <?php endforeach; ?>
263 </select>
264 </div>
265
266 <div id="helpdocs-migrate-list">
267 <?php if ( $preselect ) : ?>
268 <div class="spinner-row"><span class="spinner is-active" style="float:none;"></span> <?php esc_html_e( 'Loading posts…', 'admin-help-docs' ); ?></div>
269 <?php endif; ?>
270 </div>
271
272 <div id="helpdocs-migrate-actions-row" style="display:none;">
273 <button type="button" class="helpdocs-button button-secondary" id="helpdocs-migrate-select-all"><?php esc_html_e( 'Select All', 'admin-help-docs' ); ?></button>
274 <button type="button" class="helpdocs-button" id="helpdocs-migrate-submit" disabled><?php esc_html_e( 'Migrate Selected', 'admin-help-docs' ); ?></button>
275 <span id="helpdocs-migrate-status"></span>
276 </div>
277 </div>
278 <?php
279 } // End render_tab()
280
281
282 /**
283 * AJAX: list posts of the chosen source post type
284 *
285 * @return void
286 */
287 public function ajax_list_posts() {
288 check_ajax_referer( 'helpdocs_migrate_nonce', 'nonce' );
289
290 if ( ! current_user_can( 'edit_posts' ) ) {
291 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'admin-help-docs' ) ] );
292 }
293
294 $source_post_type = isset( $_POST[ 'source_post_type' ] ) ? sanitize_key( wp_unslash( $_POST[ 'source_post_type' ] ) ) : '';
295
296 if ( ! $source_post_type || ! array_key_exists( $source_post_type, self::eligible_post_types() ) ) {
297 wp_send_json_error( [ 'message' => __( 'Invalid post type.', 'admin-help-docs' ) ] );
298 }
299
300 $source_posts = get_posts( [
301 'post_type' => $source_post_type,
302 'post_status' => [ 'publish', 'draft' ],
303 'posts_per_page' => -1,
304 'orderby' => 'title',
305 'order' => 'ASC',
306 ] );
307
308 $already_migrated = self::get_imported_source_ids( $source_post_type );
309
310 $rows = [];
311 foreach ( $source_posts as $source_post ) {
312 $rows[] = [
313 'id' => $source_post->ID,
314 'title' => get_the_title( $source_post ),
315 'imported' => in_array( $source_post->ID, $already_migrated, true ),
316 ];
317 }
318
319 wp_send_json_success( [ 'posts' => $rows ] );
320 } // End ajax_list_posts()
321
322
323 /**
324 * AJAX: import the selected posts into Admin Help Docs
325 *
326 * @return void
327 */
328 public function ajax_import_posts() {
329 check_ajax_referer( 'helpdocs_migrate_nonce', 'nonce' );
330
331 if ( ! current_user_can( 'edit_posts' ) ) {
332 wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'admin-help-docs' ) ] );
333 }
334
335 $source_post_type = isset( $_POST[ 'source_post_type' ] ) ? sanitize_key( wp_unslash( $_POST[ 'source_post_type' ] ) ) : '';
336 $source_ids = isset( $_POST[ 'post_ids' ] ) ? array_map( 'absint', (array) wp_unslash( $_POST[ 'post_ids' ] ) ) : [];
337 $eligible = self::eligible_post_types();
338
339 if ( ! $source_post_type || ! array_key_exists( $source_post_type, $eligible ) || empty( $source_ids ) ) {
340 wp_send_json_error( [ 'message' => __( 'Nothing to import.', 'admin-help-docs' ) ] );
341 }
342
343 $folder_term_id = self::get_or_create_folder_term( $eligible[ $source_post_type ] );
344
345 global $wpdb;
346
347 $imported = 0;
348 $skipped = 0;
349 $last_id = 0;
350
351 foreach ( $source_ids as $source_id ) {
352 $source_post = get_post( $source_id );
353
354 if ( ! $source_post || $source_post_type !== $source_post->post_type ) {
355 $skipped++;
356 continue;
357 }
358
359 if ( self::already_imported( $source_post_type, $source_id ) ) {
360 $skipped++;
361 continue;
362 }
363
364 $new_id = wp_insert_post( [
365 'post_type' => HelpDocs::$post_type,
366 'post_title' => $source_post->post_title,
367 'post_content' => wp_kses_post( $source_post->post_content ),
368 'post_excerpt' => $source_post->post_excerpt,
369 'post_status' => 'publish',
370 ], true );
371
372 if ( is_wp_error( $new_id ) ) {
373 $skipped++;
374 continue;
375 }
376
377 $wpdb->update(
378 $wpdb->posts,
379 [
380 'post_date' => $source_post->post_date,
381 'post_date_gmt' => $source_post->post_date_gmt,
382 'post_modified' => $source_post->post_modified,
383 'post_modified_gmt' => $source_post->post_modified_gmt,
384 ],
385 [ 'ID' => $new_id ]
386 );
387 clean_post_cache( $new_id );
388
389 update_post_meta( $new_id, 'helpdocs_locations', [
390 [
391 'site_location' => base64_encode( 'main' ),
392 'page_location' => '',
393 'custom' => '',
394 'post_types' => [],
395 'order' => '',
396 'css_selector' => '',
397 ],
398 ] );
399
400 if ( $folder_term_id ) {
401 wp_set_object_terms( $new_id, [ $folder_term_id ], Folders::$taxonomy, false );
402 }
403
404 update_post_meta( $new_id, 'helpdocs_migrated_from', $source_post_type );
405 update_post_meta( $new_id, 'helpdocs_migrated_at', current_time( 'mysql' ) );
406 update_post_meta( $new_id, 'helpdocs_migrated_from_id', $source_id );
407
408 do_action( 'helpdocs_migrate_post_imported', $new_id, $source_post );
409
410 $last_id = $new_id;
411 $imported++;
412 }
413
414 wp_send_json_success( [
415 'imported' => $imported,
416 'skipped' => $skipped,
417 'last_id' => $last_id,
418 ] );
419 } // End ajax_import_posts()
420
421
422 /**
423 * Check whether a given source post has already been migrated
424 *
425 * @param string $source_post_type
426 * @param int $source_id
427 * @return bool
428 */
429 public static function already_imported( $source_post_type, $source_id ) : bool {
430 return in_array( absint( $source_id ), self::get_imported_source_ids( $source_post_type ), true );
431 } // End already_imported()
432
433
434 /**
435 * Get the source IDs already migrated for a given source post type
436 *
437 * @param string $source_post_type
438 * @return array
439 */
440 public static function get_imported_source_ids( $source_post_type ) : array {
441 global $wpdb;
442
443 $ids = $wpdb->get_col( $wpdb->prepare(
444 "SELECT pm2.meta_value FROM {$wpdb->postmeta} pm1
445 INNER JOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id
446 WHERE pm1.meta_key = %s AND pm1.meta_value = %s AND pm2.meta_key = %s",
447 'helpdocs_migrated_from',
448 $source_post_type,
449 'helpdocs_migrated_from_id'
450 ) );
451
452 return array_map( 'absint', $ids );
453 } // End get_imported_source_ids()
454
455
456 /**
457 * Get the folder term for a source post type's label, creating it if it doesn't exist
458 *
459 * @param string $folder_name
460 * @return int|null Term ID, or null on failure
461 */
462 public static function get_or_create_folder_term( $folder_name ) {
463 $existing = get_term_by( 'name', $folder_name, Folders::$taxonomy );
464
465 if ( $existing ) {
466 return $existing->term_id;
467 }
468
469 $created = wp_insert_term( $folder_name, Folders::$taxonomy );
470
471 if ( is_wp_error( $created ) ) {
472 return null;
473 }
474
475 return $created[ 'term_id' ];
476 } // End get_or_create_folder_term()
477
478 }
479
480 Migrate::instance();