PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.12.3
Translate and Go multilingual – Automatic AI translation – wpLingua v2.12.3
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
wplingua / inc / util.php

util.php in Translate and Go multilingual – Automatic AI translation – wpLingua 2.12.3, at inc/util.php

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