PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.16.7
Translate and Go multilingual – Automatic AI translation – wpLingua v2.16.7
2.16.7 2.16.6 2.16.5 2.16.4 2.16.3 2.16.2 2.16.1 2.16.0 2.15.2 2.15.1 2.15.0 2.14.3 2.14.2 2.14.1 2.14.0 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 trunk 1.0.3 1.0.4 1.0.5 1.1.0 All 112 releases
← All changes | inc/util.php +589 -87 1.1.02.16.7 View file →
@@ -6,8 +6,59 @@
6 6 }
7 7
8 8
9 9 /**
10 + * Check if substring is contained in string
11 + *
12 + * @param string $haystack String to check
13 + * @param string $needle Sub-string
14 + *
15 + * @return bool
16 + */
17 +function wplng_str_contains( $haystack, $needle ) {
18 + if ( ! is_string( $haystack ) || ! is_string( $needle ) ) {
19 + return false;
20 + }
21 + return ( strpos( $haystack, $needle ) !== false );
22 +}
23 +
24 +
25 +/**
26 + * Check if string starts by sub_string
27 + *
28 + * @param string $haystack String to check
29 + * @param string $needle Sub-string
30 + *
31 + * @return bool
32 + */
33 +function wplng_str_starts_with( $haystack, $needle ) {
34 +
35 + if ( ! is_string( $haystack ) || ! is_string( $needle ) ) {
36 + return false;
37 + }
38 +
39 + return substr_compare( $haystack, $needle, 0, strlen( $needle ) ) === 0;
40 +}
41 +
42 +
43 +/**
44 + * Check if string ends by sub_string
45 + *
46 + * @param string $haystack String to check
47 + * @param string $needle Sub-string
48 + *
49 + * @return bool
50 + */
51 +function wplng_str_ends_with( $haystack, $needle ) {
52 +
53 + if ( ! is_string( $haystack ) || ! is_string( $needle ) ) {
54 + return false;
55 + }
56 +
57 + return substr_compare( $haystack, $needle, -strlen( $needle ) ) === 0;
58 +}
59 +
60 +/**
10 61 * Return true is $str is an URL
11 62 *
12 63 * @param string $str
13 64 * @return bool
@@ -13,27 +64,33 @@
13 64 * @return bool
14 65 */
15 66 function wplng_str_is_url( $str ) {
16 67
68 + if ( ! is_string( $str )
69 + || trim( $str ) === ''
70 + || ! wplng_str_contains( $str, '/' )
71 + || wplng_str_starts_with( $str, 'GlotPress/' ) // JSON WP translation system
72 + || wplng_str_starts_with( $str, 'wpgb-content-block/' ) // Plugin: WP Grid Builder
73 + || wplng_str_starts_with( $str, '/wc/store/v1' ) // Plugin: WooCommerce
74 + || wplng_str_starts_with( $str, 'contact-form-7/v1' ) // Plugin: Contact Form 7
75 + ) {
76 + return false;
77 + }
78 +
79 + $is_url = false;
17 80 $parsed = wp_parse_url( $str );
18 - $is_url = false;
19 81
20 - if ( is_string( $str )
21 - && ( '' !== trim( $str ) )
22 - && ( false !== strpos( $str, '/' ) )
82 + if ( isset( $parsed['scheme'] )
83 + && (
84 + ( 'https' === $parsed['scheme'] )
85 + || ( 'http' === $parsed['scheme'] )
86 + )
23 87 ) {
24 - if ( isset( $parsed['scheme'] )
25 - && (
26 - ( 'https' === $parsed['scheme'] )
27 - || ( 'http' === $parsed['scheme'] )
28 - )
29 - ) {
30 - // URL has http/https/...
31 - $is_url = ! ( filter_var( $str, FILTER_VALIDATE_URL ) === false );
32 - } else {
33 - // PHP filter_var does not support relative urls, so we simulate a full URL
34 - $is_url = ( filter_var( 'https://website.com/' . ltrim( $str, '/' ), FILTER_VALIDATE_URL ) !== false );
35 - }
88 + // URL has http/https/...
89 + $is_url = ! ( filter_var( $str, FILTER_VALIDATE_URL ) === false );
90 + } else {
91 + // PHP filter_var does not support relative urls, so we simulate a full URL
92 + $is_url = ( filter_var( 'https://website.com/' . ltrim( $str, '/' ), FILTER_VALIDATE_URL ) !== false );
36 93 }
37 94
38 95 return $is_url;
39 96 }
@@ -47,9 +104,45 @@
47 104 * @return bool
48 105 */
49 106 function wplng_text_is_translatable( $text ) {
50 107
51 - // Check if it's a mail address
108 + if ( ! is_string( $text ) ) {
109 + return false;
110 + }
111 +
112 + $text = trim( $text );
113 +
114 + if ( '' === $text ) {
115 + return false;
116 + }
117 +
118 + // Check special no translate tag
119 + if ( wplng_str_contains( $text, '_wplingua_no_translate_' ) ) {
120 + return false;
121 + }
122 +
123 + // Check for better plugin compatibility
124 + if ( wplng_str_contains( $text, 'presto_player' )
125 + || wplng_str_contains( $text, 'presto-player' )
126 + ) {
127 + return false;
128 + }
129 +
130 + // Check bad HTML tags and templating tags
131 + if ( wplng_str_starts_with( $text, '<' )
132 + && wplng_str_ends_with( $text, '>' )
133 + ) {
134 + return false;
135 + }
136 +
137 + // Check JS tags
138 + if ( wplng_str_starts_with( $text, '{{' )
139 + && wplng_str_ends_with( $text, '}}' )
140 + ) {
141 + return false;
142 + }
143 +
144 + // Check if it's a email address
52 145 if ( filter_var( $text, FILTER_VALIDATE_EMAIL ) ) {
53 146 return false;
54 147 }
55 148
@@ -58,25 +151,115 @@
58 151 $letters = html_entity_decode( $letters );
59 152 $letters = preg_replace( '#[^\p{L}\p{N}]#u', '', $letters );
60 153 $letters = preg_replace( '#[\d\s]#u', '', $letters );
61 154
62 - return ! empty( $letters );
155 + if ( empty( $letters ) ) {
156 + return false;
157 + }
158 +
159 + // Check if string is malicious
160 + if ( wplng_str_is_malicious( $text ) ) {
161 + return false;
162 + }
163 +
164 + return true;
63 165 }
64 166
65 167
66 168 /**
169 + * Check if a string contains malicious patterns (SQL injection, XSS, etc.)
170 + *
171 + * This function detects common attack patterns used in:
172 + * - SQL injection (UNION, SELECT, SLEEP, BENCHMARK, etc.)
173 + * - XSS attacks (script tags, javascript: protocol, event handlers)
174 + * - Database enumeration (INFORMATION_SCHEMA, system variables)
175 + * - File operations (LOAD_FILE, INTO OUTFILE)
176 + *
177 + * @param string $text The string to check for malicious patterns.
178 + * @return bool True if the string contains malicious patterns, false otherwise.
179 + */
180 +function wplng_str_is_malicious( $text ) {
181 +
182 + // Check for SQL injection attempts
183 + $sql_patterns = array(
184 + '/[\'\"=\)]\s*--\s*$/i', // Matches: ' --, " --, = --, ) --
185 + '/\bOR\b\s*\d+\s*=\s*\(\s*SELECT\b/i', // OR-based injection: OR 123=(SELECT ...)
186 + '/\bAND\b\s*\d+\s*=\s*\(\s*SELECT\b/i', // AND-based injection: AND 123=(SELECT ...)
187 + '/\bUNION\b\s+(ALL\s+)?SELECT\b/i', // UNION-based injection: UNION SELECT or UNION ALL SELECT
188 + '/PG_SLEEP\s*\(/i', // PostgreSQL time-based injection: PG_SLEEP()
189 + '/\bSLEEP\s*\(\s*\d/i', // MySQL time-based injection: SLEEP(15)
190 + '/BENCHMARK\s*\(/i', // MySQL time-based injection: BENCHMARK()
191 + '/WAITFOR\s+DELAY\s+\'/i', // SQL Server time-based injection: WAITFOR DELAY '...'
192 + '/;\s*DROP\s+TABLE\b/i', // Destructive query: ;DROP TABLE
193 + '/;\s*DELETE\s+FROM\b/i', // Destructive query: ;DELETE FROM
194 + '/;\s*INSERT\s+INTO\b/i', // Stacked query: ;INSERT INTO
195 + '/;\s*UPDATE\s+\w+\s+SET\b/i', // Stacked query: ;UPDATE ... SET
196 + '/\'\s*OR\s*\'[\d\w]+\'\s*=\s*\'[\d\w]+/i', // Classic bypass: ' OR '1'='1
197 + '/\'\s*OR\s+\d+\s*=\s*\d+/i', // Classic bypass: ' OR 1=1
198 + '/\)\s*OR\s+\d+\s*=\s*\(\s*SELECT\b/i', // Subquery injection: ) OR 123=(SELECT ...)
199 + '/^-?\d+\s+OR\s+[\d+\-*\/]+\s*=\s*[\d+\-*\/]+/i', // Boolean injection: -1 OR 2+866-866-1=0+0+0+1
200 + '/\bOR\b\s+[\d+\-*\/]+\s*=\s*[\d+\-*\/]+\s*--/i', // Boolean injection with comment: OR 1+1=2 --
201 + '/\bAND\b\s+[\d+\-*\/]+\s*=\s*[\d+\-*\/]+/i', // Boolean injection: AND 1+1=2
202 + '/LOAD_FILE\s*\(/i', // MySQL file read: LOAD_FILE()
203 + '/INTO\s+(OUT|DUMP)FILE/i', // MySQL file write: INTO OUTFILE / INTO DUMPFILE
204 + '/\bEXEC\s*\(/i', // SQL Server command execution: EXEC()
205 + '/\bXP_\w+\s*\(/i', // SQL Server extended stored procedures: XP_CMDSHELL(), etc.
206 + '/@@[a-zA-Z_]\w*/i', // MySQL system variables extraction
207 + '/CHAR\s*\(\s*\d+\s*(,\s*\d+\s*){2,}\)/i', // String obfuscation: CHAR(65,66,67) with 3+ args
208 + '/0x[0-9a-f]{16,}/i', // Hex-encoded payload: long hexadecimal string (16+ chars)
209 + '/CONCAT\s*\([^)]*SELECT/i', // Obfuscated injection: CONCAT() containing SELECT
210 + '/ORDER\s+BY\s+\d+\s*--/i', // Column enumeration: ORDER BY 1--
211 + '/INFORMATION_SCHEMA\./i', // Database schema enumeration: INFORMATION_SCHEMA.tables, etc.
212 + '/EXTRACTVALUE\s*\(/i', // MySQL XML-based injection: EXTRACTVALUE()
213 + '/UPDATEXML\s*\(/i', // MySQL XML-based injection: UPDATEXML()
214 + );
215 +
216 + foreach ( $sql_patterns as $pattern ) {
217 + if ( preg_match( $pattern, $text ) ) {
218 + return true;
219 + }
220 + }
221 +
222 + // Check for XSS attempts
223 + $xss_patterns = array(
224 + '/<script\b[^>]*>/i', // Script tag injection: <script> or <script src="...">
225 + '/javascript\s*:/i', // JavaScript protocol handler: javascript:alert()
226 + '/\bon(error|load|click|mouseover|focus|blur)\s*=/i', // Event handler injection: onerror=, onclick=, etc.
227 + '/data\s*:\s*text\/html/i', // Data URI XSS: data:text/html,...
228 + '/vbscript\s*:/i', // VBScript protocol handler: vbscript:msgbox()
229 + );
230 +
231 + foreach ( $xss_patterns as $pattern ) {
232 + if ( preg_match( $pattern, $text ) ) {
233 + return true;
234 + }
235 + }
236 +
237 + return false;
238 +}
239 +
240 +
241 +/**
67 242 * Escape texte (used for comparison)
68 243 *
69 - * @param string $text
70 - * @return string
244 + * @param string $text String to escape
245 + * @return string Escape texte for comparison
71 246 */
72 247 function wplng_text_esc( $text ) {
73 248
249 + $text = wp_strip_all_tags( $text );
74 250 $text = html_entity_decode( $text );
75 251 $text = esc_html( $text );
76 252 $text = esc_attr( $text );
253 +
254 + $text = wp_specialchars_decode(
255 + $text,
256 + ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
257 + );
258 +
77 259 $text = str_replace( '\\', '', $text );
78 - $text = preg_replace( '#\s+#', ' ', $text );
260 + $text = preg_replace( '/[\x00-\x1F\x7F]+/u', '', $text );
261 + $text = preg_replace( '/\s+/u', ' ', $text );
79 262 $text = trim( $text );
80 263
81 264 return $text;
82 265 }
@@ -82,27 +265,67 @@
82 265 }
83 266
84 267
85 268 /**
269 + * Escape texte (used for editor)
270 + *
271 + * @param string $text String to escape
272 + * @return string Escape texte for editor
273 + */
274 +function wplng_text_esc_displayed( $text ) {
275 +
276 + $search = array( '<', '&lt;', '>', '&gt;' );
277 + $replace = array( '[', '[', ']', ']' );
278 +
279 + $text = str_replace(
280 + $search,
281 + $replace,
282 + $text
283 + );
284 +
285 + return $text;
286 +}
287 +
288 +
289 +/**
86 290 * Return true if $str is HTML
87 291 *
88 - * @param string $str
89 - * @return string
292 + * @param string $str String to check
293 + * @return bool true if $str is HTML
90 294 */
91 295 function wplng_str_is_html( $str ) {
92 - return $str !== wp_strip_all_tags( $str );
296 + return wplng_str_contains( $str, '<' )
297 + && wplng_str_contains( $str, '>' )
298 + && ( $str !== wp_strip_all_tags( $str ) );
93 299 }
94 300
95 301
96 302 /**
97 - * Return true is $str is a JSON
303 + * Checks whether a string is a valid XML.
98 304 *
99 - * @param string $str
100 - * @return string
305 + * @param string $str The string to validate.
306 + * @return bool Returns true if the string is valid XML, false otherwise.
101 307 */
102 -function wplng_str_is_json( $str ) {
103 - $decoded = json_decode( $str, true );
104 - return ( json_last_error() === JSON_ERROR_NONE ) && is_array( $decoded );
308 +function wplng_str_is_xml( $str ) {
309 + // Return false if the input is empty or not a string.
310 + if ( empty( $str ) || ! is_string( $str ) ) {
311 + return false;
312 + }
313 +
314 + // Suppress XML parsing errors to avoid warnings/notices.
315 + libxml_use_internal_errors( true );
316 +
317 + // Try to load the string as XML.
318 + $xml = simplexml_load_string( $str );
319 +
320 + // Determine if parsing was successful.
321 + $is_valid_xml = ( $xml !== false );
322 +
323 + // Clear any accumulated libxml errors.
324 + libxml_clear_errors();
325 + libxml_use_internal_errors( false );
326 +
327 + return $is_valid_xml;
105 328 }
106 329
107 330
108 331 /**
@@ -108,10 +331,10 @@
108 331 /**
109 332 * Return true if $str is a local ID
110 333 * Ex: fr_FR, fr, FR, ...
111 334 *
112 - * @param string $str
113 - * @return bool
335 + * @param string $str String to check
336 + * @return bool true if $str is a local ID
114 337 */
115 338 function wplng_str_is_locale_id( $str ) {
116 339
117 340 $locale = get_locale();
@@ -128,82 +351,361 @@
128 351 }
129 352
130 353
131 354 /**
132 - * Return true if a JSON string element is translatable
355 + * Return true if $str contains sub-strings present in the i18n script
133 356 *
134 - * @param string $element
135 - * @param array $parents
136 - * @return bool
357 + * @param string $str String to check
358 + * @return bool String is a i18n script
137 359 */
138 -function wplng_json_element_is_translatable( $element, $parents ) {
360 +function wplng_str_is_script_i18n( $str ) {
139 361
140 - $is_translatable = false;
141 - $json_excluded = wplng_data_excluded_json();
142 - $json_to_translate = wplng_data_json_to_translate();
362 + $str = trim( $str );
143 363
144 - if ( in_array( $parents, $json_excluded ) ) {
364 + if ( empty( $str ) ) {
365 + return false;
366 + }
145 367
146 - /**
147 - * Is an excluded JSON
148 - */
368 + return (
369 + wplng_str_contains( $str, 'wp.i18n.setLocaleData' )
370 + && wplng_str_contains( $str, 'translations.locale_data' )
371 + && ( preg_match( '#function\s*\(\s*domain\s*,\s*translations\s*\)\s*\{#i', $str ) === 1 )
372 + && wplng_str_contains( $str, ');' )
373 + );
374 +}
149 375
150 - $is_translatable = false;
151 376
152 - } elseif ( in_array( $parents, $json_to_translate ) ) {
377 +/**
378 + * Return true is $str is a JSON
379 + *
380 + * @param string $str String to check
381 + * @return bool true is $str is a JSON
382 + */
383 +function wplng_str_is_json( $str ) {
384 + if ( ! is_string( $str ) ) {
385 + return false;
386 + }
387 + $decoded = json_decode( $str, true );
388 + return ( json_last_error() === JSON_ERROR_NONE ) && is_array( $decoded );
389 +}
153 390
154 - /**
155 - * Is an included JSON
156 - */
157 391
158 - $is_translatable = true;
392 +/**
393 + * Unescape a JavaScript string
394 + *
395 + * Converts escaped characters in a JS string back to their original form.
396 + * Handles newlines, carriage returns, tabs, quotes, backslashes,
397 + * Unicode sequences (\uXXXX), and hexadecimal sequences (\xXX).
398 + *
399 + * @param string $str The escaped JavaScript string.
400 + * @return string The unescaped string.
401 + */
402 +function wplng_unescape_js_string( $str ) {
159 403
160 - } else {
404 + if ( ! is_string( $str ) ) {
405 + return '';
406 + }
161 407
162 - if (
163 - ! empty( $parents[0] )
164 - && ( '@graph' === $parents[0] )
165 - && ( count( $parents ) > 2 )
166 - && (
167 - (
168 - ( 'logo' === $parents[ count( $parents ) - 2 ] )
169 - && ( 'caption' === $parents[ count( $parents ) - 1 ] )
170 - )
171 - || ( 'name' === $parents[ count( $parents ) - 1 ] )
172 - )
173 - ) {
408 + // Simple escape sequences
409 + $replacements = array(
410 + '\\n' => "\n",
411 + '\\r' => "\r",
412 + '\\t' => "\t",
413 + '\\v' => "\v",
414 + '\\f' => "\f",
415 + '\\b' => "\x08",
416 + '\\"' => '"',
417 + "\\'" => "'",
418 + '\\/' => '/',
419 + '\\0' => "\0",
420 + '\\\\' => '\\',
421 + );
174 422
175 - /**
176 - * Is schema-graph
177 - */
423 + $str = str_replace( array_keys( $replacements ), array_values( $replacements ), $str );
178 424
179 - $is_translatable = true;
425 + // Unicode sequences: \uXXXX
426 + $str = preg_replace_callback(
427 + '/\\\\u([0-9a-fA-F]{4})/',
428 + function ( $matches ) {
429 + return mb_convert_encoding( pack( 'H*', $matches[1] ), 'UTF-8', 'UTF-16BE' );
430 + },
431 + $str
432 + );
180 433
181 - } elseif (
182 - ! empty( $parents[0] )
183 - && ( 'wc_address_i18n_params' === $parents[0] )
184 - && ( count( $parents ) > 1 )
185 - && (
186 - ( 'placeholder' === $parents[ count( $parents ) - 1 ] )
187 - || ( 'label' === $parents[ count( $parents ) - 1 ] )
188 - )
189 - ) {
434 + // Hexadecimal sequences: \xXX
435 + $str = preg_replace_callback(
436 + '/\\\\x([0-9a-fA-F]{2})/',
437 + function ( $matches ) {
438 + return chr( hexdec( $matches[1] ) );
439 + },
440 + $str
441 + );
190 442
191 - /**
192 - * Is WooCommerce address params
193 - */
194 -
195 - $is_translatable = true;
443 + return $str;
444 +}
445 +
446 +
447 +/**
448 + * Checks if a JSON element should be excluded based on defined exclusion rules.
449 + *
450 + * @param mixed $element The JSON element to check.
451 + * @param array $parents The parent elements of the JSON element.
452 + *
453 + * @return bool True if the element matches any exclusion rule, false otherwise.
454 + */
455 +function wplng_json_element_is_excluded( $element, $parents ) {
456 +
457 + $rules = wplng_data_json_rules_exclusion();
458 +
459 + foreach ( $rules as $rule ) {
460 + if ( $rule( $element, $parents ) === true ) {
461 + return true;
196 462 }
463 + }
197 464
198 - if ( ! wplng_text_is_translatable( $element ) ) {
199 - $is_translatable = false;
465 + return false;
466 +}
467 +
468 +
469 +/**
470 + * Checks if a JSON element should be included based on defined inclusion rules.
471 + *
472 + * @param mixed $element The JSON element to check.
473 + * @param array $parents The parent elements of the JSON element.
474 + *
475 + * @return bool True if the element matches any inclusion rule, false otherwise.
476 + */
477 +function wplng_json_element_is_included( $element, $parents ) {
478 +
479 + $rules = wplng_data_json_rules_inclusion();
480 +
481 + foreach ( $rules as $rule ) {
482 + if ( $rule( $element, $parents ) === true ) {
483 + return true;
200 484 }
201 485 }
202 486
487 + return false;
488 +}
489 +
490 +
491 +/**
492 + * Get the context
493 + *
494 + * @return string Context
495 + */
496 +function wplng_get_context() {
497 +
498 + $context = 'UNKNOW';
499 +
500 + if ( defined( 'DOING_AJAX' )
501 + && DOING_AJAX
502 + && ! empty( $_SERVER['HTTP_REFERER'] )
503 + ) {
504 + $context = $_SERVER['HTTP_REFERER'];
505 + $context = sanitize_url( $context );
506 + } elseif ( isset( $_SERVER['HTTPS'] )
507 + && isset( $_SERVER['HTTP_HOST'] )
508 + && isset( $_SERVER['REQUEST_URI'] )
509 + ) {
510 + $context = ( empty( $_SERVER['HTTPS'] ) ? 'http' : 'https' );
511 + $context .= '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
512 + $context = sanitize_url( $context );
513 + }
514 +
203 515 return apply_filters(
204 - 'wplng_json_element_is_translatable',
205 - $is_translatable,
206 - $element,
207 - $parents
516 + 'wplng_api_call_translate_context',
517 + $context
208 518 );
519 +}
520 +
521 +
522 +/**
523 + * Return true is website is in sub folder
524 + *
525 + * @return bool
526 + */
527 +function wplng_website_in_sub_folder() {
528 + $parsed = wp_parse_url( get_home_url() );
529 + return ! empty( $parsed['path'] );
530 +}
531 +
532 +
533 +/**
534 + * Get WordPress home base path.
535 + *
536 + * Examples:
537 + * - Root install => ''
538 + * - Subdirectory install => '/blog'
539 + *
540 + * @return string
541 + */
542 +function wplng_get_home_base_path() {
543 +
544 + static $base_path = null;
545 +
546 + if ( null !== $base_path ) {
547 + return $base_path;
548 + }
549 +
550 + $parsed_url = wp_parse_url( home_url() );
551 + $base_path = '';
552 +
553 + if ( isset( $parsed_url['path'] )
554 + && is_string( $parsed_url['path'] )
555 + ) {
556 + $base_path = untrailingslashit( $parsed_url['path'] );
557 + }
558 +
559 + return $base_path;
560 +}
561 +
562 +
563 +/**
564 + * Normalize a relative request path by stripping WordPress home base path.
565 + *
566 + * @param string $path
567 + * @return string
568 + */
569 +function wplng_normalize_request_path( $path ) {
570 +
571 + if ( ! is_string( $path ) || '' === $path ) {
572 + return '/';
573 + }
574 +
575 + $parsed_url = wp_parse_url( $path );
576 +
577 + if ( is_array( $parsed_url )
578 + && isset( $parsed_url['host'] )
579 + ) {
580 + $path = '/';
581 +
582 + if ( isset( $parsed_url['path'] ) && is_string( $parsed_url['path'] ) ) {
583 + $path = $parsed_url['path'];
584 + }
585 +
586 + if ( isset( $parsed_url['query'] ) && is_string( $parsed_url['query'] ) && '' !== $parsed_url['query'] ) {
587 + $path .= '?' . $parsed_url['query'];
588 + }
589 + }
590 +
591 + if ( '' === $path ) {
592 + $path = '/';
593 + } elseif ( '?' === substr( $path, 0, 1 ) ) {
594 + $path = '/' . $path;
595 + } elseif ( ! wplng_str_starts_with( $path, '/' ) ) {
596 + $path = '/' . $path;
597 + }
598 +
599 + $base_path = wplng_get_home_base_path();
600 +
601 + if ( '' === $base_path ) {
602 + return $path;
603 + }
604 +
605 + if ( $path === $base_path ) {
606 + return '/';
607 + }
608 +
609 + if ( wplng_str_starts_with( $path, $base_path . '/' )
610 + || wplng_str_starts_with( $path, $base_path . '?' )
611 + ) {
612 + $path = substr( $path, strlen( $base_path ) );
613 +
614 + if ( '' === $path ) {
615 + $path = '/';
616 + } elseif ( '?' === substr( $path, 0, 1 ) ) {
617 + $path = '/' . $path;
618 + }
619 + }
620 +
621 + return $path;
622 +}
623 +
624 +
625 +/**
626 + * Build request URI for WordPress core from an internal normalized path.
627 + *
628 + * @param string $path
629 + * @return string
630 + */
631 +function wplng_request_path_for_wp( $path ) {
632 +
633 + if ( ! is_string( $path ) || '' === $path ) {
634 + $path = '/';
635 + }
636 +
637 + $base_path = wplng_get_home_base_path();
638 +
639 + if ( '' === $base_path ) {
640 + return $path;
641 + }
642 +
643 + if ( $path === $base_path
644 + || wplng_str_starts_with( $path, $base_path . '/' )
645 + || wplng_str_starts_with( $path, $base_path . '?' )
646 + ) {
647 + return $path;
648 + }
649 +
650 + if ( '/' === $path ) {
651 + return trailingslashit( $base_path );
652 + }
653 +
654 + if ( '?' === substr( $path, 0, 1 ) ) {
655 + $path = '/' . $path;
656 + }
657 +
658 + if ( wplng_str_starts_with( $path, '/' ) ) {
659 + return $base_path . $path;
660 + }
661 +
662 + return trailingslashit( $base_path ) . $path;
663 +}
664 +
665 +
666 +/**
667 + * Counts the number of published posts for all public post types.
668 + *
669 + * This function retrieves all registered public post types, including custom post types,
670 + * and counts the total number of published posts for those post types.
671 + * It explicitly includes 'post', 'page', and 'product' post types to ensure they are counted.
672 + *
673 + * @global wpdb $wpdb WordPress database abstraction object.
674 + *
675 + * @return int The total count of published posts for the specified post types.
676 + */
677 +function wplng_count_public_content() {
678 + global $wpdb;
679 +
680 + // Retrieve all public post types
681 + $post_types = get_post_types( array( 'public' => true ), 'names' );
682 +
683 + // Explicitly add specific post types to ensure they are included
684 + $post_types = array_merge( $post_types, array( 'post', 'page', 'product' ) );
685 +
686 + // Remove duplicate post types
687 + $post_types = array_unique( $post_types );
688 +
689 + // Check if any post types were found
690 + if ( empty( $post_types ) ) {
691 + return 0; // Return 0 if no post types are found
692 + }
693 +
694 + // Prepare placeholders for the SQL query
695 + $placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
696 +
697 + // Build the SQL query to count published posts for the specified post types
698 + $sql = 'SELECT COUNT(*)' . PHP_EOL;
699 + $sql .= "FROM {$wpdb->posts}" . PHP_EOL;
700 + $sql .= "WHERE post_status = 'publish'" . PHP_EOL;
701 + $sql .= "AND post_type IN ($placeholders)" . PHP_EOL;
702 +
703 + // Secure the SQL query using $wpdb->prepare()
704 + $query = $wpdb->prepare( $sql, $post_types );
705 +
706 + // Execute the query and retrieve the result
707 + $count_posts = $wpdb->get_var( $query );
708 +
709 + // Return the total count as an integer
710 + return intval( $count_posts );
209 711 }