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 / documentation.php
admin-help-docs / inc / tabs Last commit date
css 22 hours ago js 22 hours ago admin-menu.php 22 hours ago documentation.php 22 hours ago faq.php 22 hours ago import.php 22 hours ago migrate.php 22 hours ago settings.php 22 hours ago support.php 22 hours 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();