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(); |