PluginProbe ʕ •ᴥ•ʔ
Admin Help Docs / 2.0.2
Admin Help Docs v2.0.2
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 / import.php
admin-help-docs / inc / tabs Last commit date
css 1 week ago js 1 week ago admin-menu.php 1 week ago documentation.php 1 week ago faq.php 1 week ago import.php 1 week ago settings.php 1 week ago support.php 1 week ago
import.php
924 lines
1 <?php
2 /**
3 * Import Editor Loader
4 */
5
6 namespace PluginRx\AdminHelpDocs;
7
8 if ( ! defined( 'ABSPATH' ) ) exit;
9
10 class ImportEditor {
11
12 /**
13 * @var array Per-request memoization of remote lookups
14 */
15 private static array $request_cache = [];
16
17
18 /**
19 * The single instance of the class
20 *
21 * @var self|null
22 */
23 private static ?ImportEditor $instance = null;
24
25
26 /**
27 * Get the singleton instance
28 *
29 * @return self
30 */
31 public static function instance() : self {
32 return self::$instance ??= new self();
33 } // End instance()
34
35
36 /**
37 * Constructor
38 */
39 private function __construct() {
40 add_action( 'helpdocs_subheader_left', [ $this, 'header_buttons' ] );
41 add_action( 'helpdocs_subheader_right', [ $this, 'debug_quick_link' ] );
42 add_action( 'admin_head', [ $this, 'pre_cache_locations' ] );
43 add_action( 'admin_init', [ $this, 'save_import_settings' ] );
44 add_action( 'admin_notices', [ $this, 'render_failed_import_notice' ] );
45 add_action( 'wp_ajax_helpdocs_fetch_remote_docs', [ $this, 'ajax_fetch_remote_docs' ] );
46 add_action( 'wp_ajax_helpdocs_import_individual_doc', [ $this, 'ajax_import_individual_doc' ] );
47 } // End __construct()
48
49
50 /**
51 * Add header buttons
52 *
53 * @param string $current_tab The current tab slug
54 */
55 public function header_buttons( $current_tab ) {
56 if ( $current_tab === 'import' ) {
57 $import_id = isset( $_GET[ 'id' ] ) ? absint( $_GET[ 'id' ] ) : 0; // phpcs:ignore
58 $status = get_post_status( $import_id );
59
60 $is_active = ( 'publish' === $status );
61 $legacy_enabled = get_post_meta( $import_id, 'helpdocs_enabled', true );
62 if ( '0' === $legacy_enabled ) {
63 $is_active = false;
64 }
65
66 $show_success = isset( $_GET[ 'settings-updated' ] ) && 'true' === sanitize_text_field( wp_unslash( $_GET[ 'settings-updated' ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
67 ?>
68 <div class="helpdocs-header-controls">
69 <div class="helpdocs-active-toggle">
70 <span class="helpdocs-toggle-label"><?php esc_html_e( 'Active', 'admin-help-docs' ); ?></span>
71 <label class="helpdocs-switch">
72 <input type="checkbox" name="helpdocs_import_active" id="helpdocs_import_active" value="1" <?php checked( $is_active ); ?> form="helpdocs_import_form">
73 <span class="helpdocs-slider"></span>
74 </label>
75 </div>
76 <span id="helpdocs-inactive-notice" style="display: <?php echo ( $import_id && ! $is_active ) ? 'inline' : 'none'; ?>;"> <?php echo esc_html__( 'Don\'t forget to activate the import.', 'admin-help-docs' ); ?></span>
77 <span id="helpdocs-save-reminder"><?php echo esc_html__( 'Remember to click "Save Import Settings" after making changes to your import.', 'admin-help-docs' ); ?></span>
78 <span id="helpdocs-saved-success" style="display: <?php echo $show_success ? 'inline' : 'none'; ?>;"><?php echo esc_html__( 'Settings saved successfully.', 'admin-help-docs' ); ?></span>
79 </div>
80 <?php
81 }
82 } // End header_buttons()
83
84
85 /**
86 * Add debug quick link for Dev Debug Tools plugin if active
87 *
88 * @param string $current_tab The current tab slug
89 */
90 public function debug_quick_link( $current_tab ) {
91 if ( $current_tab === 'import' ) {
92 if ( is_plugin_active( 'dev-debug-tools/dev-debug-tools.php' ) ) {
93 $import_id = isset( $_GET[ 'id' ] ) ? absint( $_GET[ 'id' ] ) : 0; // phpcs:ignore
94 if ( ! $import_id ) {
95 return;
96 }
97
98 $icon = apply_filters( 'ddtt_quick_link_icon', '&#9889;' );
99 $nonce = wp_create_nonce( 'ddtt_metadata_lookup' );
100 ?>
101 <div class="ddtt-debug-quick-link">
102 <span class="ddtt-icon"><?php echo esc_html( $icon ); ?></span><a href="/wp-admin/admin.php?page=dev-debug-tools&tool=metadata&s=post&lookup=<?php echo esc_attr( $import_id ); ?>&_wpnonce=<?php echo esc_attr( $nonce ); ?>" target="_blank">
103 <?php esc_html_e( 'Debug Import', 'admin-help-docs' ); ?>
104 </a>
105 </div>
106 <?php
107 }
108 }
109 } // End debug_quick_link()
110
111
112 /**
113 * Pre-cache site locations on the import page to speed up the UI
114 */
115 public function pre_cache_locations() {
116 if ( Menu::get_current_page() !== Bootstrap::textdomain() || Menu::get_current_tab() !== 'import' ) {
117 return;
118 }
119
120 HelpDocs::site_locations();
121 } // End pre_cache_locations()
122
123
124 /**
125 * Show an admin notice when a published import cannot reach its remote site
126 *
127 * @return void
128 */
129 public function render_failed_import_notice() {
130 if ( ! Helpers::user_can_edit() ) {
131 return;
132 }
133
134 $import_posts = get_posts( [
135 'posts_per_page' => -1,
136 'post_status' => 'publish',
137 'post_type' => Imports::$post_type,
138 'no_found_rows' => true,
139 'meta_query' => [
140 [
141 'key' => 'helpdocs_url',
142 'value' => '',
143 'compare' => '!=',
144 ],
145 ],
146 ] );
147
148 if ( empty( $import_posts ) ) {
149 return;
150 }
151
152 $current_import_id = ( Menu::is_our_tab( 'import' ) && isset( $_GET[ 'id' ] ) ) ? absint( $_GET[ 'id' ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
153
154 $failed = [];
155
156 foreach ( $import_posts as $import ) {
157 $url = get_post_meta( $import->ID, 'helpdocs_url', true );
158 $api_key = get_post_meta( $import->ID, 'helpdocs_api_key', true );
159 $data = self::get_all_import_data( $url, $api_key );
160
161 if ( ! empty( $data[ 'error' ] ) ) {
162 $failed[] = [
163 'title' => $import->post_title,
164 'error' => $data[ 'error' ],
165 'is_current' => ( $import->ID === $current_import_id ),
166 'url' => add_query_arg( [ 'page' => Bootstrap::textdomain(), 'tab' => 'import', 'id' => $import->ID ], Bootstrap::admin_url( 'admin.php' ) ),
167 ];
168 }
169 }
170
171 if ( empty( $failed ) ) {
172 return;
173 }
174
175 echo '<div class="notice notice-error" style="background:#fff;box-shadow:0 1px 3px rgba(0,0,0,0.1);"><p><strong>' . esc_html__( 'Admin Help Docs:', 'admin-help-docs' ) . '</strong> ';
176
177 foreach ( $failed as $item ) {
178 $reason = ( 'unauthorized' === $item[ 'error' ] )
179 ? __( 'invalid API key', 'admin-help-docs' )
180 : __( 'connection failed', 'admin-help-docs' );
181
182 if ( $item[ 'is_current' ] ) {
183 printf(
184 /* translators: 1: import title, 2: failure reason */
185 esc_html__( 'The import "%1$s" could not connect to its remote site (%2$s).', 'admin-help-docs' ),
186 esc_html( $item[ 'title' ] ),
187 esc_html( $reason )
188 );
189 } else {
190 printf(
191 /* translators: 1: import title, 2: failure reason, 3: edit URL */
192 esc_html__( 'The import "%1$s" could not connect to its remote site (%2$s). %3$s', 'admin-help-docs' ),
193 esc_html( $item[ 'title' ] ),
194 esc_html( $reason ),
195 '<a href="' . esc_url( $item[ 'url' ] ) . '">' . esc_html__( 'Review import', 'admin-help-docs' ) . '</a>'
196 );
197 }
198 echo ' ';
199 }
200
201 echo '</p></div>';
202 } // End render_failed_import_notice()
203
204
205 /**
206 * Render the tab
207 */
208 public function render_tab() {
209 $import_id = isset( $_GET[ 'id' ] ) ? absint( $_GET[ 'id' ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
210 $post_title = $import_id ? get_the_title( $import_id ) : '';
211 $website_url = sanitize_url( get_post_meta( $import_id, 'helpdocs_url', true ) );
212
213 $selected_docs = get_post_meta( $import_id, 'helpdocs_docs', true ) ?: [];
214 $selected_tocs = get_post_meta( $import_id, 'helpdocs_tocs', true ) ?: [];
215
216 $all_docs = get_post_meta( $import_id, 'helpdocs_all', true );
217 $all_tocs = get_post_meta( $import_id, 'helpdocs_all_tocs', true );
218
219 $api_key = get_post_meta( $import_id, 'helpdocs_api_key', true );
220
221 $table_row_header = '
222 <tr>
223 <th scope="col" class="manage-column column-feed">' . esc_html__( 'Auto Feed', 'admin-help-docs' ) . '</th>
224 <th scope="col" class="manage-column column-title">' . esc_html__( 'Title', 'admin-help-docs' ) . '</th>
225 <th scope="col" class="manage-column column-date">' . esc_html__( 'Publish Date', 'admin-help-docs' ) . '</th>
226 <th scope="col" class="manage-column column-author">' . esc_html__( 'Created By', 'admin-help-docs' ) . '</th>
227 <th scope="col" class="manage-column column-location">' . esc_html__( 'Site Location', 'admin-help-docs' ) . '</th>
228 <th scope="col" class="manage-column column-toc">' . esc_html__( 'TOC', 'admin-help-docs' ) . '</th>
229 <th scope="col" class="manage-column column-action">' . esc_html__( 'Action', 'admin-help-docs' ) . '</th>
230 </tr>';
231 ?>
232 <div class="helpdocs-full-width-box">
233
234 <p class="helpdocs-instructions"><em><?php echo esc_html__( "You can choose to remotely feed documents from the other website, which will update automatically if they are changed on the other site. This is useful if you manage several sites and want to control them in one spot. You may also import them individually, which will clone them and add them to this website and no longer be linked to the remote site.", 'admin-help-docs' ); ?></em></p>
235
236 <p class="helpdocs-instructions"><em><?php echo esc_html__( "The \"TOC\" option allows you to add the doc to the Dashboard Table of Contents, provided that you have enabled Dashboard TOC in your settings and the feed's site location is set to \"Main Documentation Page.\"", 'admin-help-docs' ); ?></em></p>
237
238 <form id="helpdocs_import_form" method="post">
239 <input type="hidden" name="action" value="helpdocs_import_docs">
240 <input type="hidden" name="id" id="helpdocs_import_post_id" value="<?php echo esc_attr( $import_id ); ?>">
241 <?php wp_nonce_field( 'helpdocs_save_import_nonce', 'helpdocs_save_import_nonce' ); ?>
242
243 <div id="helpdocs_title_container">
244 <label for="helpdocs_import_title"><?php esc_html_e( 'Import Name', 'admin-help-docs' ); ?>:</label>
245 <input name="helpdocs_import_title" id="helpdocs_import_title" type="text" value="<?php echo esc_attr( $post_title ); ?>" placeholder="<?php esc_attr_e( 'e.g. My Remote Docs', 'admin-help-docs' ); ?>">
246 </div>
247
248 <div id="helpdocs_website_url_container">
249 <label for="helpdocs_website_url"><?php esc_html_e( 'Enter the URL of the website you would like to import help docs from', 'admin-help-docs' ); ?>:</label>
250 <div id="helpdocs_url_field">
251 <input name="helpdocs_website_url" id="helpdocs_website_url" type="url" value="<?php echo esc_url( $website_url ); ?>" placeholder="https://example.com">
252
253 <div class="helpdocs-api-key-container">
254 <input name="helpdocs_api_key" id="helpdocs_api_key" type="password" value="<?php echo esc_attr( $api_key ); ?>" placeholder="<?php esc_attr_e( 'API Key (Optional)', 'admin-help-docs' ); ?>" style="width: 200px; padding-right: 30px;">
255 <span class="dashicons dashicons-visibility helpdocs-toggle-visibility"></span>
256 </div>
257
258 <a id="helpdocs_fetch_remote_docs" class="helpdocs-button" href="#"><?php esc_html_e( 'Fetch Docs', 'admin-help-docs' ); ?></a>
259 <?php if ( Bootstrap::is_test_mode() ) : ?>
260 <a href="<?php echo esc_url( $website_url ); ?>/wp-json/admin-help-docs/v1/docs" class="helpdocs-button button-secondary" target="_blank"><?php esc_html_e( 'v1', 'admin-help-docs' ); ?></a>
261 <a href="<?php echo esc_url( $website_url ); ?>/wp-json/admin-help-docs/v2/docs" class="helpdocs-button button-secondary" target="_blank"><?php esc_html_e( 'v2', 'admin-help-docs' ); ?></a>
262 <?php endif; ?>
263 </div>
264 </div>
265
266 <?php
267 $import_data = $website_url ? self::get_all_import_data( $website_url, $api_key ) : [];
268 $docs = $import_data[ 'docs' ] ?? [];
269 $error = $import_data[ 'error' ] ?? '';
270 $api_version = $import_data[ 'version' ] ?? 'v2';
271 $total_items = count( $docs );
272 $version_class = $api_version === 'v1' && ! empty( $docs ) ? 'display:block;' : 'display:none;';
273 ?>
274
275 <div id="helpdocs_version_notice" class="helpdocs_warning_notice" style="<?php echo esc_attr( $version_class ); ?>"><p>
276 <?php echo esc_html__( 'Note: This remote site is using an older version of the plugin. Some newer features, like multiple locations per document, may be limited until the remote site is updated.', 'admin-help-docs' ); ?>
277 </p></div>
278
279 <div id="helpdocs_api_error" class="helpdocs_error_notice" style="<?php echo ( $error === 'unauthorized' ) ? '' : 'display:none;'; ?>">
280 <p><strong><?php esc_html_e( 'Connection Unauthorized:', 'admin-help-docs' ); ?></strong> <?php esc_html_e( 'The remote site requires a valid API Key. Please check your key and try again.', 'admin-help-docs' ); ?></p>
281 </div>
282
283 <div id="helpdocs_connection_error" class="helpdocs_error_notice" style="<?php echo ( $error === 'connection_failed' ) ? '' : 'display:none;'; ?>">
284 <p><?php esc_html_e( 'Could not connect to the remote site. Please check the URL.', 'admin-help-docs' ); ?></p>
285 </div>
286
287 <div id="helpdocs_remote_docs_wrapper" style="<?php echo empty( $website_url ) ? 'display:none;' : ''; ?>">
288
289 <div id="helpdocs_tablenav_top" class="tablenav top">
290 <div class="alignleft actions">
291 <button type="button" class="button helpdocs-select-all-toggle" data-type="feed"><?php esc_html_e( 'Select All Auto Feeds', 'admin-help-docs' ); ?></button>
292 <button type="button" class="button helpdocs-select-all-toggle" data-type="toc"><?php esc_html_e( 'Select All TOCs', 'admin-help-docs' ); ?></button>
293 <label class="helpdocs-all-checkbox">
294 <input type="checkbox" name="helpdocs_all" id="helpdocs_all" value="1" <?php checked( $all_docs, '1' ); ?>>
295 <strong><?php esc_html_e( 'Feed All Documents Automatically', 'admin-help-docs' ); ?></strong>
296 </label>
297 <label class="helpdocs-all-checkbox" id="helpdocs_all_tocs_container" style="<?php echo ( '1' === $all_docs ) ? 'display:inline-flex;' : 'display:none;'; ?>">
298 <input type="checkbox" name="helpdocs_all_tocs" id="helpdocs_all_tocs" value="1" <?php checked( $all_tocs, '1' ); ?>>
299 <?php esc_html_e( 'Add All Main Docs to Dashboard Table of Contents (Must be Enabled in Settings)', 'admin-help-docs' ); ?>
300 </label>
301 </div>
302 <div class="tablenav-pages">
303 <span class="displaying-num">
304 <?php if ( $total_items ) echo esc_html( sprintf( _n( '%s item', '%s items', $total_items, 'admin-help-docs' ), number_format_i18n( $total_items ) ) ); ?>
305 </span>
306 </div>
307 <br class="clear">
308 </div>
309
310 <table id="helpdocs_imports_table" class="wp-list-table widefat fixed striped posts" data-import-id="<?php echo esc_attr( $import_id ); ?>">
311 <thead><?php echo wp_kses_post( $table_row_header ); ?></thead>
312 <tbody id="the-list">
313 <?php
314 if ( ! empty( $docs ) ) {
315 $this->render_table_rows( $docs, $selected_docs, $selected_tocs );
316 }
317 ?>
318 </tbody>
319 <tfoot><?php echo wp_kses_post( $table_row_header ); ?></tfoot>
320 </table>
321
322 <div id="helpdocs_tablenav_bottom" class="tablenav bottom">
323 <div class="tablenav-pages">
324 <span class="displaying-num">
325 <?php if ( $total_items ) echo esc_html( sprintf( _n( '%s item', '%s items', $total_items, 'admin-help-docs' ), number_format_i18n( $total_items ) ) ); ?>
326 </span>
327 </div>
328 <br class="clear">
329 </div>
330 </div>
331
332 <div id="helpdocs_no_docs_found" style="<?php echo ( $website_url && empty( $docs ) ) ? '' : 'display:none;'; ?>">
333 <p><em><?php echo esc_html__( 'No documents found at the provided URL.', 'admin-help-docs' ); ?></em></p>
334 </div>
335 </form>
336 </div>
337 <?php
338 } // End render_tab()
339
340
341 /**
342 * Render table rows for the import list
343 *
344 * @param array $docs Array of document objects.
345 * @param array $selected_docs IDs of already selected docs.
346 * @param array $selected_tocs IDs of already selected TOCs.
347 */
348 private function render_table_rows( $docs, $selected_docs = [], $selected_tocs = [] ) {
349 if ( empty( $docs ) ) {
350 return;
351 }
352
353 foreach ( $docs as $doc ) {
354 $doc_id = absint( $doc->ID );
355 $is_imported = in_array( $doc_id, $selected_docs );
356 $has_toc = in_array( $doc_id, $selected_tocs );
357
358 // Handle multi-location logic for remote docs
359 $locations = isset( $doc->locations ) ? (array) $doc->locations : [];
360 if ( empty( $locations ) && ! empty( $doc->site_location ) ) {
361 $locations = [
362 [
363 'site_location' => $doc->site_location,
364 'page_location' => $doc->page_location ?? '',
365 'custom' => $doc->custom ?? '',
366 'post_types' => $doc->post_types ?? [],
367 'order' => $doc->order ?? '',
368 'css_selector' => $doc->css_selector ?? '',
369 ]
370 ];
371 }
372
373 $can_have_toc = false;
374 foreach ( $locations as $loc ) {
375 $loc_array = (array) $loc;
376 if ( isset( $loc_array[ 'site_location' ] ) && 'main' === base64_decode( $loc_array[ 'site_location' ] ) ) {
377 $can_have_toc = true;
378 break;
379 }
380 }
381 ?>
382 <tr data-doc-id="<?php echo esc_attr( $doc_id ); ?>">
383 <td class="column-feed check-column">
384 <input type="checkbox" name="helpdocs_auto_feed[]" value="<?php echo esc_attr( $doc_id ); ?>" class="feed-checkbox" <?php checked( $is_imported ); ?>>
385 </td>
386 <td class="column-title title">
387 <strong><?php echo esc_html( $doc->title ); ?></strong>
388 <button type="button" class="toggle-row"><span class="screen-reader-text"><?php esc_html_e( 'Show more details', 'admin-help-docs' ); ?></span></button>
389 </td>
390 <td class="column-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $doc->publish_date ) ) ); ?></td>
391 <td class="column-author"><?php echo esc_html( $doc->created_by ); ?></td>
392
393 <td class="column-location">
394 <?php if ( empty( $locations ) ) : ?>
395 <span class="description"><?php esc_html_e( 'No locations set.', 'admin-help-docs' ); ?></span>
396 <?php else : ?>
397 <ul style="margin:0; padding:0; list-style:none;">
398 <?php foreach ( $locations as $loc ) {
399 $loc_array = (array) $loc;
400 $site_val = $loc_array[ 'site_location' ] ?? '';
401 if ( empty( $site_val ) ) continue;
402
403 $label = Helpers::get_admin_page_title_from_url( $site_val );
404 $details = [];
405
406 // 1. Page Location
407 if ( ! empty( $loc_array[ 'page_location' ] ) ) {
408 $page_loc = ucwords( str_replace( '_', ' ', $loc_array[ 'page_location' ] ) );
409 if ( $page_loc === 'Contextual' ) $page_loc .= ' Help Tab';
410 $details[] = '<em>' . esc_html( $page_loc ) . '</em>';
411 }
412
413 // 2. Custom URL
414 if ( ! empty( $loc_array[ 'custom' ] ) ) {
415 $details[] = '<code style="font-size:10px;">' . esc_html( $loc_array[ 'custom' ] ) . '</code>';
416 }
417
418 // 3. Post Types (Handled serialized remote data)
419 if ( ! empty( $loc_array[ 'post_types' ] ) ) {
420 $pt_raw = $loc_array[ 'post_types' ];
421 $pts = is_serialized( $pt_raw ) ? unserialize( $pt_raw ) : $pt_raw;
422 $pts = is_array( $pts ) ? $pts : [ $pts ];
423
424 if ( ! empty( $pts ) ) {
425 $clean_pts = array_map( function( $pt ) {
426 return ucwords( str_replace( [ '-', '_' ], ' ', $pt ) );
427 }, $pts );
428 $details[] = 'Types: ' . esc_html( implode( ', ', $clean_pts ) );
429 }
430 }
431
432 echo '<li style="margin-bottom: 8px; border-bottom: 1px solid #eee; padding-bottom: 4px;">';
433 echo '<strong>' . wp_kses_post( $label ) . '</strong>';
434 if ( ! empty( $details ) ) {
435 echo '<br><span class="description" style="font-size:11px;">' . wp_kses_post( implode( ' | ', $details ) ) . '</span>';
436 }
437 echo '</li>';
438 } ?>
439 </ul>
440 <?php endif; ?>
441 </td>
442
443 <td class="column-toc check-column">
444 <?php if ( $can_have_toc ) : ?>
445 <input type="checkbox" name="helpdocs_tocs[]" value="<?php echo esc_attr( $doc_id ); ?>" class="toc-checkbox" <?php checked( $has_toc ); ?>>
446 <?php else : ?>
447 <span class="dash">&mdash;</span>
448 <?php endif; ?>
449 </td>
450 <td class="column-action">
451 <button type="button" class="button button-secondary helpdocs-clone-individual" data-id="<?php echo esc_attr( $doc_id ); ?>">
452 <?php esc_html_e( 'Copy to Local', 'admin-help-docs' ); ?>
453 </button>
454 </td>
455 </tr>
456 <?php
457 }
458 } // End render_table_rows()
459
460
461 /**
462 * Get all import data for a given website
463 *
464 * @param string $website_url The remote site URL
465 * @param string $api_key Optional API key for the remote site
466 * @return array
467 */
468 public static function get_all_import_data( $website_url, $api_key = '' ) {
469 $website_url = esc_url_raw( trim( (string) $website_url ) );
470 if ( empty( $website_url ) ) {
471 return [ 'docs' => [] ];
472 }
473
474 if ( empty( $api_key ) ) {
475 $import_id = isset( $_GET[ 'id' ] ) ? absint( $_GET[ 'id' ] ) : 0; // phpcs:ignore
476 $api_key = isset( $_POST[ 'helpdocs_api_key' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'helpdocs_api_key' ] ) ) : (string) get_post_meta( $import_id, 'helpdocs_api_key', true ); // phpcs:ignore
477 }
478
479 $cache_key = 'helpdocs_remote_' . md5( $website_url . $api_key );
480
481 if ( isset( self::$request_cache[ $cache_key ] ) ) {
482 return self::$request_cache[ $cache_key ];
483 }
484
485 $cached = get_transient( $cache_key );
486 if ( false !== $cached ) {
487 self::$request_cache[ $cache_key ] = $cached;
488 return $cached;
489 }
490
491 $timeout = apply_filters( 'helpdocs_import_timeout', is_admin() && wp_doing_ajax() ? 20 : 5, $website_url );
492
493 $v2_url = str_replace( home_url(), $website_url, API::api_path() );
494
495 $version_found = 'v2';
496 $response = self::fetch_remote_json( $v2_url, $api_key, $timeout );
497 $status = wp_remote_retrieve_response_code( $response );
498
499 if ( ! is_wp_error( $response ) && 404 === $status ) {
500 $version_found = 'v1';
501 $response = self::fetch_remote_json( str_replace( '/v2/', '/v1/', $v2_url ), $api_key, $timeout );
502 $status = wp_remote_retrieve_response_code( $response );
503 }
504
505 if ( is_wp_error( $response ) ) {
506 return self::cache_result( $cache_key, [ 'error' => 'connection_failed' ], true, $website_url );
507 }
508
509 if ( 401 === $status ) {
510 return self::cache_result( $cache_key, [ 'error' => 'unauthorized' ], true, $website_url );
511 }
512
513 $docs = json_decode( wp_remote_retrieve_body( $response ) );
514
515 if ( ! empty( $docs ) && is_array( $docs ) ) {
516 $data = [
517 'version' => $version_found,
518 'docs' => $docs,
519 ];
520
521 return self::cache_result( $cache_key, $data, false, $website_url );
522 }
523
524 return self::cache_result( $cache_key, [ 'docs' => [] ], true, $website_url );
525 } // End get_all_import_data()
526
527
528 /**
529 * Store a lookup result in both the request cache and a transient
530 *
531 * @param string $cache_key
532 * @param array $data
533 * @param bool $is_failure Failures get a much shorter TTL so a fixed remote recovers quickly
534 * @param string $website_url
535 * @return array
536 */
537 private static function cache_result( $cache_key, $data, $is_failure, $website_url ) {
538 $success_ttl = apply_filters( 'helpdocs_import_cache_duration', 12 * HOUR_IN_SECONDS, $website_url );
539 $failure_ttl = apply_filters( 'helpdocs_import_failure_cache_duration', 15 * MINUTE_IN_SECONDS, $website_url );
540
541 set_transient( $cache_key, $data, $is_failure ? $failure_ttl : $success_ttl );
542 self::$request_cache[ $cache_key ] = $data;
543
544 return $data;
545 } // End cache_result()
546
547
548 /**
549 * Force a fresh lookup, bypassing both caches
550 *
551 * @param string $website_url
552 * @param string $api_key
553 * @return array
554 */
555 public static function refresh_import_data( $website_url, $api_key = '' ) {
556 $cache_key = 'helpdocs_remote_' . md5( esc_url_raw( trim( (string) $website_url ) ) . $api_key );
557
558 unset( self::$request_cache[ $cache_key ] );
559 delete_transient( $cache_key );
560
561 return self::get_all_import_data( $website_url, $api_key );
562 } // End refresh_import_data()
563
564
565 /**
566 * Helper to keep the remote request clean
567 *
568 * @param string $url
569 * @param string $api_key
570 * @param int $timeout
571 * @return array|\WP_Error
572 */
573 private static function fetch_remote_json( $url, $api_key = '', $timeout = 5 ) {
574 $args = [
575 'timeout' => absint( $timeout ),
576 'httpversion' => '1.1',
577 'user-agent' => 'AdminHelpDocs Importer; ' . home_url(),
578 'headers' => [
579 'Accept' => 'application/json',
580 ],
581 ];
582
583 if ( ! empty( $api_key ) ) {
584 $args[ 'headers' ][ 'X-HelpDocs-API-Key' ] = $api_key;
585 }
586
587 return wp_remote_get( $url, $args );
588 } // End fetch_remote_json()
589
590
591 /**
592 * Save import settings
593 */
594 public function save_import_settings() {
595 if ( Menu::get_current_page() !== Bootstrap::textdomain() || Menu::get_current_tab() !== 'import' ) {
596 return;
597 }
598
599 if ( ! isset( $_POST[ 'action' ] ) || 'helpdocs_import_docs' !== sanitize_text_field( wp_unslash( $_POST[ 'action' ] ) ) ) {
600 return;
601 }
602
603 if ( ! isset( $_POST[ 'helpdocs_save_import_nonce' ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ 'helpdocs_save_import_nonce' ] ) ), 'helpdocs_save_import_nonce' ) ) {
604 return;
605 }
606
607 if ( ! Helpers::user_can_edit() ) {
608 return;
609 }
610
611 $website_url = isset( $_POST[ 'helpdocs_website_url' ] ) ? sanitize_url( wp_unslash( $_POST[ 'helpdocs_website_url' ] ) ) : '';
612 if ( empty( $website_url ) ) {
613 return;
614 }
615
616 $import_id = isset( $_POST[ 'id' ] ) ? absint( $_POST[ 'id' ] ) : 0;
617 $is_active = isset( $_POST[ 'helpdocs_import_active' ] );
618 $post_status = $is_active ? 'publish' : 'draft';
619 $import_title = isset( $_POST[ 'helpdocs_import_title' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'helpdocs_import_title' ] ) ) : '';
620
621 if ( empty( $import_title ) ) {
622 $import_title = ucfirst( str_ireplace( [ 'http://', 'https://', 'www.' ], '', $website_url ) );
623 $import_title = untrailingslashit( $import_title );
624 }
625
626 $original_title = $import_title;
627 $count = 1;
628 while ( $this->title_exists( $import_title, $import_id ) ) {
629 $count++;
630 $import_title = ( $original_title ) . ' (' . $count . ')';
631 }
632
633 $post_data = [
634 'post_type' => Imports::$post_type,
635 'post_title' => $import_title,
636 'post_status' => $post_status,
637 ];
638
639 if ( ! $import_id ) {
640 $import_id = wp_insert_post( $post_data );
641 } else {
642 $post_data[ 'ID' ] = $import_id;
643 wp_update_post( $post_data );
644
645 // MIGRATION: Prune the old legacy key now that we're using the post status
646 delete_post_meta( $import_id, 'helpdocs_enabled' );
647 }
648
649 if ( is_wp_error( $import_id ) || ! $import_id ) {
650 return;
651 }
652
653 $api_key = isset( $_POST[ 'helpdocs_api_key' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'helpdocs_api_key' ] ) ) : '';
654
655 $all_docs = isset( $_POST[ 'helpdocs_all' ] ) ? '1' : '0';
656 $all_tocs = isset( $_POST[ 'helpdocs_all_tocs' ] ) ? '1' : '0';
657
658 $selected_docs = isset( $_POST[ 'helpdocs_auto_feed' ] ) ? array_map( 'absint', (array) wp_unslash( $_POST[ 'helpdocs_auto_feed' ] ) ) : [];
659 $selected_tocs = isset( $_POST[ 'helpdocs_tocs' ] ) ? array_map( 'absint', (array) wp_unslash( $_POST[ 'helpdocs_tocs' ] ) ) : [];
660
661 // Save Metadata
662 update_post_meta( $import_id, 'helpdocs_api_key', $api_key );
663 update_post_meta( $import_id, 'helpdocs_url', $website_url );
664 update_post_meta( $import_id, 'helpdocs_all', $all_docs );
665 update_post_meta( $import_id, 'helpdocs_all_tocs', $all_tocs );
666 update_post_meta( $import_id, 'helpdocs_docs', $selected_docs );
667 update_post_meta( $import_id, 'helpdocs_tocs', $selected_tocs );
668
669 // Redirect with ID and Success Message
670 $redirect_url = add_query_arg( [
671 'id' => $import_id,
672 'import-updated' => 'true',
673 ], wp_get_referer() );
674
675 wp_safe_redirect( $redirect_url );
676 exit;
677 } // End save_import_settings()
678
679
680 /**
681 * Helper to check if title already exists for this CPT
682 *
683 * @param string $title The title to check
684 * @param int $post_id The current post ID to exclude from the check
685 */
686 private function title_exists( $title, $post_id ) {
687 global $wpdb;
688 return $wpdb->get_var( $wpdb->prepare( // phpcs:ignore
689 "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = %s AND ID != %d LIMIT 1",
690 $title,
691 Imports::$post_type,
692 $post_id
693 ) );
694 } // End title_exists()
695
696
697 /**
698 * AJAX handler to fetch remote documents
699 */
700 public function ajax_fetch_remote_docs() {
701 check_ajax_referer( 'helpdocs_import_fetch_nonce', 'nonce' );
702
703 if ( ! Helpers::user_can_edit() ) {
704 wp_send_json_error( [ 'message' => __( 'You do not have permission to fetch remote documents.', 'admin-help-docs' ) ] );
705 }
706
707 $url = isset( $_POST[ 'url' ] ) ? sanitize_url( wp_unslash( $_POST[ 'url' ] ) ) : '';
708 $api_key = isset( $_POST[ 'api_key' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'api_key' ] ) ) : '';
709
710 if ( empty( $url ) ) {
711 wp_send_json_error( [ 'message' => __( 'Please enter a valid URL.', 'admin-help-docs' ) ] );
712 }
713
714 $import_data = self::refresh_import_data( $url, $api_key );
715 $docs = $import_data[ 'docs' ] ?? [];
716 $version = $import_data[ 'version' ] ?? 'v2';
717 $error = $import_data[ 'error' ] ?? '';
718
719 // If we have a specific error (unauthorized or connection_failed)
720 if ( ! empty( $error ) ) {
721 wp_send_json_success( [
722 'error' => $error,
723 'docs' => [],
724 'count' => 0,
725 'version' => $version
726 ] );
727 }
728
729 if ( empty( $docs ) ) {
730 wp_send_json_error( [ 'message' => __( 'No documents found at this URL.', 'admin-help-docs' ) ] );
731 }
732
733 ob_start();
734 $this->render_table_rows( $docs );
735 $html = ob_get_clean();
736
737 wp_send_json_success( [
738 'html' => $html,
739 'count' => count( $docs ),
740 'version' => $version,
741 'error' => ''
742 ] );
743 } // End ajax_fetch_remote_docs()
744
745
746 /**
747 * AJAX handler to import an individual document
748 */
749 public function ajax_import_individual_doc() {
750 check_ajax_referer( 'helpdocs_import_clone_nonce', 'nonce' );
751 if ( ! Helpers::user_can_edit() ) {
752 wp_send_json_error( __( 'You do not have permission to import documents.', 'admin-help-docs' ) );
753 }
754
755 $doc_id = isset( $_POST[ 'doc_id' ] ) ? absint( $_POST[ 'doc_id' ] ) : 0;
756 $import_id = isset( $_POST[ 'import_id' ] ) ? absint( $_POST[ 'import_id' ] ) : 0;
757 $url = isset( $_POST[ 'website_url' ] ) ? sanitize_url( wp_unslash( $_POST[ 'website_url' ] ) ) : '';
758 $api_key = isset( $_POST[ 'api_key' ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'api_key' ] ) ) : '';
759
760 if ( ! $doc_id || ! $url ) {
761 wp_send_json_error( __( 'Invalid request data.', 'admin-help-docs' ) );
762 }
763
764 $import_data = self::refresh_import_data( $url, $api_key );
765 if ( ! empty( $import_data[ 'error' ] ) ) {
766 wp_send_json_error( sprintf( __( 'Remote error: %s', 'admin-help-docs' ), $import_data[ 'error' ] ) );
767 }
768
769 // Fetch all remote data and find our specific doc
770 $remote_docs = $import_data[ 'docs' ] ?? [];
771 $raw_target = null;
772
773 foreach ( (array) $remote_docs as $doc ) {
774 if ( isset( $doc->ID ) && absint( $doc->ID ) === $doc_id ) {
775 $raw_target = $doc;
776 break;
777 }
778 }
779
780 if ( ! $raw_target ) {
781 wp_send_json_error( __( 'Could not find the document in the remote feed.', 'admin-help-docs' ) );
782 }
783
784 // Convert the entire stdClass object and all nested objects into associative arrays.
785 $target_doc = json_decode( json_encode( $raw_target ), true );
786
787 // 1. Prepare and Insert the Post
788 $new_post_id = wp_insert_post( [
789 'post_title' => sanitize_text_field( $target_doc[ 'title' ] ),
790 'post_content' => wp_kses_post( $target_doc[ 'content' ] ),
791 'post_excerpt' => sanitize_text_field( $target_doc[ 'desc' ] ?? '' ),
792 'post_status' => 'publish',
793 'post_type' => HelpDocs::$post_type,
794 ] );
795
796 if ( is_wp_error( $new_post_id ) ) {
797 wp_send_json_error( $new_post_id->get_error_message() );
798 }
799
800 // 2. Handle Taxonomies
801 if ( ! empty( $target_doc[ 'taxonomies' ] ) && is_array( $target_doc[ 'taxonomies' ] ) ) {
802 foreach ( $target_doc[ 'taxonomies' ] as $taxonomy => $terms ) {
803 if ( ! taxonomy_exists( $taxonomy ) || empty( $terms ) ) {
804 continue;
805 }
806
807 $term_ids = [];
808 foreach ( $terms as $term_data ) {
809 $term = get_term_by( 'slug', $term_data[ 'slug' ], $taxonomy );
810 if ( ! $term ) {
811 $new_term = wp_insert_term( $term_data[ 'name' ], $taxonomy, [ 'slug' => $term_data[ 'slug' ] ] );
812 if ( ! is_wp_error( $new_term ) ) {
813 $term_ids[] = (int) $new_term[ 'term_id' ];
814 }
815 } else {
816 $term_ids[] = (int) $term->term_id;
817 }
818 }
819 wp_set_object_terms( $new_post_id, $term_ids, $taxonomy );
820 }
821 }
822
823 // 3. Normalize Locations
824 $all_site_locations = HelpDocs::site_locations();
825 $final_locations = [];
826 $source_locations = [];
827
828 if ( ! empty( $target_doc[ 'locations' ] ) && is_array( $target_doc[ 'locations' ] ) ) {
829 $source_locations = $target_doc[ 'locations' ];
830 } elseif ( ! empty( $target_doc[ 'site_location' ] ) ) {
831 $source_locations = [
832 [
833 'site_location' => $target_doc[ 'site_location' ],
834 'page_location' => $target_doc[ 'page_location' ] ?? '',
835 'custom' => $target_doc[ 'custom' ] ?? '',
836 'post_types' => $target_doc[ 'post_types' ] ?? [],
837 'order' => $target_doc[ 'order' ] ?? 0,
838 'toc' => $target_doc[ 'toc' ] ?? false,
839 'css_selector' => $target_doc[ 'css_selector' ] ?? '',
840 'addt_params' => $target_doc[ 'addt_params' ] ?? false,
841 ]
842 ];
843 }
844
845 foreach ( $source_locations as $loc ) {
846 // $loc is now guaranteed to be an array because of our json_decode(..., true)
847 $site_key_encoded = sanitize_text_field( $loc[ 'site_location' ] ?? '' );
848 $site_key_decoded = base64_decode( $site_key_encoded );
849
850 if ( isset( $all_site_locations[ $site_key_decoded ] ) ) {
851 $clean = [ 'site_location' => $site_key_encoded ];
852 $allowed_fields = $all_site_locations[ $site_key_decoded ][ 'fields' ] ?? [];
853
854 foreach ( $allowed_fields as $field_rule ) {
855 $field_parts = explode( ':', $field_rule );
856 $field_name = ( count( $field_parts ) > 1 ) ? end( $field_parts ) : $field_rule;
857 $val = $loc[ $field_name ] ?? '';
858
859 switch ( $field_name ) {
860 case 'order':
861 $clean[ $field_name ] = intval( $val );
862 break;
863 case 'toc':
864 case 'addt_params':
865 $clean[ $field_name ] = ! empty( $val );
866 break;
867 case 'post_types':
868 // Ensure this is saved as a clean array of keys
869 $pts = is_serialized( $val ) ? unserialize( $val ) : (array) $val;
870 $clean[ $field_name ] = array_values( array_map( 'sanitize_key', (array) $pts ) );
871 break;
872 case 'custom':
873 $clean[ $field_name ] = esc_url_raw( $val );
874 break;
875 case 'page_location':
876 $clean[ $field_name ] = sanitize_key( $val );
877 break;
878 default:
879 $clean[ $field_name ] = sanitize_text_field( $val );
880 break;
881 }
882 }
883 $final_locations[] = $clean;
884 }
885 }
886
887 // 4. Update Meta
888 update_post_meta( $new_post_id, 'helpdocs_locations', $final_locations );
889 update_post_meta( $new_post_id, 'helpdocs_view_roles', array_map( 'sanitize_key', (array) ( $target_doc->view_roles ?? [] ) ) );
890 update_post_meta( $new_post_id, 'helpdocs_imported_doc_id', $doc_id );
891 update_post_meta( $new_post_id, 'helpdocs_import_id', $import_id );
892 update_post_meta( $new_post_id, 'helpdocs_imported_from', $url );
893 update_post_meta( $new_post_id, 'helpdocs_imported_by', get_current_user_id() );
894
895 // Compatibility meta for Classic Editor
896 if ( ! empty( $target_doc->editor_type ) ) {
897 update_post_meta( $new_post_id, 'classic-editor-remember', sanitize_text_field( $target_doc->editor_type ) );
898 }
899
900 // Support for extra fields added via 'helpdocs_api_doc_object_out' filter
901 $standard_keys = [ 'ID', 'title', 'created_by', 'publish_date', 'modified_date', 'modified_by', 'desc', 'content', 'taxonomies', 'site_location', 'page_location', 'custom', 'post_types', 'order', 'css_selector', 'addt_params', 'view_roles', 'editor_type' ];
902
903 foreach ( $target_doc as $key => $value ) {
904 if ( ! in_array( $key, $standard_keys ) ) {
905 update_post_meta( $new_post_id, 'helpdocs_' . sanitize_key( $key ), is_scalar( $value ) ? sanitize_text_field( $value ) : $value );
906 }
907 }
908
909 Helpers::flush_location_cache();
910
911 wp_send_json_success( __( 'Document imported successfully.', 'admin-help-docs' ) );
912 } // End ajax_import_individual_doc()
913
914
915 /**
916 * Prevent cloning and unserializing
917 */
918 public function __clone() {}
919 public function __wakeup() {}
920
921 }
922
923
924 ImportEditor::instance();