PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / Templates / Validator.php

Validator.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/Templates/Validator.php

329 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template Validator Class.
4 *
5 * Validates template code for security and syntax.
6 *
7 * @author Paul Kilmurray <paul@kilbot.com>
8 *
9 * @see http://wcpos.com
10 * @package WCPOS\WooCommercePOS
11 */
12
13 namespace WCPOS\WooCommercePOS\Templates;
14
15 use WP_Error;
16
17 /**
18 * Validator class.
19 */
20 class Validator {
21 /**
22 * List of dangerous PHP functions that should be flagged.
23 *
24 * @var array
25 */
26 private static $dangerous_functions = array(
27 'eval',
28 'exec',
29 'system',
30 'shell_exec',
31 'passthru',
32 'proc_open',
33 'popen',
34 'curl_exec',
35 'curl_multi_exec',
36 'parse_ini_file',
37 'show_source',
38 'file_put_contents',
39 'fopen',
40 'fwrite',
41 'unlink',
42 'rmdir',
43 'mkdir',
44 'chmod',
45 'chown',
46 'chgrp',
47 'touch',
48 'symlink',
49 'link',
50 'tempnam',
51 'tmpfile',
52 'move_uploaded_file',
53 'phpinfo',
54 'assert',
55 'create_function',
56 'call_user_func',
57 'call_user_func_array',
58 );
59
60 /**
61 * Validate template code.
62 *
63 * @param string $content Template content.
64 * @param string $language Template language (php, javascript).
65 *
66 * @return true|WP_Error True on success, WP_Error on failure.
67 */
68 public static function validate( string $content, string $language ) {
69 if ( 'php' === $language ) {
70 return self::validate_php( $content );
71 }
72
73 if ( 'javascript' === $language ) {
74 return self::validate_javascript( $content );
75 }
76
77 return new WP_Error(
78 'invalid_language',
79 /* translators: Validation error shown when an unsupported template language is provided. */
80 __( 'Invalid template language.', 'woocommerce-pos' )
81 );
82 }
83
84 /**
85 * Sanitize template content.
86 *
87 * @param string $content Template content.
88 * @param string $language Template language.
89 *
90 * @return string Sanitized content.
91 */
92 public static function sanitize( string $content, string $language ): string {
93 if ( 'php' === $language ) {
94 // For PHP, we don't want to sanitize too much as it would break the code
95 // Just ensure proper slashing for storage.
96 return $content;
97 }
98
99 if ( 'javascript' === $language ) {
100 // For JavaScript, similarly we want to preserve the code structure.
101 return $content;
102 }
103
104 return $content;
105 }
106
107 /**
108 * Check if user is allowed to edit templates.
109 *
110 * @return bool True if user can edit templates, false otherwise.
111 */
112 public static function can_edit_templates(): bool {
113 return current_user_can( 'manage_woocommerce_pos' );
114 }
115
116 /**
117 * Log template validation attempt.
118 *
119 * @param int $template_id Template ID.
120 * @param string $content Template content.
121 * @param mixed $result Validation result.
122 *
123 * @return void
124 */
125 public static function log_validation( int $template_id, string $content, $result ): void {
126 if ( ! \defined( 'WCPOS_TEMPLATE_VALIDATION_LOG' ) || ! WCPOS_TEMPLATE_VALIDATION_LOG ) {
127 return;
128 }
129
130 $user = wp_get_current_user();
131 $log_entry = array(
132 'timestamp' => current_time( 'mysql' ),
133 'user_id' => $user->ID,
134 'user_login' => $user->user_login,
135 'template_id' => $template_id,
136 'result' => is_wp_error( $result ) ? $result->get_error_message() : 'success',
137 'content_hash' => md5( $content ),
138 );
139
140 // Store in option (you might want to use a custom table for better performance).
141 $logs = get_option( 'wcpos_template_validation_logs', array() );
142 $logs[] = $log_entry;
143
144 // Keep only last 100 entries.
145 if ( \count( $logs ) > 100 ) {
146 $logs = \array_slice( $logs, -100 );
147 }
148
149 update_option( 'wcpos_template_validation_logs', $logs );
150 }
151
152 /**
153 * Validate PHP template code.
154 *
155 * @param string $content PHP template content.
156 *
157 * @return true|WP_Error True on success, WP_Error on failure.
158 */
159 private static function validate_php( string $content ) {
160 // Check for syntax errors.
161 $syntax_check = self::check_php_syntax( $content );
162 if ( is_wp_error( $syntax_check ) ) {
163 return $syntax_check;
164 }
165
166 // Check for dangerous functions.
167 $dangerous_check = self::check_dangerous_functions( $content );
168 if ( is_wp_error( $dangerous_check ) ) {
169 return $dangerous_check;
170 }
171
172 /*
173 * Filters the PHP template validation result.
174 *
175 * @param true|WP_Error $result Validation result.
176 * @param string $content Template content.
177 *
178 * @since 1.8.0
179 *
180 * @hook woocommerce_pos_validate_php_template
181 */
182 return apply_filters( 'woocommerce_pos_validate_php_template', true, $content );
183 }
184
185 /**
186 * Validate JavaScript template code.
187 *
188 * @param string $content JavaScript template content.
189 *
190 * @return true|WP_Error True on success, WP_Error on failure.
191 */
192 private static function validate_javascript( string $content ) {
193 // Basic validation for JavaScript
194 // Check for obviously dangerous patterns.
195 $dangerous_patterns = array(
196 '/eval\s*\(/i',
197 '/Function\s*\(/i',
198 '/setTimeout\s*\(\s*["\']/',
199 '/setInterval\s*\(\s*["\']/',
200 );
201
202 foreach ( $dangerous_patterns as $pattern ) {
203 if ( preg_match( $pattern, $content ) ) {
204 return new WP_Error(
205 'dangerous_javascript',
206 \sprintf(
207 // translators: %s: pattern that was found.
208 __( 'Template contains potentially dangerous JavaScript code: %s', 'woocommerce-pos' ),
209 $pattern
210 )
211 );
212 }
213 }
214
215 /*
216 * Filters the JavaScript template validation result.
217 *
218 * @param true|WP_Error $result Validation result.
219 * @param string $content Template content.
220 *
221 * @since 1.8.0
222 *
223 * @hook woocommerce_pos_validate_javascript_template
224 */
225 return apply_filters( 'woocommerce_pos_validate_javascript_template', true, $content );
226 }
227
228 /**
229 * Check PHP syntax.
230 *
231 * @param string $content PHP code to check.
232 *
233 * @return true|WP_Error True if syntax is valid, WP_Error otherwise.
234 */
235 private static function check_php_syntax( string $content ) {
236 // Use php -l to check syntax if available.
237 if ( \function_exists( 'exec' ) && ! \defined( 'DISABLE_TEMPLATE_SYNTAX_CHECK' ) ) {
238 $temp_file = tempnam( sys_get_temp_dir(), 'wcpos_template_' );
239 file_put_contents( $temp_file, $content );
240
241 $output = array();
242 $return_var = 0;
243 exec( 'php -l ' . escapeshellarg( $temp_file ) . ' 2>&1', $output, $return_var );
244
245 unlink( $temp_file );
246
247 if ( 0 !== $return_var ) {
248 return new WP_Error(
249 'php_syntax_error',
250 \sprintf(
251 // translators: %s: error message.
252 __( 'PHP syntax error: %s', 'woocommerce-pos' ),
253 implode( "\n", $output )
254 )
255 );
256 }
257 }
258
259 // Fallback: Basic check for unclosed PHP tags.
260 $open_tags = substr_count( $content, '<?php' ) + substr_count( $content, '<?' );
261 $close_tags = substr_count( $content, '?>' );
262
263 // Note: It's valid to have more open tags than close tags (files can end without closing tag)
264 // But if we have more close tags, that's definitely an error.
265 if ( $close_tags > $open_tags ) {
266 return new WP_Error(
267 'php_syntax_error',
268 __( 'PHP syntax error: Mismatched PHP tags.', 'woocommerce-pos' )
269 );
270 }
271
272 return true;
273 }
274
275 /**
276 * Check for dangerous PHP functions.
277 *
278 * @param string $content PHP code to check.
279 *
280 * @return true|WP_Error True if no dangerous functions found, WP_Error otherwise.
281 */
282 private static function check_dangerous_functions( string $content ) {
283 /**
284 * Filters the list of dangerous PHP functions.
285 *
286 * @param array $functions List of dangerous function names.
287 *
288 * @since 1.8.0
289 *
290 * @hook woocommerce_pos_template_dangerous_functions
291 */
292 $dangerous = apply_filters( 'woocommerce_pos_template_dangerous_functions', self::$dangerous_functions );
293
294 // Remove comments from content to avoid false positives.
295 $content_without_comments = preg_replace( '/\/\*[\s\S]*?\*\/|\/\/.*$/m', '', $content );
296
297 foreach ( $dangerous as $function ) {
298 // Check for function calls.
299 if ( preg_match( '/\b' . preg_quote( $function, '/' ) . '\s*\(/i', $content_without_comments ) ) {
300 /**
301 * Filters whether to allow dangerous functions in templates.
302 *
303 * @param bool $allow Whether to allow the dangerous function.
304 * @param string $function Function name.
305 * @param string $content Template content.
306 *
307 * @since 1.8.0
308 *
309 * @hook woocommerce_pos_template_allow_dangerous_function
310 */
311 $allow = apply_filters( 'woocommerce_pos_template_allow_dangerous_function', false, $function, $content );
312
313 if ( ! $allow ) {
314 return new WP_Error(
315 'dangerous_function',
316 \sprintf(
317 // translators: %s: function name.
318 __( 'Template contains dangerous function: %s(). This function is not allowed for security reasons.', 'woocommerce-pos' ),
319 $function
320 )
321 );
322 }
323 }
324 }
325
326 return true;
327 }
328 }
329