PluginProbe
MetalpriceAPI / trunk
MetalpriceAPI vtrunk
1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 trunk 1.0.0 1.0.1 1.0.2
metalpriceapi / index.php

index.php in MetalpriceAPI trunk, at index.php

598 lines 19.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Plugin Name: MetalpriceAPI
4 * Author: metalpriceapi.com
5 * Version: 1.1.8
6 * Description: Official <a href="https://metalpriceapi.com/">metalpriceapi.com</a> plugin.
7 * Author URL: https://metalpriceapi.com
8 * Requires at least: 5.0
9 * Requires PHP: 7.4
10 * License: GPLv2 or later
11 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
12 */
13
14 if (!defined('ABSPATH')) exit; // Exit if accessed directly
15
16 class MetalpriceAPI {
17
18 private $opt_api_key = 'mpa_api_key';
19 private $opt_api_status = 'mpa_api_status';
20
21 private $opt_data_success = 'mpa_data_success';
22 private $opt_data_none = 'mpa_data_none';
23 private $opt_data_error = 'mpa_data_error';
24
25 private $opt_data_carat_success = 'mpa_data_carat_success';
26
27 private function safe_rate($rates, $key) {
28 if (!is_array($rates) || !isset($rates[$key]) || !is_numeric($rates[$key])) {
29 return null;
30 }
31 $rate = (float) $rates[$key];
32 return $rate != 0.0 ? $rate : null;
33 }
34
35 private function format_date($timestamp, $timezone, $format) {
36 if (!is_numeric($timestamp)) {
37 return '';
38 }
39 $datetime = new DateTime();
40 $datetime->setTimestamp((int) $timestamp);
41 try {
42 $datetime->setTimezone(new DateTimeZone($timezone));
43 } catch (Exception $e) {
44 $datetime->setTimezone(new DateTimeZone('UTC'));
45 }
46 return $datetime->format($format);
47 }
48
49 private function response_base($json, $fallback) {
50 return isset($json['base']) && is_string($json['base'])
51 ? $json['base']
52 : $fallback;
53 }
54
55 private function replace_date_placeholders($value, $timestamp, $timezone, $format) {
56 if (!is_numeric($timestamp)) {
57 return str_replace(array('{{timestamp}}', '{{date}}'), '', $value);
58 }
59
60 $value = str_replace('{{timestamp}}', esc_html($timestamp), $value);
61 $date = $this->format_date($timestamp, $timezone, $format);
62 return str_replace('{{date}}', esc_html($date), $value);
63 }
64
65 private function error_output($reason) {
66 $template = get_option($this->opt_data_error, 'Error: {{error}}');
67 return str_replace('{{error}}', esc_html($reason), $template);
68 }
69
70 private function error_message($json) {
71 $status_code = isset($json['error']['statusCode']) ? $json['error']['statusCode'] : '';
72 $message = isset($json['error']['message']) ? $json['error']['message'] : '';
73 return array($status_code, $message);
74 }
75
76 // Safe mathematical expression parser
77 private function safe_math_eval($expression, $value) {
78 // Remove any whitespace
79 $expression = preg_replace('/\s+/', '', $expression);
80
81 // Only allow basic math operations, numbers, and the value keyword.
82 // A character class would let stray v/a/l/u/e letters through as tokens.
83 if (!preg_match('/^(?:\(value\)|[0-9]+(?:\.[0-9]+)?|[\+\-\*\/\(\)])+$/', $expression)) {
84 return $value;
85 }
86
87 // %F avoids scientific notation, which would break tokenising below
88 $expression = str_replace('(value)', sprintf('%.10F', $value), $expression);
89
90 // Use a safe evaluation method
91 try {
92 // Split the expression into tokens
93 $tokens = preg_split('/([\+\-\*\/\(\)])/', $expression, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
94
95 // Convert to Reverse Polish Notation (RPN)
96 $output = array();
97 $operators = array();
98
99 foreach ($tokens as $token) {
100 if (is_numeric($token)) {
101 $output[] = $token;
102 } elseif ($token === '(') {
103 $operators[] = $token;
104 } elseif ($token === ')') {
105 while (count($operators) > 0 && end($operators) !== '(') {
106 $output[] = array_pop($operators);
107 }
108 if (count($operators) > 0 && end($operators) === '(') {
109 array_pop($operators);
110 }
111 } else {
112 $precedence = array(
113 '+' => 1,
114 '-' => 1,
115 '*' => 2,
116 '/' => 2
117 );
118
119 $token_prec = isset($precedence[$token]) ? $precedence[$token] : 0;
120 while (count($operators) > 0 &&
121 end($operators) !== '(' &&
122 $token_prec <= $precedence[end($operators)]) {
123 $output[] = array_pop($operators);
124 }
125 $operators[] = $token;
126 }
127 }
128
129 while (count($operators) > 0) {
130 $output[] = array_pop($operators);
131 }
132
133 // Evaluate RPN
134 $stack = array();
135 foreach ($output as $token) {
136 if (is_numeric($token)) {
137 array_push($stack, $token);
138 } else {
139 $b = array_pop($stack);
140 $a = array_pop($stack);
141
142 switch ($token) {
143 case '+':
144 array_push($stack, $a + $b);
145 break;
146 case '-':
147 array_push($stack, $a - $b);
148 break;
149 case '*':
150 array_push($stack, $a * $b);
151 break;
152 case '/':
153 if ($b == 0) {
154 return $value; // Division by zero
155 }
156 array_push($stack, $a / $b);
157 break;
158 }
159 }
160 }
161
162 $result = array_pop($stack);
163 return is_numeric($result) ? $result : $value;
164 } catch (Exception $e) {
165 return $value;
166 }
167 }
168
169 function __construct() {
170 add_shortcode('metalpriceapi',[$this, 'shortcode']);
171 add_shortcode('metalpriceapi_change',[$this, 'shortcode_change']);
172 add_shortcode('metalpriceapi_carat',[$this, 'shortcode_carat']);
173 add_action('admin_menu', [$this, 'action']);
174 }
175
176 // helper
177
178 function saveOption($name) {
179 if (isset($_POST[$name])) {
180 // Add nonce verification
181 if (!isset($_POST['mpa_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mpa_nonce'])), -1)) {
182 wp_die('Security check failed');
183 }
184
185 // Check user capabilities
186 if (!current_user_can('manage_options')) {
187 wp_die('Unauthorized access');
188 }
189
190 update_option($name, sanitize_text_field(wp_unslash($_POST[$name])));
191 }
192 }
193
194 function saveHTMLOption($name) {
195 if(isset($_POST[$name])){
196 // Add nonce verification
197 if (!isset($_POST['mpa_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mpa_nonce'])), -1)) {
198 wp_die('Security check failed');
199 }
200
201 // Check user capabilities
202 if (!current_user_can('manage_options')) {
203 wp_die('Unauthorized access');
204 }
205
206 update_option($name, wp_kses_post(wp_unslash($_POST[$name])));
207 }
208 }
209
210 // hooks
211
212 function shortcode($attrs) {
213 $attrs = shortcode_atts(array(
214 'base' => 'USD',
215 'symbol' => 'XAU',
216 'date_format' => 'Y-m-d',
217 'date_timezone' => 'UTC',
218 'date' => null,
219 'days' => null,
220 'price_round' => 2,
221 'unit' => null,
222 'purity' => null,
223 'operation' => null,
224 ), $attrs);
225
226 // Sanitize shortcode attributes
227 $attrs['base'] = sanitize_text_field($attrs['base']);
228 $attrs['symbol'] = sanitize_text_field($attrs['symbol']);
229 $attrs['date_format'] = sanitize_text_field($attrs['date_format']);
230 $attrs['date_timezone'] = sanitize_text_field($attrs['date_timezone']);
231 $attrs['date'] = $attrs['date'] !== null ? sanitize_text_field($attrs['date']) : null;
232 $attrs['days'] = is_numeric($attrs['days']) ? intval($attrs['days']) : null;
233 $attrs['price_round'] = is_numeric($attrs['price_round']) ? intval($attrs['price_round']) : 2;
234 $attrs['unit'] = $attrs['unit'] !== null ? sanitize_text_field($attrs['unit']) : null;
235 $attrs['purity'] = $attrs['purity'] !== null ? sanitize_text_field($attrs['purity']) : null;
236 $attrs['operation'] = $attrs['operation'] !== null ? sanitize_text_field($attrs['operation']) : null;
237
238 $endpoint = 'latest';
239 if (isset($attrs['date']) && !empty(trim($attrs['date']))) {
240 $endpoint = gmdate('Y-m-d', strtotime($attrs['date']));
241 } else if (isset($attrs['days']) && !empty(trim($attrs['days'])) && is_numeric($attrs['days']) && $attrs['days'] > 0) {
242 $endpoint = gmdate('Y-m-d', strtotime("-{$attrs['days']} days", strtotime('now')));
243 }
244
245 $api_key = get_option($this->opt_api_key);
246
247 $uri = "https://api.metalpriceapi.com/v1/$endpoint?api_key=$api_key&base=".$attrs['base']."&currencies=".$attrs['symbol']."";
248
249 if (isset($attrs['unit'])) {
250 $uri .= "&unit=".$attrs['unit']."";
251 }
252 if (isset($attrs['purity'])) {
253 $uri .= "&purity=".$attrs['purity']."";
254 }
255
256 $response = wp_remote_get($uri);
257 $body = wp_remote_retrieve_body($response);
258
259 if(empty(trim($body))) {
260 return $this->error_output('Response was empty.');
261 }
262 $json = json_decode($body, true);
263
264 if (isset($json['success']) && $json['success'] == true) {
265 $value = get_option($this->opt_data_success, '{{symbol}} {{price}}');
266
267 $symbol = $attrs['symbol'];
268 $rate = $this->safe_rate(isset($json['rates']) ? $json['rates'] : null, $symbol);
269 if ($rate === null) {
270 return get_option($this->opt_data_none, 'No result.');
271 }
272
273 $base = $this->response_base($json, $attrs['base']);
274 $timestamp = isset($json['timestamp']) ? $json['timestamp'] : null;
275
276 include(plugin_dir_path(__FILE__). 'module/currency_symbols.php');
277 if(isset($currency_symbols[$base])) {
278 $value = str_replace('{{base}}', esc_html($currency_symbols[$base]), $value);
279 } else {
280 $value = str_replace('{{base}}', esc_html($base), $value);
281 }
282
283 $value = str_replace('{{symbol}}', esc_html($symbol), $value);
284
285 $value = $this->replace_date_placeholders(
286 $value,
287 $timestamp,
288 $attrs['date_timezone'],
289 $attrs['date_format']
290 );
291
292 $price = 1.0/$rate;
293
294 if (isset($attrs['operation'])) {
295 $price = $this->safe_math_eval($attrs['operation'], $price);
296 }
297
298 $price = number_format($price, $attrs['price_round'], '.', '');
299
300 update_option($this->opt_api_status, 'API key is working.');
301
302 $value = str_replace('{{price}}', esc_html($price), $value);
303 } else if (isset($json['success']) && $json['success'] == false) {
304 $value = get_option($this->opt_data_error, 'Error: {{error}}');
305
306 list($status_code, $message) = $this->error_message($json);
307 if ($status_code !== '' || $message !== '') {
308 $error_text = $status_code !== '' ? trim("$status_code: {$message}") : $message;
309 $value = str_replace('{{error}}', esc_html($error_text), $value);
310
311 if ($status_code == 101 || $status_code == 102) {
312 update_option($this->opt_api_status, esc_html($message));
313 }
314 } else {
315 $value = str_replace('{{error}}', '', $value);
316 }
317 } else {
318 $value = $this->error_output('Unexpected response.');
319 }
320
321 return $value;
322 }
323
324 function shortcode_change($attrs) {
325 $attrs = shortcode_atts(array(
326 'base' => 'USD',
327 'symbol' => 'XAU',
328 'date_type' => 'recent', // recent, yesterday, week, month, year
329 'positive_text_color' => '#008033',
330 'negative_text_color' => '#FF0000',
331 'display_type' => 'percent', // percent, amount
332 'number_round' => 2,
333 'format' => '{{value}}', // {{base}}, {{value}}
334 ), $attrs);
335
336 // Sanitize shortcode attributes
337 $attrs['base'] = sanitize_text_field($attrs['base']);
338 $attrs['symbol'] = sanitize_text_field($attrs['symbol']);
339 $attrs['date_type'] = sanitize_text_field($attrs['date_type']);
340 $positive_color = sanitize_hex_color($attrs['positive_text_color']);
341 $negative_color = sanitize_hex_color($attrs['negative_text_color']);
342 $attrs['positive_text_color'] = $positive_color ? $positive_color : '#008033';
343 $attrs['negative_text_color'] = $negative_color ? $negative_color : '#FF0000';
344 $attrs['display_type'] = sanitize_text_field($attrs['display_type']);
345 $attrs['number_round'] = is_numeric($attrs['number_round']) ? intval($attrs['number_round']) : 2;
346 $attrs['format'] = sanitize_text_field($attrs['format']);
347
348 $base = $attrs['base'];
349 $symbol = $attrs['symbol'];
350
351 // Validate date_type
352 $valid_date_types = ['recent', 'yesterday', 'week', 'month', 'year'];
353 if (!in_array($attrs['date_type'], $valid_date_types)) {
354 return 'Invalid date_type parameter';
355 }
356
357 // Validate display_type
358 if (!in_array($attrs['display_type'], ['percent', 'amount'])) {
359 return 'Invalid display_type parameter';
360 }
361
362 $api_key = get_option($this->opt_api_key);
363
364 $uri = add_query_arg(array(
365 'api_key' => $api_key,
366 'base' => $base,
367 'currencies' => $symbol,
368 'date_type' => $attrs['date_type']
369 ), 'https://api.metalpriceapi.com/v1/change');
370
371 $response = wp_remote_get($uri);
372 $body = wp_remote_retrieve_body($response);
373
374 if (empty(trim($body))) {
375 return $this->error_output('Response was empty.');
376 }
377 $json = json_decode($body, true);
378
379 if (isset($json['success']) && $json['success'] == true) {
380 $symbol_rates = isset($json['rates'][$symbol]) ? $json['rates'][$symbol] : null;
381 $start_rate = $this->safe_rate($symbol_rates, 'start_rate');
382 $end_rate = $this->safe_rate($symbol_rates, 'end_rate');
383 if ($start_rate === null || $end_rate === null) {
384 return get_option($this->opt_data_none, 'No result.');
385 }
386 $change_pct = isset($symbol_rates['change_pct']) && is_numeric($symbol_rates['change_pct'])
387 ? $symbol_rates['change_pct'] : 0;
388
389 update_option($this->opt_api_status, 'API key is working.');
390
391 if ($attrs['display_type'] == 'percent') {
392 $raw_value = $change_pct;
393 } else {
394 $raw_value = (1.0/$end_rate) - (1.0/$start_rate);
395 }
396
397 $value = number_format($raw_value, intval($attrs['number_round']), '.', '');
398
399 if ($attrs['display_type'] == 'percent') {
400 $value = $value.'%';
401 }
402
403 $format = $attrs['format'];
404 include(plugin_dir_path(__FILE__). 'module/currency_symbols.php');
405 if(isset($currency_symbols[$base])) {
406 $format = str_replace('{{base}}', esc_html($currency_symbols[$base]), $format);
407 } else {
408 $format = str_replace('{{base}}', esc_html($base), $format);
409 }
410
411 $format = str_replace('{{value}}', esc_html($value), $format);
412
413 if ($raw_value > 0) {
414 $format = sprintf('<span style="color: %s">%s</span>',
415 esc_attr($attrs['positive_text_color']),
416 $format
417 );
418 } else if ($raw_value < 0) {
419 $format = sprintf('<span style="color: %s">%s</span>',
420 esc_attr($attrs['negative_text_color']),
421 $format
422 );
423 }
424
425 return $format;
426
427 } else if (isset($json['success']) && $json['success'] == false) {
428 $value = get_option($this->opt_data_error, 'Error: {{error}}');
429
430 list($status_code, $message) = $this->error_message($json);
431 if ($status_code !== '' || $message !== '') {
432 $error_text = $status_code !== '' ? trim("$status_code: {$message}") : $message;
433 $value = str_replace('{{error}}', esc_html($error_text), $value);
434
435 if ($status_code == 101 || $status_code == 102) {
436 update_option($this->opt_api_status, esc_html($message));
437 }
438 } else {
439 $value = str_replace('{{error}}', '', $value);
440 }
441 } else {
442 $value = $this->error_output('Unexpected response.');
443 }
444
445 return $value;
446 }
447
448 function shortcode_carat($attrs) {
449 $attrs = shortcode_atts(array(
450 'base' => 'USD',
451 'date_format' => 'Y-m-d',
452 'date_timezone' => 'UTC',
453 'date' => null,
454 'price_round' => 2,
455 'purity' => '24k',
456 ), $attrs);
457
458 // Sanitize shortcode attributes
459 $attrs['base'] = sanitize_text_field($attrs['base']);
460 $attrs['date_format'] = sanitize_text_field($attrs['date_format']);
461 $attrs['date_timezone'] = sanitize_text_field($attrs['date_timezone']);
462 $attrs['date'] = $attrs['date'] !== null ? sanitize_text_field($attrs['date']) : null;
463 $attrs['price_round'] = is_numeric($attrs['price_round']) ? intval($attrs['price_round']) : 2;
464 $attrs['purity'] = sanitize_text_field($attrs['purity']);
465
466 $api_key = get_option($this->opt_api_key);
467
468 $uri = "https://api.metalpriceapi.com/v1/carat?api_key=$api_key&base=".$attrs['base']."";
469 if (isset($attrs['date']) && !empty(trim($attrs['date']))) {
470 $uri .= "&date=".$attrs['date']."";
471 }
472
473 $response = wp_remote_get($uri);
474 $body = wp_remote_retrieve_body($response);
475
476 if(empty(trim($body))) {
477 return $this->error_output('Response was empty.');
478 }
479 $json = json_decode($body, true);
480
481 if (isset($json['success']) && $json['success'] == true) {
482 $value = get_option($this->opt_data_carat_success, '{{price}}');
483
484 $rate = $this->safe_rate(isset($json['data']) ? $json['data'] : null, $attrs['purity']);
485 if ($rate === null) {
486 return get_option($this->opt_data_none, 'No result.');
487 }
488
489 $base = $this->response_base($json, $attrs['base']);
490 $timestamp = isset($json['timestamp']) ? $json['timestamp'] : null;
491
492 include(plugin_dir_path(__FILE__). 'module/currency_symbols.php');
493 if(isset($currency_symbols[$base])) {
494 $value = str_replace('{{base}}', esc_html($currency_symbols[$base]), $value);
495 } else {
496 $value = str_replace('{{base}}', esc_html($base), $value);
497 }
498
499 $value = $this->replace_date_placeholders(
500 $value,
501 $timestamp,
502 $attrs['date_timezone'],
503 $attrs['date_format']
504 );
505
506 $price = $rate;
507 $price = number_format($price, $attrs['price_round'], '.', '');
508
509 update_option($this->opt_api_status, 'API key is working.');
510
511 $value = str_replace('{{price}}', esc_html($price), $value);
512 } else if (isset($json['success']) && $json['success'] == false) {
513 $value = get_option($this->opt_data_error, 'Error: {{error}}');
514
515 list($status_code, $message) = $this->error_message($json);
516 if ($status_code !== '' || $message !== '') {
517 $error_text = $status_code !== '' ? trim("$status_code: {$message}") : $message;
518 $value = str_replace('{{error}}', esc_html($error_text), $value);
519
520 if ($status_code == 101 || $status_code == 102) {
521 update_option($this->opt_api_status, esc_html($message));
522 }
523 } else {
524 $value = str_replace('{{error}}', '', $value);
525 }
526 } else {
527 $value = $this->error_output('Unexpected response.');
528 }
529
530 return $value;
531 }
532
533 function action() {
534 add_menu_page(
535 'MetalpriceAPI Settings',
536 'MetalpriceAPI',
537 'administrator',
538 'metalpriceapi',
539 [$this, 'render_settings']
540 );
541
542 add_submenu_page(
543 'metalpriceapi',
544 'MetalpriceAPI Settings',
545 'Settings',
546 'administrator',
547 'metalpriceapi-settings',
548 [$this, 'render_settings']
549 );
550
551 add_submenu_page(
552 'metalpriceapi',
553 'Instructions',
554 'Instructions',
555 'administrator',
556 'metalpriceapi-info',
557 [$this, 'render_info']
558 );
559
560 remove_submenu_page('metalpriceapi', 'metalpriceapi');
561 }
562
563 // render
564
565 function render_settings() {
566 // Add capability check
567 if (!current_user_can('manage_options')) {
568 wp_die('Unauthorized access');
569 }
570
571 $is_saved = 0;
572 if (isset($_POST['save'])) {
573 // Verify nonce
574 if (!isset($_POST['mpa_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mpa_nonce'])), -1)) {
575 wp_die('Security check failed');
576 }
577
578 $this->saveOption($this->opt_api_key);
579 $this->saveHTMLOption($this->opt_data_success);
580 $this->saveHTMLOption($this->opt_data_none);
581 $this->saveHTMLOption($this->opt_data_error);
582 $this->saveHTMLOption($this->opt_data_carat_success);
583
584 $is_saved = 1;
585 }
586
587 $api_status = get_option($this->opt_api_status);
588
589 require plugin_dir_path(__FILE__). 'module/settings.php';
590 }
591
592 function render_info() {
593 require plugin_dir_path(__FILE__). 'module/info.php';
594 }
595 }
596
597 new MetalpriceAPI();
598