PluginProbe ʕ •ᴥ•ʔ
Admin Help Docs / trunk
Admin Help Docs vtrunk
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 / documentation.php
admin-help-docs / inc / tabs Last commit date
css 3 months ago js 3 months ago admin-menu.php 3 months ago documentation.php 3 months ago faq.php 3 months ago import.php 3 months ago settings.php 3 months ago support.php 3 months ago
documentation.php
724 lines
1 <?php
2 /**
3 * Documentation Tab Loader
4 */
5
6 namespace PluginRx\AdminHelpDocs;
7
8 if ( ! defined( 'ABSPATH' ) ) exit;
9
10 class Documentation {
11
12 /**
13 * The single instance of the class
14 *
15 * @var self|null
16 */
17 private static ?Documentation $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 * Constructor
32 */
33 private function __construct() {
34 add_action( 'helpdocs_subheader_right', [ $this, 'render_search_box' ] );
35 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_styles' ] );
36 add_action( 'wp_ajax_helpdocs_save_docs_order', [ $this, 'ajax_save_docs_order' ] );
37 if ( get_option( 'helpdocs_curly_quotes' ) ) {
38 add_action( 'current_screen', [ $this, 'remove_curly_quotes' ] );
39 }
40 } // End __construct()
41
42
43 /**
44 * Add a search box to the subheader on the folders page
45 *
46 * @param string $current_tab The current admin tab
47 * @return void
48 */
49 public function render_search_box( string $current_tab ) {
50 if ( $current_tab !== 'documentation' ) {
51 return;
52 }
53
54 $search_value = sanitize_text_field( wp_unslash( $_GET[ 'search' ] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
55 ?>
56 <div id="helpdocs-header-action-links" style="display: none;">
57 <a id="expand-all" class="action-links" href="#"><?php echo esc_html__( 'Expand Folders', 'admin-help-docs' ); ?></a>
58 <a id="collapse-all" class="action-links" href="#"><?php echo esc_html__( 'Collapse Folders', 'admin-help-docs' ); ?></a>
59 </div>
60 <form method="get" class="helpdocs-tax-search">
61 <input type="hidden" name="page" value="<?php echo esc_attr( Bootstrap::textdomain() ); ?>">
62 <input type="hidden" name="tab" value="documentation">
63 <input type="search"
64 name="search"
65 value="<?php echo esc_attr( $search_value ); ?>"
66 placeholder="<?php echo esc_attr__( 'Search Docs', 'admin-help-docs' ); ?>"
67 class="helpdocs-search-input" />
68
69 <input type="submit"
70 class="helpdocs-button"
71 value="<?php echo esc_attr__( 'Search', 'admin-help-docs' ); ?>" />
72
73 <a href="<?php
74 $clear_url = remove_query_arg( 'search' );
75 echo esc_url( $clear_url );
76 ?>" class="helpdocs-button">
77 <?php echo esc_html__( 'Clear', 'admin-help-docs' ); ?>
78 </a>
79 </form>
80 <?php
81 } // End render_search_box()
82
83
84 /**
85 * Render the tab
86 */
87 public function render_tab() {
88 $user_can_edit = Helpers::user_can_edit();
89
90 $docs = Helpers::get_docs( [
91 'site_location' => 'main',
92 'category' => isset( $_GET[ 'cat' ] ) ? absint( $_GET[ 'cat' ] ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
93 'tag' => isset( $_GET[ 'tag' ] ) ? sanitize_text_field( wp_unslash( $_GET[ 'tag' ] ) ) : '' // phpcs:ignore WordPress.Security.NonceVerification.Recommended
94 ] );
95
96 $docs = array_filter( $docs, fn( $doc ) => Helpers::user_can_view( $doc->ID ) );
97 if ( empty( $docs ) ) {
98 ?>
99 <br><br><br>
100 <?php
101 if ( $user_can_edit ) {
102 ?>
103 <em><?php esc_html_e( 'No documents found. Start by clicking "Add New" above!', 'admin-help-docs' ); ?></em>
104 <?php
105 } else {
106 ?>
107 <em><?php esc_html_e( 'No documents found.', 'admin-help-docs' ); ?></em>
108 <?php
109 }
110 return;
111 }
112
113 $doc_ids = wp_list_pluck( $docs, 'ID' );
114
115 // $docs_for_testing = array_map( function( $doc ) {
116 // return [
117 // 'ID' => $doc->ID,
118 // 'post_title' => $doc->post_title,
119 // 'helpdocs_order' => $doc->helpdocs_order,
120 // // 'feed_id' => $doc->feed_id ?? '',
121 // // 'helpdocs_view_roles' => $doc->helpdocs_view_roles ?? [],
122 // // 'helpdocs_locations' => $doc->helpdocs_locations ?? [],
123 // // 'local_folder_id' => $doc->local_folder_id ?? null,
124 // // 'taxonomies' => $doc->taxonomies ?? (object)[]
125 // ];
126 // }, $docs );
127 // dpr( $docs_for_testing );
128
129 $current_url = Bootstrap::tab_url( 'documentation' );
130
131 if ( isset( $_GET[ 'search' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
132 $s = sanitize_text_field( wp_unslash( $_GET[ 'search' ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
133 $current_url = add_query_arg( 'search', $s, $current_url );
134 } else {
135 $s = '';
136 }
137
138 $folder_taxonomy = Folders::$taxonomy;
139 $folders = get_terms( [
140 'taxonomy' => $folder_taxonomy,
141 'hide_empty' => false,
142 ] );
143
144 usort( $folders, function( $a, $b ) {
145 $order_a = get_term_meta( $a->term_id, 'helpdocs_order', true );
146 $order_b = get_term_meta( $b->term_id, 'helpdocs_order', true );
147
148 $order_a_missing = ( $order_a === '' );
149 $order_b_missing = ( $order_b === '' );
150
151 if ( $order_a_missing && $order_b_missing ) {
152 return strcasecmp( $a->name, $b->name );
153 }
154
155 if ( $order_a_missing ) {
156 return 1;
157 }
158
159 if ( $order_b_missing ) {
160 return -1;
161 }
162
163 return (int) $order_a - (int) $order_b;
164 } );
165
166 $mapped_folders = [];
167 foreach ( $folders as $folder ) {
168 $folder_id = $folder->term_id;
169 $folder_name = $folder->name;
170 $folder_slug = $folder->slug;
171
172 // 1. Get local docs assigned to this folder
173 $folder_doc_args = [
174 'post_type' => HelpDocs::$post_type,
175 'posts_per_page' => -1,
176 'post_status' => 'publish',
177 'tax_query' => [
178 [
179 'taxonomy' => $folder_taxonomy,
180 'field' => 'term_id',
181 'terms' => $folder_id,
182 ]
183 ],
184 'fields' => 'ids'
185 ];
186 $folder_docs = get_posts( $folder_doc_args );
187
188 // 2. Identify and map imports
189 foreach ( $docs as $doc ) {
190 if ( ! str_starts_with( (string) $doc->ID, 'import_' ) ) {
191 continue;
192 }
193
194 $is_match = false;
195
196 // Check if explicitly mapped via local_folder_id
197 if ( isset( $doc->local_folder_id ) && absint( $doc->local_folder_id ) === $folder_id ) {
198 $is_match = true;
199 }
200 // Fallback to taxonomy matching if local_folder_id isn't set
201 elseif ( isset( $doc->taxonomies ) && ! empty( $doc->taxonomies->$folder_taxonomy ) ) {
202 foreach ( $doc->taxonomies->$folder_taxonomy as $term ) {
203 if ( sanitize_title( $term->slug ) === $folder_slug || sanitize_text_field( $term->name ) === $folder_name ) {
204 $is_match = true;
205 break;
206 }
207 }
208 }
209
210 if ( $is_match ) {
211 $folder_docs[] = $doc->ID;
212 }
213 }
214
215 // Clean up and restrict to current viewable docs
216 $folder_docs = array_intersect( $folder_docs, $doc_ids );
217
218 if ( $user_can_edit || ! empty( $folder_docs ) ) {
219 $mapped_folders[ $folder_id ] = [
220 'name' => $folder_name,
221 'docs' => $folder_docs
222 ];
223 }
224 }
225
226 $current_doc_id = false;
227 $is_import = false;
228
229 if ( isset( $_GET[ 'id' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
230 $current_doc_id = sanitize_key( $_GET[ 'id' ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
231 $is_import = str_starts_with( (string) $current_doc_id, 'import_' );
232 if ( ! in_array( $current_doc_id, $doc_ids ) || ( ! $is_import && ! get_post_status( $current_doc_id ) ) ) {
233 $current_doc_id = false;
234 }
235 }
236
237 if ( ! $s && ! $current_doc_id ) {
238 $default_doc_id = get_option( 'helpdocs_default_doc' );
239 $is_import = str_starts_with( (string) $default_doc_id, 'import_' );
240 if ( $default_doc_id && ( $is_import || 'publish' == get_post_status( $default_doc_id ) ) ) {
241 $current_doc_id = $default_doc_id;
242 } else {
243 if ( ! empty( $docs ) ) {
244 $current_doc_id = $docs[0]->ID;
245 }
246 }
247 if ( $current_doc_id ) {
248 Helpers::add_qs_without_refresh( 'id', $current_doc_id );
249 }
250 }
251
252 $current_doc = (Object)[];
253 $feed = false;
254 ?>
255 <div id="helpdocs-sidebar" class="helpdocs-box">
256 <ul id="helpdocs-docs-list">
257 <?php
258
259 $in_folders = [];
260 if ( $s == '' && ! empty( $folders ) ) {
261 foreach ( $mapped_folders as $folder_id => $folder ) {
262
263 if ( ! $user_can_edit && empty( $folder[ 'docs' ] ) ) {
264 continue;
265 }
266
267 $folder_count = count( $folder[ 'docs' ] );
268
269 if ( $current_doc_id && in_array( $current_doc_id, $folder[ 'docs' ] ) ) {
270 $active_folder = ' active-folder';
271 } else {
272 $active_folder = ' hide-in-folder';
273 }
274
275 ?>
276 <li id="folder-<?php echo esc_attr( $folder_id ); ?>" class="helpdocs-folder<?php echo esc_attr( $active_folder ); ?>" data-type="folder" draggable="true" data-folder-id="<?php echo absint( $folder_id ); ?>">
277 <a href="#"><span class="folder-icon"></span> <?php echo esc_html( $folder[ 'name' ] ); ?> (<span class="folder-count"><?php echo absint( $folder_count ); ?></span>)</a>
278 </li>
279 <?php
280
281 foreach ( $docs as $doc ) {
282
283 if ( in_array( $doc->ID, $folder[ 'docs' ] ) ) {
284 $in_folders[] = $doc->ID;
285 } else {
286 continue;
287 }
288
289 $incl_doc = true;
290 if ( $s !== '' ) {
291
292 if ( strpos( strtolower( $doc->post_title ), strtolower( $s ) ) !== false ) {
293 $incl_doc = true;
294 } else if ( strpos( strtolower( $doc->post_content ), strtolower( $s ) ) !== false ) {
295 $incl_doc = true;
296 } else {
297 $incl_doc = false;
298 }
299 }
300
301 if ( ! $incl_doc ) {
302 continue;
303 }
304
305 if ( ! $current_doc_id ) {
306 $current_doc_id = $doc->ID;
307 Helpers::add_qs_without_refresh( 'id', $current_doc_id );
308 }
309
310 if ( $doc->ID == $current_doc_id ) {
311 $active = ' active';
312 $current_doc = $doc;
313 } else {
314 $active = '';
315 }
316
317 $is_folder_active = ( $current_doc_id && in_array( $current_doc_id, $folder[ 'docs' ] ) );
318 $visibility_class = $is_folder_active ? ' in-active-folder' : '';
319
320 if ( isset( $doc->auto_feed ) && $doc->auto_feed != '' ) {
321 $incl_feed = '&feed=true';
322 $feed = $doc->ID;
323 $file_icon_class = 'file-import-icon';
324 $data_import = 'true';
325 $import_id = $doc->feed_id;
326 } else {
327 $incl_feed = '';
328 $file_icon_class = 'file-icon';
329 $data_import = 'false';
330 $import_id = '';
331 }
332
333 $item_url = add_query_arg( [
334 'id' => $doc->ID,
335 ], $current_url );
336 ?>
337 <li id="item-<?php echo esc_attr( $doc->ID ); ?>" class="helpdocs-sidebar-item in-folder<?php echo esc_attr( $visibility_class ); ?><?php echo esc_attr( $active ); ?>" draggable="true" data-type="item" data-item-id="<?php echo esc_attr( $doc->ID ); ?>" data-import="<?php echo esc_attr( $data_import ); ?>" data-folder-id="<?php echo esc_attr( $folder_id ?? 0 ); ?>" data-import-id="<?php echo esc_attr( $import_id ); ?>">
338 <a href="<?php echo esc_url( $item_url ); ?><?php echo esc_attr( $incl_feed ); ?>"><span class="<?php echo esc_attr( $file_icon_class ); ?>"></span> <span class="item-title"><?php echo esc_html( $doc->post_title ); ?></span></a>
339 </li>
340 <?php
341 }
342 }
343 if ( $user_can_edit || ! empty( $in_folders ) ) {
344 ?>
345 <li id="folder-0" class="invisible-folder" data-folder="0"></li>
346 <?php
347 }
348 }
349
350 foreach ( $docs as $doc ) {
351
352 $incl_doc = true;
353 if ( $s !== '' ) {
354
355 if ( strpos( strtolower( $doc->post_title ), strtolower( $s ) ) !== false ) {
356 $incl_doc = true;
357 } elseif ( strpos( strtolower( $doc->post_content ), strtolower( $s ) ) !== false ) {
358 $incl_doc = true;
359 } else {
360 $incl_doc = false;
361 }
362 }
363
364 if ( ! $incl_doc ) {
365 continue;
366 }
367
368 if ( $s == '' && in_array( $doc->ID, $in_folders ) ) {
369 continue;
370 }
371
372 if ( ! $current_doc_id ) {
373 $current_doc_id = $doc->ID;
374 Helpers::add_qs_without_refresh( 'id', $current_doc_id );
375 }
376
377 if ( $doc->ID == $current_doc_id ) {
378 $active = ' active';
379 $current_doc = $doc;
380 } else {
381 $active = '';
382 }
383
384 if ( isset( $doc->auto_feed ) && $doc->auto_feed != '' ) {
385 $incl_feed = '&feed=true';
386 $feed = $doc->ID;
387 $file_icon_class = 'file-import-icon';
388 $data_import = 'true';
389 $import_id = $doc->feed_id;
390 } else {
391 $incl_feed = '';
392 $file_icon_class = 'file-icon';
393 $data_import = 'false';
394 $import_id = '';
395 }
396
397 $item_url = add_query_arg( [
398 'id' => $doc->ID,
399 ], $current_url );
400 ?>
401 <li id="item-<?php echo esc_attr( $doc->ID ); ?>" class="helpdocs-sidebar-item not-in-folder<?php echo esc_attr( $active ); ?>" draggable="true" data-type="item" data-item-id="<?php echo esc_attr( $doc->ID ); ?>" data-import="<?php echo esc_attr( $data_import ); ?>" data-folder-id="0" data-import-id="<?php echo esc_attr( $import_id ); ?>">
402 <a href="<?php echo esc_url( $item_url ); ?><?php echo esc_attr( $incl_feed ); ?>"><span class="<?php echo esc_attr( $file_icon_class ); ?>"></span> <span class="item-title"><?php echo esc_html( $doc->post_title ); ?></span></a>
403 </li>
404 <?php
405 }
406
407 ?>
408 </ul>
409 </div>
410 <?php
411 $current_doc_as_array = (array)$current_doc;
412 if ( ! empty( $current_doc_as_array ) ) {
413
414 ?>
415 <div id="helpdocs-document-viewer" class="helpdocs-box">
416 <?php
417 if ( ! get_option( 'helpdocs_hide_doc_meta' ) ) {
418
419 if ( $is_import ) {
420 $created_by = isset( $current_doc->created_by ) ? sanitize_text_field( $current_doc->created_by ) : __( 'an unknown author', 'admin-help-docs' );
421 $created_by .= ' from ' . esc_html( $current_doc->feed_id ? get_the_title( $current_doc->feed_id ) : __( 'an unknown source', 'admin-help-docs' ) );
422 } elseif ( is_numeric( $current_doc->post_author ) ) {
423 $created_by = get_userdata( $current_doc->post_author );
424 $created_by = $created_by->display_name;
425 } else {
426 $created_by = $current_doc->post_author;
427 }
428 $incl_created_by = __( 'Created: ', 'admin-help-docs' ) . date_i18n(
429 'F j, Y g:i A T',
430 strtotime( $current_doc->post_date )
431 ) . ' by ' . $created_by;
432
433 if ( $current_doc->_edit_last ) {
434
435 if ( is_numeric( $current_doc->_edit_last ) ) {
436 $modified_by = get_userdata( $current_doc->_edit_last );
437 $modified_by = $modified_by->display_name;
438 } else {
439 $modified_by = $current_doc->_edit_last;
440 }
441
442 $incl_modified = '<br>' . __( 'Last modified: ', 'admin-help-docs' ) . date_i18n(
443 'F j, Y g:i A T',
444 strtotime( $current_doc->post_modified )
445 ) . ' by ' . esc_attr( $modified_by );
446 } else {
447 $incl_modified = '';
448 }
449 } else {
450 $incl_created_by = '';
451 $incl_modified = '';
452 }
453
454 if ( Helpers::user_can_edit() ) {
455 if ( $feed == $current_doc_id ) {
456 $post_id = $current_doc->feed_id;
457 } else {
458 $post_id = $current_doc_id;
459 }
460 $edit_url = get_edit_post_link( $post_id );
461 $incl_edit = ' <span id="edit-link"><a href="' . esc_url( $edit_url ) . '">✎ ' . __( 'Edit', 'admin-help-docs' ) . '</a></span>';
462 } else {
463 $incl_edit = '';
464 }
465
466 if ( $feed == $current_doc_id ) {
467 $incl_feed = '<br>' . __( 'Content feed: ', 'admin-help-docs' ) . $current_doc->auto_feed;
468 } else {
469 $incl_feed = '';
470 }
471
472 if ( $s != '' ) {
473 $post_title = preg_replace( '/'.$s.'/i', '<span class="highlight">$0</span>', sanitize_text_field( $current_doc->post_title ) );
474 } else {
475 $post_title = sanitize_text_field( $current_doc->post_title );
476 }
477
478 ?>
479 <div id="helpdoc-header">
480 <h2><?php echo wp_kses_post( $post_title ); ?></h2><?php echo wp_kses_post( $incl_edit ); ?>
481 <span id="helpdocs-meta"><?php echo wp_kses_post( $incl_created_by ); ?>
482 <?php echo wp_kses_post( $incl_modified ); ?>
483 <?php echo wp_kses_post( $incl_feed ); ?></span>
484 </div>
485 <?php
486
487 if ( $s != '' ) {
488 $post_content = str_replace( $s, '<span class="highlight">'.$s.'</span>', $current_doc->post_content );
489 } else {
490 $post_content = $current_doc->post_content;
491 }
492
493 if ( get_option( 'helpdocs_auto_htoc' ) ) {
494 preg_match_all( '/<h([2-6])[^>]*>(.*?)<\/h\1>/i', $post_content, $matches, PREG_SET_ORDER );
495
496 if ( $matches ) {
497 $toc = '<div id="page-toc"><ul class="page-toc-list">';
498 $prev_level = 0;
499
500 foreach ( $matches as $match ) {
501 $level = intval( $match[1] );
502 $heading_text = wp_strip_all_tags( $match[2] );
503 $anchor = sanitize_title( $heading_text );
504
505 $post_content = preg_replace(
506 '/' . preg_quote( $match[0], '/' ) . '/',
507 '<h' . $level . ' id="' . $anchor . '">' . $heading_text . '</h' . $level . '>',
508 $post_content,
509 1 // Only replace first occurrence
510 );
511
512 if ( $prev_level && $level > $prev_level ) {
513 $toc .= str_repeat( '<ul>', $level - $prev_level );
514 } elseif ( $prev_level && $level < $prev_level ) {
515 $toc .= str_repeat( '</ul>', $prev_level - $level );
516 }
517
518 $toc .= '<li><a href="#' . $anchor . '">' . esc_html( rtrim( $heading_text, ':' ) ) . '</a></li>';
519 $prev_level = $level;
520 }
521
522 if ( $prev_level > 0 ) {
523 $toc .= str_repeat( '</ul>', $prev_level - 1 );
524 }
525
526 $toc .= '</ul></div>';
527
528 echo wp_kses_post( $toc );
529 }
530 } else {
531 echo '<hr class="helpdocs-title-hr">';
532 }
533
534 $allowed_tags = wp_kses_allowed_html( 'post' );
535 $allowed_tags = Helpers::allow_addt_tags( $allowed_tags );
536 ?>
537 <div id="helpdoc-content"><?php echo wp_kses( apply_filters( 'the_content', $post_content ), $allowed_tags ); ?></div>
538 </div>
539 <?php
540
541 // Search with no results
542 } else if ( $s !== '' ) {
543
544 Helpers::remove_qs_without_refresh( 'id' );
545 ?>
546 <div id="no-docs-found" class="helpdocs-box"><?php echo sprintf( esc_html__( 'No docs found with the keyword "%s"... Please try again.', 'admin-help-docs' ), esc_html( $s ) ); ?></div>
547 <?php
548
549 // Otherwise redirect to page without doc id
550 } else {
551 // wp_safe_redirect( Bootstrap::tab_url( 'documentation' ) );
552 }
553 } // End render_tab()
554
555
556 /**
557 * Enqueue admin styles.
558 */
559 public function enqueue_admin_styles() {
560 $text_domain = Bootstrap::textdomain();
561 $current_screen = get_current_screen();
562 $is_helpdocs_screen = $current_screen->id === 'toplevel_page_' . $text_domain || $current_screen->id === 'toplevel_page_' . $text_domain . '-network';
563 $current_tab = Menu::get_current_tab();
564
565 if ( $current_screen->id == HelpDocs::$post_type && $current_screen->base == 'post' && get_option( 'helpdocs_gutenberg_editor' ) ) {
566 wp_enqueue_style(
567 'wp-block-library-frontend',
568 includes_url( 'css/dist/block-library/style.css' ),
569 [],
570 Bootstrap::script_version()
571 );
572 }
573
574 if ( $is_helpdocs_screen && $current_tab === 'documentation' && get_option( 'helpdocs_enqueue_frontend_styles' ) ) {
575 global $wp_styles;
576
577 do_action( 'wp_enqueue_scripts' );
578
579 $skip = [ 'wp-block-library', 'wp-admin', 'colors', 'dashicons' ];
580
581 foreach ( $wp_styles->queue as $handle ) {
582 if ( in_array( $handle, $skip, true ) ) {
583 continue;
584 }
585
586 $style = $wp_styles->registered[ $handle ] ?? null;
587
588 if ( $style && ! wp_style_is( $handle, 'enqueued' ) ) {
589 wp_enqueue_style(
590 $handle,
591 $style->src,
592 $style->deps,
593 $style->ver
594 );
595 }
596 }
597
598 wp_add_inline_style( 'wp-admin', 'html.wp-toolbar { padding-top: 0 !important; }' );
599 }
600 } // End enqueue_admin_styles()
601
602
603 /**
604 * AJAX handler to save the order of docs and folders
605 */
606 public function ajax_save_docs_order() {
607 check_ajax_referer( 'helpdocs_documentation_nonce', 'nonce' );
608
609 if ( ! Helpers::user_can_edit() ) {
610 wp_send_json_error( __( 'Insufficient permissions.', 'admin-help-docs' ) );
611 }
612
613 // Get the arrays from wp_unslash before processing (sanitize below)
614 $folders = isset( $_POST[ 'folders' ] ) ? (array) wp_unslash( $_POST[ 'folders' ] ) : []; // phpcs:ignore
615 $items = isset( $_POST[ 'items' ] ) ? (array) wp_unslash( $_POST[ 'items' ] ) : []; // phpcs:ignore
616
617 // 1. Update Folder Order
618 foreach ( $folders as $index => $folder_id ) {
619 update_term_meta( absint( $folder_id ), 'helpdocs_order', $index + 1 );
620 }
621
622 // 2. Update Items (Files) Order and Taxonomy
623 $import_customizations = [];
624
625 foreach ( $items as $index => $item ) {
626 $raw_id = sanitize_text_field( $item[ 'id' ] );
627 $folder_id = absint( $item[ 'folder' ] );
628 $import_id = absint( $item[ 'import_id' ] );
629 $new_order = $index + 1;
630
631 // Imports
632 if ( ! empty( $import_id ) ) {
633 $remote_id = absint( str_replace( 'import_', '', $raw_id ) );
634 $import_customizations[ $import_id ][ $remote_id ] = [
635 'order' => $new_order,
636 'folder' => $folder_id
637 ];
638
639 // Local Docs
640 } else {
641 $item_id = absint( $raw_id );
642 $locations = get_post_meta( $item_id, 'helpdocs_locations', true );
643
644 // Backward Compatibility: If locations is empty, migrate legacy meta
645 if ( ! is_array( $locations ) || empty( $locations ) ) {
646 $meta = get_post_meta( $item_id );
647
648 $locations = [
649 [
650 'site_location' => $meta[ 'helpdocs_site_location' ][ 0 ] ?? '',
651 'page_location' => $meta[ 'helpdocs_page_location' ][ 0 ] ?? '',
652 'custom' => $meta[ 'helpdocs_custom' ][ 0 ] ?? '',
653 'addt_params' => ! empty( $meta[ 'helpdocs_addt_params' ][ 0 ] ),
654 'post_types' => isset( $meta[ 'helpdocs_post_types' ][ 0 ] ) ? Helpers::normalize_meta_array( $meta[ 'helpdocs_post_types' ][ 0 ] ) : [],
655 'order' => $new_order, // Set the new order here
656 'toc' => isset( $meta[ 'helpdocs_toc' ][ 0 ] ) ? filter_var( $meta[ 'helpdocs_toc' ][ 0 ], FILTER_VALIDATE_BOOLEAN ) : false,
657 'css_selector' => $meta[ 'helpdocs_css_selector' ][ 0 ] ?? '',
658 ]
659 ];
660
661 // Cleanup legacy keys now that we've migrated
662 $legacy_keys = [ 'site_location', 'page_location', 'custom', 'addt_params', 'post_types', 'order', 'toc', 'priority' ];
663 foreach ( $legacy_keys as $key ) {
664 delete_post_meta( $item_id, 'helpdocs_' . $key );
665 }
666 } else {
667 // New method: Update the 'main' location order (bWFpbg==)
668 foreach ( $locations as $key => $loc ) {
669 if ( isset( $loc[ 'site_location' ] ) && $loc[ 'site_location' ] === 'bWFpbg==' ) {
670 $locations[ $key ][ 'order' ] = $new_order;
671 break;
672 }
673 }
674 }
675
676 update_post_meta( $item_id, 'helpdocs_locations', $locations );
677
678 // Update Taxonomy Association (Local only)
679 wp_set_post_terms( $item_id, ( $folder_id > 0 ) ? [ $folder_id ] : [], Folders::$taxonomy );
680 }
681 }
682
683 // 3. Save bundled import customizations
684 foreach ( $import_customizations as $cpt_id => $data ) {
685 $existing = get_post_meta( $cpt_id, 'helpdocs_local_customizations', true ) ?: [];
686 $updated = array_replace( (array) $existing, $data );
687 update_post_meta( $cpt_id, 'helpdocs_local_customizations', $updated );
688 }
689
690 Helpers::flush_location_cache();
691
692 wp_send_json_success();
693 } // End ajax_save_docs_order()
694
695
696 /**
697 * Remove wptexturize filter to prevent curly quotes in documentation content
698 */
699 public function remove_curly_quotes() {
700 $current_screen = get_current_screen();
701 if ( ! is_admin() || ! $current_screen ) {
702 return;
703 }
704
705 $text_domain = Bootstrap::textdomain();
706 $is_helpdocs_screen = ( $current_screen->id === 'toplevel_page_' . $text_domain || $current_screen->id === 'toplevel_page_' . $text_domain . '-network' );
707 $current_tab = Menu::get_current_tab();
708
709 if ( $is_helpdocs_screen && 'documentation' === $current_tab ) {
710 remove_filter( 'the_content', 'wptexturize' );
711 }
712 } // End remove_curly_quotes()
713
714
715 /**
716 * Prevent cloning and unserializing
717 */
718 public function __clone() {}
719 public function __wakeup() {}
720
721 }
722
723
724 Documentation::instance();