PluginProbe
weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot / trunk
weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot vtrunk
2.5.0 2.4.1 2.4.0 2.3.1 2.3.0 2.2.5 2.2.6 2.2.7 2.2.2 2.2.3 2.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.4 1.4.1 1.5 1.6 1.6.1 1.6.2 1.6.3 1.7 1.7.1 1.7.2 All 54 releases
wedocs / includes / functions.php

functions.php in weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot trunk, at includes/functions.php

1,225 lines 38.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Get template part implementation for wedocs.
4 * Looks at the theme directory first.
5 *
6 * @since 2.0.0
7 *
8 * @param string $slug
9 * @param string $name
10 * @param array $args
11 *
12 * @return void
13 */
14 function wedocs_get_template_part( $slug, $name = '', $args = array() ) {
15 $defaults = array(
16 'pro' => false,
17 );
18
19 $args = wp_parse_args( $args, $defaults );
20 if ( $args && is_array( $args ) ) {
21 extract( $args ); // phpcs:ignore
22 }
23
24 $wedocs = ! empty( $args['pro'] ) && true === $args['pro'] ? WeDocs_Pro::init() : WeDocs::init();
25 $template = '';
26
27 // Look in yourtheme/wedocs/slug-name.php and yourtheme/wedocs/slug.php.
28 $template_path = ! empty( $name ) ? "{$slug}-{$name}.php" : "{$slug}.php";
29 $template = locate_template( [ $wedocs->theme_dir_path() . $template_path ] );
30
31 $template_path = apply_filters( 'wedocs_set_template_path', $wedocs->plugin_path() . '/templates', $template, $args );
32
33 // Get default slug-name.php.
34 if ( ! $template && $name && file_exists( $template_path . "/{$slug}-{$name}.php" ) ) {
35 $template = $template_path . "/{$slug}-{$name}.php";
36 }
37
38 if ( ! $template && ! $name && file_exists( $template_path . "/{$slug}.php" ) ) {
39 $template = $template_path . "/{$slug}.php";
40 }
41
42 // Allow 3rd party plugin filter template file from their plugin
43 $template = apply_filters( 'wedocs_get_template_part', $template, $slug, $name );
44
45 if ( $template ) {
46 include $template;
47 }
48 }
49
50 /**
51 * Include a template by precedance.
52 *
53 * Looks at the theme directory first
54 *
55 * @param string $template_name
56 * @param array $args
57 *
58 * @return void
59 */
60 function wedocs_get_template( $template_name, $args = [] ) {
61 $wedocs = ! empty( $args['pro'] ) && true === $args['pro'] ? WeDocs_Pro::init() :WeDocs::init();
62
63 if ( $args && is_array( $args ) ) {
64 extract( $args );
65 }
66
67 $template = locate_template( [
68 $wedocs->theme_dir_path . $template_name,
69 $template_name,
70 ] );
71
72 if ( !$template ) {
73 $template = $wedocs->template_path() . $template_name;
74 }
75
76 if ( file_exists( $template ) ) {
77 include $template;
78 }
79 }
80
81 /**
82 * Control display content length.
83 *
84 * @since 2.0.0
85 *
86 * @param string $content
87 * @param int $max_content_number
88 *
89 * @return string
90 */
91 function wedocs_apply_short_content( $content, $max_content_number ) {
92 // Control content length by substr.
93 return ( mb_strlen( $content ) > $max_content_number ) ? mb_substr( $content, 0, $max_content_number ) . '...' : $content;
94 }
95
96 if ( ! function_exists( 'wedocs_get_doc_breadcrumb_trail' ) ) {
97
98 /**
99 * Build the ordered breadcrumb trail for a single doc post.
100 *
101 * Produces the default ("before_doc") trail: Docs hub crumb (if set),
102 * then the full ancestor chain root-first, then the current doc.
103 *
104 * Pro (or any third-party) can reorder the trail via the
105 * `wedocs_doc_breadcrumb_trail` filter — that's how the "after_doc"
106 * ordering is applied when the Pro URL structure feature is active.
107 *
108 * Each returned item is an array:
109 * - type: 'doc_ancestor' | 'docs_home' | 'current'
110 * - title: string (short-trimmed for ancestors)
111 * - url: string permalink, or null for the current crumb
112 * - post: WP_Post for doc-ancestor and current items, null for docs_home
113 *
114 * Does NOT include the "Home" crumb — callers render that themselves so
115 * they can keep their own home icon/link wrapping conventions.
116 *
117 * @since 2.2.2
118 *
119 * @param WP_Post|null $post Current doc post. Falls back to global $post.
120 *
121 * @return array<int, array<string, mixed>>
122 */
123 function wedocs_get_doc_breadcrumb_trail( $post = null ) {
124 if ( ! $post ) {
125 $post = get_post();
126 }
127
128 if ( ! $post || 'docs' !== $post->post_type ) {
129 return [];
130 }
131
132 $docs_home = wedocs_get_general_settings( 'docs_home' );
133
134 // Walk the ancestor chain bottom-up, then reverse to root-first order.
135 $ancestors = [];
136 $parent_id = (int) $post->post_parent;
137
138 while ( $parent_id ) {
139 $parent = get_post( $parent_id );
140 if ( ! $parent ) {
141 break;
142 }
143 $ancestors[] = $parent;
144 $parent_id = (int) $parent->post_parent;
145 }
146
147 $ancestors = array_reverse( $ancestors );
148 $trail = [];
149
150 if ( $docs_home ) {
151 $trail[] = [
152 'type' => 'docs_home',
153 'title' => __( 'Docs', 'wedocs' ),
154 'url' => get_permalink( $docs_home ),
155 'post' => null,
156 ];
157 }
158
159 foreach ( $ancestors as $ancestor ) {
160 $trail[] = [
161 'type' => 'doc_ancestor',
162 'title' => $ancestor->post_title,
163 'url' => get_permalink( $ancestor->ID ),
164 'post' => $ancestor,
165 ];
166 }
167
168 $trail[] = [
169 'type' => 'current',
170 'title' => $post->post_title,
171 'url' => null,
172 'post' => $post,
173 ];
174
175 return apply_filters( 'wedocs_doc_breadcrumb_trail', $trail, $post );
176 }
177 }
178
179 if ( !function_exists( 'wedocs_breadcrumbs' ) ) {
180
181 /**
182 * Docs breadcrumb.
183 *
184 * @return void
185 */
186 function wedocs_breadcrumbs() {
187 global $post;
188
189 $html = '';
190 $args = apply_filters( 'wedocs_breadcrumbs', [
191 'delimiter' => '<li class="delimiter"><i class="wedocs-icon wedocs-icon-angle-right"></i></li>',
192 'home' => __( 'Home', 'wedocs' ),
193 'before' => '<li><span class="current">',
194 'after' => '</span></li>',
195 ] );
196
197 $breadcrumb_position = 1;
198 $trail = wedocs_get_doc_breadcrumb_trail( $post );
199
200 $html .= '<ol class="wedocs-breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">';
201 $html .= '<li><i class="wedocs-icon wedocs-icon-home"></i></li>';
202 $html .= wedocs_get_breadcrumb_item( $args['home'], home_url( '/' ), $breadcrumb_position );
203 $html .= $args['delimiter'];
204
205 $last_index = count( $trail ) - 1;
206 foreach ( $trail as $index => $crumb ) {
207 if ( 'current' === $crumb['type'] ) {
208 $html .= ' ' . $args['before'] . esc_html( $crumb['title'] ) . $args['after'];
209 } else {
210 ++$breadcrumb_position;
211 $html .= wedocs_get_breadcrumb_item( $crumb['title'], $crumb['url'], $breadcrumb_position );
212 }
213
214 // Append a delimiter after every crumb except the last, regardless of order.
215 if ( $index !== $last_index ) {
216 // Preserve the existing whitespace quirk for ancestor delimiters (not the hub).
217 $html .= 'doc_ancestor' === $crumb['type'] ? ' ' . $args['delimiter'] . ' ' : $args['delimiter'];
218 }
219 }
220
221 $html .= '</ol>';
222
223 echo apply_filters( 'wedocs_breadcrumbs_html', $html, $args );
224 }
225 }
226
227 if ( !function_exists( 'wedocs_get_breadcrumb_item' ) ) {
228
229 /**
230 * Schema.org breadcrumb item wrapper for a link.
231 *
232 * @param string $label
233 * @param string $permalink
234 * @param int $position
235 *
236 * @return string
237 */
238 function wedocs_get_breadcrumb_item( $label, $permalink, $position = 1 ) {
239 $breadcrumb_label = wedocs_apply_short_content( $label, 25 );
240
241 return apply_filters(
242 'wedocs_breadcrumbs_items',
243 '<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
244 <a itemprop="item" href="' . esc_attr( $permalink ) . '">
245 <span itemprop="name">' . esc_html( $breadcrumb_label ) . '</span></a>
246 <meta itemprop="position" content="' . $position . '" />
247 </li>'
248 );
249 }
250 }
251
252 /**
253 * Get next and previous document posts for navigation.
254 * Improved version that handles menu_order = 0 and non-sequential ordering.
255 *
256 * @param WP_Post $post Current post object
257 * @return array Array with 'next' and 'prev' keys containing post objects or null
258 */
259 function wedocs_get_doc_navigation_posts( $post ) {
260 global $wpdb;
261
262 if ( ! $post || $post->post_type !== 'docs' ) {
263 return [ 'next' => null, 'prev' => null ];
264 }
265 // Get all sibling posts ordered by menu_order, then by date
266 $siblings_query = "SELECT ID, post_title, menu_order FROM {$wpdb->posts}
267 WHERE post_parent = {$post->post_parent} and post_type = 'docs' and post_status = 'publish'
268 ORDER BY menu_order ASC, post_date ASC";
269 $siblings = $wpdb->get_results( $siblings_query );
270 $next_post = null;
271 $prev_post = null;
272 $current_found = false;
273 // Find current post position and determine next/prev
274 foreach ( $siblings as $index => $sibling ) {
275 if ( $sibling->ID == $post->ID ) {
276 $current_found = true;
277 // Get previous post (if exists)
278 if ( $index > 0 ) {
279 $prev_post = $siblings[ $index - 1 ];
280 }
281 // Get next post (if exists)
282 if ( $index < count( $siblings ) - 1 ) {
283 $next_post = $siblings[ $index + 1 ];
284 }
285 break;
286 }
287 }
288 // Fallback to original queries if current post not found in siblings
289 if ( ! $current_found ) {
290 $next_query = "SELECT ID, post_title FROM {$wpdb->posts}
291 WHERE post_parent = {$post->post_parent} and post_type = 'docs' and post_status = 'publish' and menu_order > {$post->menu_order}
292 ORDER BY menu_order ASC
293 LIMIT 0, 1";
294 $prev_query = "SELECT ID, post_title FROM {$wpdb->posts}
295 WHERE post_parent = {$post->post_parent} and post_type = 'docs' and post_status = 'publish' and menu_order < {$post->menu_order}
296 ORDER BY menu_order DESC
297 LIMIT 0, 1";
298 $next_post = $wpdb->get_row( $next_query );
299 $prev_post = $wpdb->get_row( $prev_query );
300 }
301
302 return [
303 'next' => $next_post,
304 'prev' => $prev_post,
305 ];
306 }
307
308 /**
309 * Next, previous post navigation for a single doc.
310 *
311 * @return void
312 */
313 function wedocs_doc_nav() {
314 global $post;
315
316 // Use the improved navigation function
317 $navigation_posts = wedocs_get_doc_navigation_posts($post);
318 $next_post = $navigation_posts['next'];
319 $prev_post = $navigation_posts['prev'];
320
321 if ( $next_post || $prev_post ) {
322 echo '<nav class="wedocs-doc-nav wedocs-hide-print">';
323 echo '<h3 class="assistive-text screen-reader-text">' . __( 'Doc navigation', 'wedocs' ) . '</h3>';
324
325 if ( $prev_post ) {
326 echo '<span class="nav-prev"><a href="' . get_permalink( $prev_post->ID ) . '">&larr; ' . apply_filters( 'wedocs_translate_text', $prev_post->post_title ) . '</a></span>';
327 }
328
329 if ( $next_post ) {
330 echo '<span class="nav-next"><a href="' . get_permalink( $next_post->ID ) . '">' . apply_filters( 'wedocs_translate_text', $next_post->post_title ) . ' &rarr;</a></span>';
331 }
332
333 echo '</nav>';
334 }
335 }
336
337 if ( !function_exists( 'wedocs_get_posts_children' ) ) {
338
339 /**
340 * Recursively fetch child posts.
341 *
342 * @param int $parent_id
343 * @param string $post_type
344 *
345 * @return array
346 */
347 function wedocs_get_posts_children( $parent_id, $post_type = 'page', $custom_args = array() ) {
348 $children = array();
349
350 $default = array(
351 'numberposts' => -1,
352 'post_status' => 'publish',
353 'post_type' => $post_type,
354 'post_parent' => $parent_id,
355 'suppress_filters' => false,
356 );
357
358 // Parse posts arguments.
359 $args = wp_parse_args( $custom_args, $default );
360
361 // grab the posts children
362 $posts = get_posts( $args );
363
364 // now grab the grand children
365 foreach ( $posts as $child ) {
366 // recursion!! hurrah
367 $gchildren = wedocs_get_posts_children( $child->ID, $post_type );
368
369 // merge the grand children into the children array
370 if ( !empty( $gchildren ) ) {
371 $children = array_merge( $children, $gchildren );
372 }
373 }
374
375 // merge in the direct descendants we found earlier
376 $children = array_merge( $children, $posts );
377
378 return $children;
379 }
380 }
381
382 /**
383 * Retrieve the tags for a doc formatted as a string.
384 *
385 * @param string $before Optional. Before tags.
386 * @param string $sep Optional. Between tags.
387 * @param string $after Optional. After tags.
388 * @param int $id Optional. Post ID. Defaults to the current post.
389 *
390 * @return string|false|WP_Error a list of tags on success, false if there are no terms, WP_Error on failure
391 */
392 function wedocs_get_the_doc_tags( $post_id, $before = '', $sep = '', $after = '' ) {
393 return get_the_term_list( $post_id, 'doc_tag', $before, $sep, $after );
394 }
395
396 // Check if QTranslate plugin is active before function declaration
397 $is_qtranslate = wedocs_is_plugin_active( 'qtranslate-x/qtranslate.php' );
398
399 if ( $is_qtranslate ) {
400 /**
401 * Translate dynamic text with QTranslate X plugin.
402 *
403 * @param string $text the multilingual text
404 *
405 * @return string the translated text
406 */
407 function wedocs_translate_text_with_qtranslate( $text ) {
408 return apply_filters( 'translate_text', $text );
409 }
410
411 add_filter( 'wedocs_translate_text', 'wedocs_translate_text_with_qtranslate', 10, 1 );
412 }
413
414 /**
415 * Check if a plugin is active.
416 *
417 * @param string $plugin_path_and_name the plugin relative path and filename of the plugin main file
418 *
419 * @return bool whether the plugin is active or not
420 */
421 function wedocs_is_plugin_active( $plugin_path_and_name ) {
422 if ( ! function_exists( 'is_plugin_active' ) ) {
423 include_once ABSPATH . 'wp-admin/includes/plugin.php';
424 }
425
426 return is_plugin_active( $plugin_path_and_name );
427 }
428
429 /**
430 * Get the value of a settings field.
431 *
432 * @param string $option settings field name
433 * @param string $section the section name this field belongs to
434 * @param string $default default text if it's not found
435 *
436 * @return mixed
437 */
438 function wedocs_get_option( $option, $section, $default = '' ) {
439 $options = get_option( $section );
440
441 if ( isset( $options[ $option ] ) ) {
442 return $options[ $option ];
443 }
444
445 return $default;
446 }
447
448 /**
449 * Get the value of general settings.
450 *
451 * @since 2.0.0
452 *
453 * @param string $field_name general settings field name.
454 * @param string $default default data if settings not found.
455 *
456 * @return mixed
457 */
458 function wedocs_get_general_settings( $field_name = '', $default = '' ) {
459 $general_settings = wedocs_get_option( 'general', 'wedocs_settings', [] );
460
461 if ( ! empty( $field_name ) ) {
462 $wedocs_field_data = wedocs_get_option( $field_name, 'wedocs_settings', $default );
463
464 // Check from general settings if not found then collect data from wedocs_settings.
465 return ! empty( $general_settings[ $field_name ] ) ? $general_settings[ $field_name ] : $wedocs_field_data;
466 }
467
468 return $general_settings;
469 }
470
471 /**
472 * Get a clients IP address.
473 *
474 * @return string
475 */
476 function wedocs_get_ip_address() {
477 $ipaddress = '';
478
479 if ( isset( $_SERVER['HTTP_CLIENT_IP'] ) ) {
480 $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
481 } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
482 $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
483 } elseif ( isset( $_SERVER['HTTP_X_FORWARDED'] ) ) {
484 $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
485 } elseif ( isset( $_SERVER['HTTP_FORWARDED_FOR'] ) ) {
486 $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
487 } elseif ( isset( $_SERVER['HTTP_FORWARDED'] ) ) {
488 $ipaddress = $_SERVER['HTTP_FORWARDED'];
489 } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
490 $ipaddress = $_SERVER['REMOTE_ADDR'];
491 } else {
492 $ipaddress = 'UNKNOWN';
493 }
494
495 return $ipaddress;
496 }
497
498 /**
499 * Send email feedback on a document.
500 *
501 * @param int $doc_id
502 * @param string $author
503 * @param string $email
504 * @param string $subject
505 * @param string $message
506 *
507 * @since 1.2
508 *
509 * @return void
510 */
511 function wedocs_doc_feedback_email( $doc_id, $author, $email, $subject, $message ) {
512 $wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', strtolower( $_SERVER['SERVER_NAME'] ) );
513 $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
514 $document = get_post( $doc_id );
515
516 // Collect feedback sending email address & prepare body.
517 $email_to = wedocs_get_general_settings( 'email_to', get_option( 'admin_email' ) );
518 $subject = sprintf( __( '[%1$s] New Doc Feedback: "%2$s"', 'wedocs' ), $blogname, $subject );
519
520 $email_body = sprintf( __( 'New feedback on your doc "%s"', 'wedocs' ), apply_filters( 'wedocs_translate_text', $document->post_title ) ) . "\r\n";
521 $email_body .= sprintf( __( 'Author: %1$s (IP: %2$s)', 'wedocs' ), $author, wedocs_get_ip_address() ) . "\r\n";
522 $email_body .= sprintf( __( 'Email: %s', 'wedocs' ), $email ) . "\r\n";
523 $email_body .= sprintf( __( 'Feedback: %s', 'wedocs' ), "\r\n" . $message ) . "\r\n\r\n";
524 $email_body .= sprintf( __( 'Doc Permalink: %s', 'wedocs' ), get_permalink( $document ) ) . "\r\n";
525 $email_body .= sprintf( __( 'Edit Doc: %s', 'wedocs' ), admin_url( 'post.php?action=edit&post=' . $doc_id ) ) . "\r\n";
526
527 $from = "From: $author <$wp_email>";
528 $reply_to = "Reply-To: $email <$email>";
529
530 $message_headers = "$from\n"
531 . 'Content-Type: text/plain; charset ="' . get_option( 'blog_charset' ) . "\"\n";
532 $message_headers .= $reply_to . "\n";
533
534 $email_to = apply_filters( 'wedocs_email_feedback_to', $email_to, $doc_id, $document );
535 $subject = apply_filters( 'wedocs_email_feedback_subject', $subject, $doc_id, $document, $_POST );
536 $email_body = apply_filters( 'wedocs_email_feedback_body', $email_body, $doc_id, $document, $_POST );
537 $message_headers = apply_filters( 'wedocs_email_feedback_headers', $message_headers, $doc_id, $document, $_POST );
538
539 @wp_mail( $email_to, wp_specialchars_decode( $subject ), $email_body, $message_headers );
540 }
541
542 /**
543 * Get the publishing capability for weDocs admin.
544 *
545 * @since 1.3
546 *
547 * @return string
548 */
549 function wedocs_get_publish_cap() {
550 return apply_filters( 'wedocs_publish_cap', 'manage_categories' );
551 }
552
553 if ( ! function_exists( 'wedocs_template_wrapper_start' ) ) {
554
555 /**
556 * Template start wrapper.
557 *
558 * @since 1.4
559 *
560 * @return void
561 */
562 function wedocs_template_wrapper_start() {
563 echo '<div id="primary" class="content-area">';
564 echo '<main id="main" class="site-main" role="main">';
565 }
566 }
567
568 if ( !function_exists( 'wedocs_template_wrapper_end' ) ) {
569
570 /**
571 * Template end wrapper.
572 *
573 * @since 1.4
574 *
575 * @return void
576 */
577 function wedocs_template_wrapper_end() {
578 echo '</main><!-- .site-main -->';
579 echo '</div><!-- .content-area -->';
580 }
581 }
582
583 add_action( 'wedocs_before_main_content', 'wedocs_template_wrapper_start', 10 );
584 add_action( 'wedocs_after_main_content', 'wedocs_template_wrapper_end', 10 );
585
586 /**
587 * Sidebar open/closed status css classes.
588 *
589 * @param array $css_class
590 * @param WP_Post $page
591 * @param int $depth
592 * @param array $args
593 * @param int $current_page
594 *
595 * @return array
596 */
597 function wedocs_sidebar_page_status_class( $css_class, $page, $depth, $args, $current_page ) {
598 if ( 'docs' != $page->post_type ) {
599 return $css_class;
600 }
601
602 if ( 0 == $depth ) {
603 if ( in_array( 'current_page_item', $css_class ) ) {
604 $css_class[] = 'wd-state-open';
605 } else {
606 $css_class[] = 'wd-state-closed';
607 }
608 }
609
610 return $css_class;
611 }
612
613 add_filter( 'page_css_class', 'wedocs_sidebar_page_status_class', 20, 5 );
614
615 /**
616 * Add weDocs documentation handling capabilities for users.
617 *
618 * @since 1.0.0
619 *
620 * @return void
621 */
622 function wedocs_user_documentation_handling_capabilities() {
623 global $wp_roles;
624
625 if ( class_exists( 'WP_Roles' ) && ! isset( $wp_roles ) ) {
626 $wp_roles = new \WP_Roles(); // @codingStandardsIgnoreLine
627 }
628
629 $permitted_roles = array( 'administrator', 'editor' );
630 $all_roles = $wp_roles->get_names();
631 $capabilities = array( 'edit_docs', 'publish_docs', 'edit_others_docs', 'read_private_docs', 'edit_private_docs', 'edit_published_docs' );
632
633 // First, remove capabilities from unauthorized roles (cleanup for existing installations)
634 foreach ( $capabilities as $capability ) {
635 foreach ( array_keys( $all_roles ) as $role_key ) {
636 $role = $wp_roles->get_role( $role_key );
637 if ( $role && $role->has_cap( $capability ) && ! in_array( $role_key, $permitted_roles, true ) ) {
638 $wp_roles->remove_cap( $role_key, $capability );
639 }
640 }
641 }
642
643 // Push documentation handling access ONLY to permitted roles.
644 foreach ( $capabilities as $capability ) {
645 foreach ( $permitted_roles as $role_key ) {
646 if ( $wp_roles->is_role( $role_key ) ) {
647 $wp_roles->add_cap( $role_key, $capability );
648 }
649 }
650 }
651 }
652
653 /**
654 * Check premium version
655 * availability.
656 *
657 * @since 2.0.0
658 *
659 * @return bool
660 */
661 function wedocs_pro_exists() {
662 if ( ! class_exists( 'WeDocs_Pro' ) ) {
663 return false;
664 }
665
666 // Check weDocs pro plugin domain availability.
667 $active_plugins = get_option( 'active_plugins' );
668 foreach ( $active_plugins as $plugin ) {
669 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
670 $plugin_text_domain = ! empty( $plugin_data[ 'TextDomain' ] ) ? sanitize_key( $plugin_data[ 'TextDomain' ] ) : '';
671
672 if ( $plugin_text_domain === 'wedocs-pro' ) {
673 return true;
674 }
675 }
676
677 return false;
678 }
679
680 /**
681 * Collect active layout colors.
682 *
683 * @since 2.0.2
684 *
685 * @return array
686 */
687 function wedocs_get_search_modal_active_colors() {
688 return apply_filters(
689 'wedocs_search_modal_active_colors',
690 array(
691 'active_primary_color' => '#3B82F6',
692 'active_shade_color' => '#D9EBFF',
693 'active_font_color' => '#fff',
694 )
695 );
696 }
697
698 /**
699 * Control displaying content length.
700 *
701 * @since 2.1.1
702 *
703 * @param string $content
704 * @param int $max_content_number
705 *
706 * @return string
707 */
708 function wedocs_apply_extracted_content( $content, $max_content_number ) {
709 // Control content length by substr.
710 return ( mb_strlen( $content ) > $max_content_number ) ? mb_substr( $content, 0, $max_content_number ) . '...' : $content;
711 }
712
713 /**
714 * Convert UTC Time zone to EST timezone
715 *
716 * @param string $date_time
717 * @return string
718 */
719 function wedocs_convert_utc_to_est() {
720 $current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
721 $current_time->setTimezone( new DateTimeZone( 'EST' ) );
722
723 return $current_time->format( 'Y-m-d H:i:s T' );
724 }
725
726 /**
727 * Get AI provider configurations.
728 *
729 * Returns configuration for all supported AI providers including their models
730 * and vision capabilities for image analysis support.
731 *
732 * @since 2.1.15
733 * @since 2.2.0 Added vision capability metadata for image analysis support.
734 *
735 * @return array Provider configurations with models and capabilities.
736 */
737 function wedocs_get_ai_provider_configs() {
738 $provider_configs = [
739 'openai' => [
740 'name' => 'OpenAI',
741 'endpoint' => 'https://api.openai.com/v1/chat/completions',
742 'models' => [
743 'gpt-4o' => [
744 'name' => 'GPT-4o - Most Capable Multimodal',
745 'vision' => true,
746 ],
747 'gpt-4o-mini' => [
748 'name' => 'GPT-4o Mini - Efficient & Fast',
749 'vision' => true,
750 ],
751 'gpt-4-turbo' => [
752 'name' => 'GPT-4 Turbo - High Performance',
753 'vision' => true,
754 ],
755 'gpt-4' => [
756 'name' => 'GPT-4 - Advanced Reasoning',
757 'vision' => false,
758 ],
759 'gpt-3.5-turbo' => [
760 'name' => 'GPT-3.5 Turbo - Fast & Affordable',
761 'vision' => false,
762 ],
763 ],
764 'requires_key' => true,
765 ],
766 'anthropic' => [
767 'name' => 'Anthropic Claude',
768 'endpoint' => 'https://api.anthropic.com/v1/messages',
769 'models' => [
770 'claude-opus-4-5-20251101' => [
771 'name' => 'Claude Opus 4.5 - Most Capable',
772 'vision' => true,
773 ],
774 'claude-opus-4-20250514' => [
775 'name' => 'Claude Opus 4 - Best Coding Model',
776 'vision' => true,
777 ],
778 'claude-sonnet-4-20250514' => [
779 'name' => 'Claude Sonnet 4 - Advanced Reasoning',
780 'vision' => true,
781 ],
782 'claude-3-7-sonnet-20250219' => [
783 'name' => 'Claude 3.7 Sonnet - Hybrid Reasoning',
784 'vision' => true,
785 ],
786 'claude-3-5-sonnet-20241022' => [
787 'name' => 'Claude 3.5 Sonnet Latest',
788 'vision' => true,
789 ],
790 'claude-3-5-sonnet-20240620' => [
791 'name' => 'Claude 3.5 Sonnet',
792 'vision' => true,
793 ],
794 'claude-3-5-haiku-20241022' => [
795 'name' => 'Claude 3.5 Haiku',
796 'vision' => true,
797 ],
798 'claude-3-opus-20240229' => [
799 'name' => 'Claude 3 Opus',
800 'vision' => true,
801 ],
802 'claude-3-haiku-20240307' => [
803 'name' => 'Claude 3 Haiku',
804 'vision' => true,
805 ],
806 ],
807 'requires_key' => true,
808 ],
809 'google' => [
810 'name' => 'Google Gemini',
811 'endpoint' => 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
812 'models' => [
813 'gemini-2.0-flash-exp' => [
814 'name' => 'Gemini 2.0 Flash Experimental - Latest',
815 'vision' => true,
816 ],
817 'gemini-2.0-flash' => [
818 'name' => 'Gemini 2.0 Flash - Stable',
819 'vision' => true,
820 ],
821 'gemini-2.0-flash-001' => [
822 'name' => 'Gemini 2.0 Flash 001 - Stable Version',
823 'vision' => true,
824 ],
825 'gemini-2.0-flash-lite-001' => [
826 'name' => 'Gemini 2.0 Flash-Lite 001 - Lightweight',
827 'vision' => true,
828 ],
829 'gemini-2.0-flash-lite' => [
830 'name' => 'Gemini 2.0 Flash-Lite - Lightweight',
831 'vision' => true,
832 ],
833 'gemini-2.5-flash' => [
834 'name' => 'Gemini 2.5 Flash - Latest Stable',
835 'vision' => true,
836 ],
837 'gemini-2.5-pro' => [
838 'name' => 'Gemini 2.5 Pro - Most Capable',
839 'vision' => true,
840 ],
841 'gemini-2.5-flash-lite' => [
842 'name' => 'Gemini 2.5 Flash-Lite - Efficient',
843 'vision' => true,
844 ],
845 'gemini-flash-latest' => [
846 'name' => 'Gemini Flash Latest - Auto-Updated',
847 'vision' => true,
848 ],
849 'gemini-pro-latest' => [
850 'name' => 'Gemini Pro Latest - Auto-Updated',
851 'vision' => true,
852 ],
853 ],
854 'requires_key' => true,
855 ],
856 ];
857
858 return apply_filters( 'wedocs_ai_provider_configs', $provider_configs );
859 }
860
861 /**
862 * Check if a specific AI model supports vision/image analysis.
863 *
864 * @since 2.2.0
865 *
866 * @param string $provider The provider key (openai, anthropic, google).
867 * @param string $model The model identifier.
868 *
869 * @return bool True if the model supports vision, false otherwise.
870 */
871 function wedocs_model_supports_vision( $provider, $model ) {
872 $configs = wedocs_get_ai_provider_configs();
873
874 // 1. Check the static config first — cheapest path.
875 if ( isset( $configs[ $provider ]['models'][ $model ] ) ) {
876 $model_config = $configs[ $provider ]['models'][ $model ];
877 if ( is_array( $model_config ) ) {
878 return ! empty( $model_config['vision'] );
879 }
880 return false;
881 }
882
883 // 2. Model not in the static list — check the transient cached by the dynamic
884 // model endpoint (wedocs_ai_models_{provider}_{key_hash}).
885 $all_settings = get_option( 'wedocs_settings', [] );
886 $ai_settings = is_array( $all_settings['ai'] ?? null ) ? $all_settings['ai'] : [];
887 $api_key = $ai_settings['providers'][ $provider ]['api_key'] ?? '';
888
889 if ( $api_key ) {
890 $cache_key = 'wedocs_ai_models_' . $provider . '_' . substr( md5( $api_key ), 0, 8 );
891 $cached_models = get_transient( $cache_key );
892
893 if ( is_array( $cached_models ) ) {
894 foreach ( $cached_models as $m ) {
895 if ( ( $m['id'] ?? '' ) === $model ) {
896 return ! empty( $m['vision'] );
897 }
898 }
899 }
900 }
901
902 // 3. Pattern-based fallback for models unknown to both sources.
903 return wedocs_model_has_vision_by_pattern( $provider, $model );
904 }
905
906 /**
907 * Detect vision support by matching well-known model ID patterns.
908 *
909 * Used as a last resort when a model is absent from both the static config and
910 * the cached dynamic model list (e.g. a brand-new model released after the
911 * cache was last populated).
912 *
913 * @since 2.2.1
914 *
915 * @param string $provider Provider key (openai | anthropic | google).
916 * @param string $model Model identifier.
917 *
918 * @return bool
919 */
920 function wedocs_model_has_vision_by_pattern( $provider, $model ) {
921 switch ( $provider ) {
922 case 'openai':
923 return (
924 strpos( $model, 'gpt-4o' ) !== false ||
925 strpos( $model, 'gpt-4-turbo' ) !== false ||
926 strpos( $model, 'gpt-4-vision' ) !== false ||
927 strpos( $model, 'chatgpt-4o' ) !== false ||
928 preg_match( '/^o[1-9][-_]/', $model ) === 1
929 );
930 case 'anthropic':
931 // claude-3 and all claude-4 families expose multimodal input.
932 return preg_match( '/^claude-(3|3-[57]|opus-4|sonnet-4|haiku-4)/', $model ) === 1;
933 case 'google':
934 // Nearly all Gemini models support vision; AQA is text-only.
935 return strpos( $model, 'gemini' ) !== false && strpos( $model, 'aqa' ) === false;
936 default:
937 return false;
938 }
939 }
940
941 /**
942 * Get the display name of an AI model.
943 *
944 * @since 2.2.0
945 *
946 * @param string $provider The provider key (openai, anthropic, google).
947 * @param string $model The model identifier.
948 *
949 * @return string The model display name.
950 */
951 function wedocs_get_model_name( $provider, $model ) {
952 $configs = wedocs_get_ai_provider_configs();
953
954 if ( ! isset( $configs[ $provider ]['models'][ $model ] ) ) {
955 return $model;
956 }
957
958 $model_config = $configs[ $provider ]['models'][ $model ];
959
960 // Handle both old string format and new array format for backward compatibility.
961 if ( is_array( $model_config ) ) {
962 return $model_config['name'] ?? $model;
963 }
964
965 return $model_config;
966 }
967
968 /**
969 * Get AI settings safe for frontend localization.
970 *
971 * Strips sensitive fields such as API keys and secrets from the AI
972 * settings array so they are never exposed via wp_localize_script.
973 *
974 * @since 2.2.1
975 *
976 * @return array Sanitized AI settings without sensitive data.
977 */
978 function wedocs_get_ai_settings_for_frontend() {
979 $ai_settings = wedocs_get_option( 'ai', 'wedocs_settings', [] );
980
981 // Fields that must never be sent to the frontend.
982 $sensitive_fields = [ 'api_key', 'api_secret', 'secret_key', 'access_token' ];
983
984 // Fetch once before the loop so we don't hit the options table on every iteration.
985 $original_config = wedocs_get_option( 'ai', 'wedocs_settings', [] );
986
987 if ( ! empty( $ai_settings['providers'] ) && is_array( $ai_settings['providers'] ) ) {
988 foreach ( $ai_settings['providers'] as $provider => &$config ) {
989 if ( ! is_array( $config ) ) {
990 continue;
991 }
992
993 foreach ( $sensitive_fields as $field ) {
994 unset( $config[ $field ] );
995 }
996
997 // Let the frontend know whether a key has been configured.
998 $config['has_api_key'] = ! empty( $original_config['providers'][ $provider ]['api_key'] );
999 }
1000 unset( $config );
1001 }
1002
1003 return $ai_settings;
1004 }
1005
1006 /**
1007 * Check if weDocs Pro is active.
1008 *
1009 * @since 2.1.15
1010 *
1011 * @return bool True if Pro is active, false otherwise.
1012 */
1013 function wedocs_is_pro_active() {
1014 return defined( 'WEDOCS_PRO_VERSION' );
1015 }
1016
1017 /**
1018 * Get the meta_query clauses to exclude vendor docs from queries
1019 *
1020 * @since 2.2.7
1021 *
1022 * @return array
1023 */
1024 function wedocs_exclude_vendor_doc_meta_query() {
1025 return array(
1026 'relation' => 'OR',
1027 array(
1028 'key' => '_is_vendor_doc',
1029 'value' => '1',
1030 'compare' => '!=',
1031 ),
1032 array(
1033 'key' => '_is_vendor_doc',
1034 'compare' => 'NOT EXISTS',
1035 ),
1036 );
1037 }
1038
1039 /**
1040 * Get the upgrade popup content for Free to Pro promotion.
1041 *
1042 * This function provides the default content for the upgrade popup and allows
1043 * developers to customize it via filters for campaigns and promotions.
1044 *
1045 * @since 2.1.19
1046 *
1047 * @return array {
1048 * Array of popup content data.
1049 *
1050 * @type string $title Popup title text.
1051 * @type string $subtitle Popup subtitle text.
1052 * @type array $features Array of feature items.
1053 * @type string $button_text Upgrade button text.
1054 * @type string $button_url Upgrade button URL.
1055 * @type array $footer_features Array of footer feature texts.
1056 * }
1057 */
1058 function wedocs_get_upgrade_popup_content() {
1059 $default_content = array(
1060 'title' => __( 'Upgrade to', 'wedocs' ),
1061 'subtitle' => __( 'to experience even more Powerful features 🎉', 'wedocs' ),
1062 'features' => array(
1063 array(
1064 'title' => __( 'Role based permission management ', 'wedocs' ),
1065 'description' => __( 'or viewing, managing, and configuring permission settings.', 'wedocs' ),
1066 ),
1067 array(
1068 'title' => __( 'Arrange content automatically or manually ', 'wedocs' ),
1069 'description' => __( 'giving you full control over its presentation.', 'wedocs' ),
1070 ),
1071 array(
1072 'title' => __( 'Personalize messaging tab with custom titles ', 'wedocs' ),
1073 'description' => __( 'and messages for seamless communication.', 'wedocs' ),
1074 ),
1075 array(
1076 'title' => '',
1077 'description' => sprintf(
1078 /* translators: Full sentence combining: Customize with design widgets, colors, and pre-built options for an appealing interface. */
1079 __( '%1$s %2$s %3$s', 'wedocs' ),
1080 __( 'Customize with', 'wedocs' ),
1081 __( 'design widgets, colors, and pre-built options', 'wedocs' ),
1082 __( 'for an appealing interface.', 'wedocs' )
1083 ),
1084 ),
1085 array(
1086 'title' => __( 'Get assisted by A.I. Powered Chatbot ', 'wedocs' ),
1087 'description' => __( '24/7 with updated information and support.', 'wedocs' ),
1088 ),
1089 ),
1090 'button_text' => __( 'Get weDocs Pro', 'wedocs' ),
1091 'button_url' => 'https://wedocs.co/pricing/?utm_source=wp-admin&utm_medium=freemium&utm_campaign=upgrade-popup',
1092 'footer_features' => array(
1093 __( '10,000+ successful businesses', 'wedocs' ),
1094 __( '14 days no questions asked refund policy', 'wedocs' ),
1095 __( 'Industry leading 24x7 support', 'wedocs' ),
1096 ),
1097 );
1098
1099 /**
1100 * Filters the upgrade popup content.
1101 *
1102 * Use this filter to customize the popup content for campaigns and promotions.
1103 *
1104 * @since 2.1.19
1105 *
1106 * @param array $content {
1107 * Array of popup content data.
1108 *
1109 * @type string $title Popup title text.
1110 * @type string $subtitle Popup subtitle text.
1111 * @type array $features Array of feature items with 'title' and 'description' keys.
1112 * @type string $button_text Upgrade button text.
1113 * @type string $button_url Upgrade button URL.
1114 * @type array $footer_features Array of footer feature texts.
1115 * }
1116 *
1117 * @example
1118 * add_filter( 'wedocs_upgrade_popup_content', function( $content ) {
1119 * // Customize for a campaign
1120 * $content['title'] = 'Special Offer!';
1121 * $content['subtitle'] = 'Get 30% OFF weDocs Pro - Limited Time!';
1122 * $content['button_text'] = 'Claim Your Discount Now';
1123 * $content['button_url'] = 'https://wedocs.co/pricing/?discount=CAMPAIGN30';
1124 *
1125 * // Update features
1126 * $content['features'] = array(
1127 * array(
1128 * 'title' => 'Role-Based Permission Management',
1129 * 'description' => '',
1130 * ),
1131 * array(
1132 * 'title' => 'Docs Duplicator',
1133 * 'description' => '',
1134 * ),
1135 * array(
1136 * 'title' => '7-layer hierarchical article creation',
1137 * 'description' => '',
1138 * ),
1139 * array(
1140 * 'title' => 'Social Sharing Options',
1141 * 'description' => '',
1142 * ),
1143 * array(
1144 * 'title' => 'Floating Contact form',
1145 * 'description' => '',
1146 * ),
1147 * );
1148 *
1149 * return $content;
1150 * } );
1151 */
1152 return apply_filters( 'wedocs_upgrade_popup_content', $default_content );
1153 }
1154
1155 function use_wedocs_legacy_template(){
1156 $general_settings = wedocs_get_option( 'general', 'wedocs_settings', [] );
1157
1158 // If the setting has been explicitly set, use it directly.
1159 if ( isset( $general_settings['use_legacy_template'] ) ) {
1160 return $general_settings['use_legacy_template'] === 'on';
1161 }
1162
1163 // current installed version is lower than 2.1.19 and wp_is_block_theme() is false, then set use_legacy_template to on.
1164 $current_version = get_option( 'wedocs_version', '2.0.0' );
1165 if ( version_compare( $current_version, '2.1.19', '<' ) && ! wp_is_block_theme() ) {
1166
1167 $settings['general']['use_legacy_template'] = 'on';
1168 update_option( 'wedocs_settings', $settings );
1169
1170 return true;
1171 }
1172
1173 if ( wp_is_block_theme() ) {
1174
1175 $settings['general']['use_legacy_template'] = 'off';
1176 update_option( 'wedocs_settings', $settings );
1177
1178 return false;
1179 }
1180
1181
1182
1183 return false;
1184 }
1185
1186 if ( ! function_exists( 'wedocs_rate_limit_ok' ) ) {
1187 /**
1188 * Simple per-IP transient rate limiter.
1189 *
1190 * Allows up to $max hits from a single IP within $window seconds for a
1191 * given bucket. Used to bound abuse of unauthenticated endpoints.
1192 *
1193 * @since 2.3.2
1194 *
1195 * @param string $bucket Logical action name.
1196 * @param int $max Max allowed hits per window.
1197 * @param int $window Window length in seconds.
1198 *
1199 * @return bool True when the request is within the limit.
1200 */
1201 function wedocs_rate_limit_ok( $bucket, $max, $window ) {
1202 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
1203 $key = 'wedocs_rl_' . $bucket . '_' . md5( $ip );
1204 $data = get_transient( $key );
1205
1206 // First request in the window — start a fresh fixed window.
1207 if ( false === $data || ! is_array( $data ) ) {
1208 set_transient( $key, array( 'count' => 1, 'start' => time() ), $window );
1209 return true;
1210 }
1211
1212 $count = (int) ( isset( $data['count'] ) ? $data['count'] : 0 );
1213 if ( $count >= $max ) {
1214 return false;
1215 }
1216
1217 // Preserve the original window expiry instead of sliding the TTL forward.
1218 $start = (int) ( isset( $data['start'] ) ? $data['start'] : time() );
1219 $remaining = max( 1, $window - ( time() - $start ) );
1220 set_transient( $key, array( 'count' => $count + 1, 'start' => $start ), $remaining );
1221
1222 return true;
1223 }
1224 }
1225