PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / trunk
Translate and Go multilingual – Automatic AI translation – wpLingua vtrunk
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 1.1.1 All 111 releases
wplingua / inc / util.php

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

712 lines 18.9 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 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 /**
61 * Return true is $str is an URL
62 *
63 * @param string $str
64 * @return bool
65 */
66 function wplng_str_is_url( $str ) {
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;
80 $parsed = wp_parse_url( $str );
81
82 if ( isset( $parsed['scheme'] )
83 && (
84 ( 'https' === $parsed['scheme'] )
85 || ( 'http' === $parsed['scheme'] )
86 )
87 ) {
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 );
93 }
94
95 return $is_url;
96 }
97
98
99 /**
100 * Return true is $str is a translatable text
101 * Return false if $str is a number, mail addredd, symbol, ...
102 *
103 * @param string $text
104 * @return bool
105 */
106 function wplng_text_is_translatable( $text ) {
107
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
145 if ( filter_var( $text, FILTER_VALIDATE_EMAIL ) ) {
146 return false;
147 }
148
149 // Get letters only
150 $letters = $text;
151 $letters = html_entity_decode( $letters );
152 $letters = preg_replace( '#[^\p{L}\p{N}]#u', '', $letters );
153 $letters = preg_replace( '#[\d\s]#u', '', $letters );
154
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;
165 }
166
167
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 /**
242 * Escape texte (used for comparison)
243 *
244 * @param string $text String to escape
245 * @return string Escape texte for comparison
246 */
247 function wplng_text_esc( $text ) {
248
249 $text = wp_strip_all_tags( $text );
250 $text = html_entity_decode( $text );
251 $text = esc_html( $text );
252 $text = esc_attr( $text );
253
254 $text = wp_specialchars_decode(
255 $text,
256 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
257 );
258
259 $text = str_replace( '\\', '', $text );
260 $text = preg_replace( '/[\x00-\x1F\x7F]+/u', '', $text );
261 $text = preg_replace( '/\s+/u', ' ', $text );
262 $text = trim( $text );
263
264 return $text;
265 }
266
267
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 /**
290 * Return true if $str is HTML
291 *
292 * @param string $str String to check
293 * @return bool true if $str is HTML
294 */
295 function wplng_str_is_html( $str ) {
296 return wplng_str_contains( $str, '<' )
297 && wplng_str_contains( $str, '>' )
298 && ( $str !== wp_strip_all_tags( $str ) );
299 }
300
301
302 /**
303 * Checks whether a string is a valid XML.
304 *
305 * @param string $str The string to validate.
306 * @return bool Returns true if the string is valid XML, false otherwise.
307 */
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;
328 }
329
330
331 /**
332 * Return true if $str is a local ID
333 * Ex: fr_FR, fr, FR, ...
334 *
335 * @param string $str String to check
336 * @return bool true if $str is a local ID
337 */
338 function wplng_str_is_locale_id( $str ) {
339
340 $locale = get_locale();
341 $locales = array(
342 $locale, // Ex: fr_FR
343 strtolower( $locale ), // Ex: fr_fr
344 str_replace( '_', '-', $locale ), // Ex: fr-FR
345 strtolower( str_replace( '_', '-', $locale ) ), // Ex: fr-fr
346 substr( $locale, 0, 2 ), // Ex: FR
347 strtolower( substr( $locale, 0, 2 ) ), // Ex: fr
348 );
349
350 return in_array( $str, $locales );
351 }
352
353
354 /**
355 * Return true if $str contains sub-strings present in the i18n script
356 *
357 * @param string $str String to check
358 * @return bool String is a i18n script
359 */
360 function wplng_str_is_script_i18n( $str ) {
361
362 $str = trim( $str );
363
364 if ( empty( $str ) ) {
365 return false;
366 }
367
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 }
375
376
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 }
390
391
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 ) {
403
404 if ( ! is_string( $str ) ) {
405 return '';
406 }
407
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 );
422
423 $str = str_replace( array_keys( $replacements ), array_values( $replacements ), $str );
424
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 );
433
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 );
442
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;
462 }
463 }
464
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;
484 }
485 }
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
515 return apply_filters(
516 'wplng_api_call_translate_context',
517 $context
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 );
711 }
712