· 8 years ago · Feb 28, 2018, 07:52 AM
1<?php
2/**
3 * Plugin Name: Easy CryptoCurrency Ticker
4 * Description: Display cryptocurrency ticker widget on your WordPress website
5 * Plugin URI: https://urosevic.net/wordpress/plugins/cc-ticker/
6 * Author: Aleksandar Urošević
7 * Author URI: https://urosevic.net
8 * Version: 1.0.1
9 * License: GPL3 or later
10 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
11 * Text Domain: cc-ticker
12 * Domain Path: languages
13 * Network: false
14 */
15
16// Exit if accessed directly
17if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19}
20
21if ( ! class_exists( 'Wpau_Cryptocurrency_Ticker' ) ) {
22 final class Wpau_Cryptocurrency_Ticker {
23 // Hold an instance of the class
24 private static $instance;
25
26 const DB_VER = 1;
27 const VER = '1.0';
28 public $plugin_name = 'Easy CryptoCurrency Ticker';
29 public $plugin_url;
30 private $cache_timeout = 2;
31 private $messages;
32 private static $coinlist;
33 private static $upload_dir_path;
34 public static $upload_dir_url;
35
36 function __construct() {
37
38 // Define various variables
39 $this->plugin_url = plugin_dir_url( __FILE__ );
40 $wp_upload_dir = wp_upload_dir();
41 self::$upload_dir_path = $wp_upload_dir['basedir'] . '/cc-ticker';
42 self::$upload_dir_url = $wp_upload_dir['baseurl'] . '/cc-ticker';
43 $this->messages = array(
44 'delay' => sprintf(
45 __( '' ),
46 $this->cache_timeout
47 ),
48 'attribution' => sprintf(
49 __( 'and provided by %s', 'cc-ticker' ),
50 '<a href="https://creaptomania.com/bitcoin/" target="_blank"></a>'
51 ),
52 );
53
54 // If there is no upload dir, attempt to create one
55 if ( ! is_dir( self::$upload_dir_path . '/ico' ) ) {
56 wp_mkdir_p( self::$upload_dir_path . '/ico' );
57 }
58
59 // Enqueue frontend scripts
60 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
61
62 // Include widget
63 require_once( 'inc/widget.php' );
64
65 // Register stock_ticker shortcode.
66 add_shortcode( 'cryptocurrency_ticker', array( $this, 'shortcode' ) );
67
68 // AJAX calls
69 add_action( 'wp_ajax_cct_get_coinlist', array( $this, 'ajax_get_coinlist' ) );
70 add_action( 'wp_ajax_cct_parse_coinlist', array( $this, 'ajax_parse_coinlist' ) );
71
72 // Helpers to get and scramble coinlist
73 if ( ! empty( $_GET['cct-get-coinlist'] ) ) {
74 self::get_coinlist();
75 }
76 if ( ! empty( $_GET['cct-parse-coinlist'] ) ) {
77 self::parse_coinlist();
78 }
79
80 // Parse coinlist
81 self::coinlist();
82
83 } // END function __construct()
84
85 /**
86 * The singleton method
87 * @return object Instance of class Wpau_Cryptocurrency_Ticker
88 */
89 public static function instance() {
90 if ( ! isset( self::$instance ) ) {
91 self::$instance = new Wpau_Cryptocurrency_Ticker();
92 }
93 return self::$instance;
94 } // END public static function instance()
95
96 /**
97 * Enqueue frontend assets
98 */
99 function enqueue_scripts() {
100 wp_enqueue_style(
101 'cc-ticker',
102 $this->plugin_url . 'assets/css/style.css',
103 array(),
104 self::VER
105 );
106 } // END function enqueue_scripts()
107
108 /**
109 * Render widget
110 * @param array $atts Array of shortcode parameters
111 * @return string Composed HTML output
112 */
113 function shortcode( $atts ) {
114
115 // Parse shortcode with default values
116 $atts = shortcode_atts( array(
117 'f' => 'BTC,ETC,XMR',
118 't' => 'USD',
119 'noicon' => false,
120 'nolink' => false,
121 'coinbase' => '',
122 'showchange' => '',
123 ), $atts );
124
125 // Fetch stock
126 $json = $this->fetch( $atts['f'], $atts['t'] );
127
128 // Convert JSON to data array
129 $data = json_decode( $json, true );
130
131 // If any error ocurred, return error message
132 if ( ! is_array( $data ) ) {
133 return __( 'We are so sorry because Easy CryptoCurrency Ticker can not be displayed at the moment.', 'cc-ticker' );
134 }
135
136 // Prepare empty variables used for shortcode
137 $html = $icon_style = $icon_class = '';
138
139 // Open ticker table
140 $html .= '<table class="cctw"><tbody>';
141 // Loop through all `from` currencies
142 foreach ( $data['DISPLAY'] as $from_symbol => $to_symbols ) {
143 $to_prices_html = array();
144 // Loop through all `to` currencies
145 foreach ( $to_symbols as $to_symbol => $to_data ) {
146 // Get change dirrection
147 $change_day = $data['RAW'][ $from_symbol ][ $to_symbol ]['CHANGEDAY'];
148 if ( $change_day < 0 ) {
149 $change_class = 'down';
150 } else if ( $change_day > 0 ) {
151 $change_class = 'up';
152 } else {
153 $change_class = 'unchanged';
154 }
155 // Get update timestamp from RAW
156 $timestamp = $data['RAW'][ $from_symbol ][ $to_symbol ]['LASTUPDATE'];
157
158 // Display change into?
159 $change_info = '';
160 if ( ! empty( $atts['showchange'] ) ) {
161 $change_info = sprintf(
162 '%1$s (%2$s%%)',
163 $data['DISPLAY'][ $from_symbol ][ $to_symbol ]['CHANGEDAY'],
164 $data['DISPLAY'][ $from_symbol ][ $to_symbol ]['CHANGEPCTDAY']
165 );
166 }
167
168 // Compose item for amount table cell
169 $to_prices_html[] = sprintf(
170 '<span class="amount %7$s" title="Mkt. Cap. %5$s - Last update %4$s"><span class="price"><span class="currency">%1$s</span> %2$s</span> <span class="change">%6$s</span></span>',
171 $to_data['TOSYMBOL'], // 1
172 str_replace( "{$to_data['TOSYMBOL']} ", '', $to_data['PRICE'] ), // 2
173 $to_symbol, // 3
174 date( 'r', intval( $timestamp ) ), // 4
175 $to_data['MKTCAP'], // 5
176 $change_info, // 6
177 $change_class // 7
178 );
179 }
180 // Join all cell rows with linebreak separator
181 $prices_html = implode( ' ', $to_prices_html );
182
183 // Prepare currency name
184 $currency_name = ! empty( self::$coinlist[ $from_symbol ] ) ? self::$coinlist[ $from_symbol ]['c'] : $from_symbol;
185
186 // Define icon style and currency classes
187 if ( empty( $atts['noicon'] ) && ! empty( self::$coinlist[ $from_symbol ]['i'] ) ) {
188 // Try to get local image
189 $icon_url = self::get_icon_url( self::$coinlist[ $from_symbol ]['i'] );
190 $icon_style = sprintf(
191 'style="background-image: url(%1$s);"',
192 $icon_url
193 );
194 $icon_class = 'ico';
195 } else {
196 $icon_class = 'noico';
197 }
198 $currency_class = "currency $icon_class";
199
200 // Compose cryptocurrency cell
201 if ( empty( $atts['nolink'] ) && ! empty( self::$coinlist[ $from_symbol ]['u'] ) ) {
202 // Do we need to link currency to overview?
203 $from_name = sprintf(
204 '<a href="https://creaptomania.com/%3$s" class="%6$s" %5$s target="_blank" title="%4$s">%3$s</a>',
205 'https://creaptomania.com/diagramm/', // 1
206 self::$coinlist[ $from_symbol ]['u'], // 2
207 $from_symbol, // 3
208 $currency_name, // 4
209 $icon_style, // 5
210 $currency_class // 6
211
212 );
213 } else {
214 // Or just to print unlinked cryptocurrency?
215 $from_name = sprintf(
216 '<span class="%4$s" %3$s title="%1$s">%2$s</span>',
217 $currency_name, // 1
218 $from_symbol, // 2
219 $icon_style, // 3
220 $currency_class // 4
221 );
222 }
223<?php
224echo strtolower( $from_symbol, $currency_name);
225?>
226 // Join all details to table row
227 $html .= sprintf(
228 '<tr><th>%1$s</th><td>%2$s</td></tr>',
229 $from_name,
230 $prices_html
231 );
232 }
233
234 // Close ticker table
235 $html .= '</tbody>';
236
237 // Prepare coinbase referral link if exists
238 $coinbase = '';
239 if ( ! empty( $atts['coinbase'] ) ) {
240 $coinbase_referral_id = self::sanitize_coinbase_id( $atts['coinbase'] );
241 if ( ! empty( $coinbase_referral_id ) ) {
242 $coinbase = sprintf(
243 '<span class="coinbase"><a href="https://www.coinbase.com/join/%s" target="_blank">Join coinbase.com community!</a></span>',
244 $coinbase_referral_id
245 );
246 }
247 }
248
249 // Prepare attribution for CoinBase
250 $attribution = '.';
251 if ( ! empty( $atts['nolink'] ) ) {
252 $attribution = ' ' . $this->messages['attribution'];
253 }
254 // Prepare Delay message
255 $delay = sprintf(
256 '<span class="delay">%1$s%2$s</span>',
257 $this->messages['delay'],
258 $attribution
259 );
260 // Append CryptoCompare.com attribution
261 $html .= sprintf(
262 '<tfoot><tr><td colspan="2">%1$s %2$s</td></tr></tfoot>',
263 $delay, // 1
264 $coinbase // 2
265 );
266
267 // Close table
268 $html .= '</table>';
269
270 // Return rendered HTML
271 return $html;
272
273 } // END function shortcode( $atts )
274
275 /**
276 * Get data from cached transient or from live server and cache to transient
277 * @param string $from From currencies
278 * @param string $to To currencies
279 * @return string JSON value
280 */
281 function fetch( $from = '', $to = '' ) {
282
283 // If we don't know from what and to what to convert, escape
284 if ( empty( $from ) || empty( $to ) ) {
285 return '';
286 }
287
288 // Sanitize symbols
289 $from = self::sanitize_symbols( $from );
290 $to = self::sanitize_symbols( $to );
291
292 // Define transient key
293 $transient_key = 'ccticker_a' . md5( "f={$from}_t={$to}" ) . $this->cache_timeout;
294
295 // Get transient if exists and not expired
296 if ( false === ( $json = get_transient( $transient_key ) ) ) {
297
298 // Build request URL
299 $url = "https://min-api.cryptocompare.com/data/pricemultifull?fsyms={$from}&tsyms={$to}";
300
301 // Do API request
302 $wparg = array(
303 'timeout' => intval( 10 ),
304 );
305 $response = wp_remote_get( $url, $wparg );
306
307 // Parse response
308 // @TODO: Make compatible with all API responses
309 if ( is_wp_error( $response ) ) {
310 return $response->get_error_message();
311 } else {
312 // Get response from body
313 $json = wp_remote_retrieve_body( $response );
314 // Save response JSON to transient
315 set_transient( $transient_key, $json, $this->cache_timeout * MINUTE_IN_SECONDS );
316 }
317 } // END if ( false === ( $json = get_transient( $transient_key ) ) )
318
319 // Return JSON content
320 return $json;
321 } // END function fetch( $from, $to )
322
323 /**
324 * Strip from symbols string all except uppercase letters and comma
325 * @param string $symbols Raw content of symbols string
326 * @return string Sanitized content of symbols string
327 */
328 public static function sanitize_symbols( $symbols ) {
329 if ( empty( $symbols ) ) {
330 return false;
331 }
332 return preg_replace( '/[^A-Z0-9\,\*]/', '', $symbols );
333 } // END private static function sanitize_symbols( $symbols )
334
335 /**
336 * Sanizite Coinbase Referral ID
337 * @param string $coinbase_id Raw version of Coinbase referral ID
338 * @return string Sanitized Coinbase referral ID or empty value
339 */
340 public static function sanitize_coinbase_id( $coinbase_id ) {
341 // If nothing provided, return empty value
342 if ( empty( $coinbase_id ) ) {
343 return '';
344 }
345 // Clean Coinbase referral ID
346 $cleaned_id = preg_replace( '/a-z0-9/','', $coinbase_id );
347 // Compare cleaned with trimmed value and return empty value if they are not same
348 if ( trim( $coinbase_id ) !== $cleaned_id ) {
349 return '';
350 }
351 // Return cleaned Coinbase Referral ID
352 return $cleaned_id;
353 } // END public static function validate_coinbase_id( $coinbase_id )
354
355 /**
356 * Prepare Coinlist array
357 * @return array Cryptocurrency Coinlist array
358 */
359 private static function coinlist() {
360 // $coinlist = dirname( __FILE__ ) . '/coinlist.min.json';
361 $coinlist = self::$upload_dir_path . '/coinlist.min.json';
362 // If no coinlist file exists, call method to create new one
363 if ( ! file_exists( $coinlist ) ) {
364 self::parse_coinlist();
365 }
366 // Get content from coinlist file
367 $json = file_get_contents( $coinlist );
368 // Add decoded data array to $coinlist variable
369 self::$coinlist = json_decode( $json, true );
370 } // END private static function coinlist()
371
372 static function ajax_get_coinlist() {
373 self::get_coinlist();
374 self::parse_coinlist();
375 $result = array(
376 'status' => 'success',
377 'message' => 'New coinlist has been fetched from CryptoCompare.com and prepared for local use. You can view updated coinlist.',
378 );
379 $result = json_encode( $result );
380 echo $result;
381 wp_die();
382 } // END private static function ajax_get_coinlist()
383
384 /**
385 * Download coinlist from live server and store locally
386 * @TODO: Move file to `res` directory
387 */
388 private static function get_coinlist() {
389 $url = 'https://min-api.cryptocompare.com/data/all/coinlist';
390 $wparg = array(
391 'timeout' => intval( 10 ),
392 );
393 $response = wp_remote_get( $url, $wparg );
394
395 // Parse response
396 if ( is_wp_error( $response ) ) {
397 return $response->get_error_message();
398 } else {
399 // Get response from body
400 $json = wp_remote_retrieve_body( $response );
401 // Write to local file
402 // file_put_contents( dirname( __FILE__ ) . '/coinlist.json', $json );
403 file_put_contents( self::$upload_dir_path . '/coinlist.json', $json );
404 }
405 } // END private static function get_coinlist()
406
407 static function ajax_parse_coinlist() {
408 self::parse_coinlist();
409 $result = array(
410 'status' => 'success',
411 'message' => 'Local coinlist has been prepared. You can view it now.',
412 );
413 $result = json_encode( $result );
414 echo $result;
415 wp_die();
416 } // END private static function ajax_parse_coinlist()
417
418 /**
419 * Parse full coinlist and store only used data to minified coinlist for regular usage
420 * @TODO: Move file to `res` directory
421 */
422 private static function parse_coinlist() {
423 // $source_coinlist = dirname( __FILE__ ) . '/coinlist.json';
424 // $coinlist = dirname( __FILE__ ) . '/coinlist.min.json';
425 $source_coinlist = self::$upload_dir_path . '/coinlist.json';
426 $coinlist = self::$upload_dir_path . '/coinlist.min.json';
427
428 // Get new coinlist if file does not exists
429 if ( ! file_exists( $source_coinlist ) ) {
430 self::get_coinlist();
431 }
432 $json = file_get_contents( $source_coinlist );
433 $data = json_decode( $json, true );
434
435 // If response is success, go through Data array
436 if ( ! empty( $data['Response'] ) && 'Success' == $data['Response'] ) {
437 $new_data = array();
438 foreach ( $data['Data'] as $curr => $curr_data ) {
439 $new_data[ $curr ] = $curr_data['CoinName'];
440 $new_data[ $curr ] = array(
441 'u' => $curr_data['Url'],
442 'c' => $curr_data['CoinName'],
443 );
444 // Append image if exists
445 if ( ! empty( $curr_data['ImageUrl'] ) ) {
446 $new_data[ $curr ]['i'] = $curr_data['ImageUrl'];
447 }
448 }
449 // Pack PHP array to JSON
450 $new_json = json_encode( $new_data );
451 // Write JSON to local file
452 file_put_contents( $coinlist, $new_json );
453 } // END if ( ! empty( $data['Response'] ) ...
454 } // END private static function parse_coinlist()
455
456 /**
457 * Extract icon URL from local or remote URL as fallback
458 * @param string $path Cryptocurrency icon media path
459 * @return string URL from where icon will be pulled on page
460 */
461 private static function get_icon_url( $path ) {
462
463 // If no $path provided, return empty value
464 if ( empty( $path ) ) {
465 return '';
466 }
467
468 // Define remote file
469 $remote_icon_url = "https://www.cryptocompare.com{$path}";
470
471 // Prepare filename for new image
472 $icon_filename = str_replace( '/media/', '', $path );
473 $icon_filename = str_replace( '/', '-', $icon_filename );
474 // Prepare target path for new image
475 $icon_path = self::$upload_dir_path . '/ico/' . $icon_filename;
476 $icon_url = self::$upload_dir_url . '/ico/' . $icon_filename;
477
478 // If image file does not exists, download it
479 if ( ! file_exists( $icon_path ) ) {
480
481 // Include file.php
482 @include_once( ABSPATH . '/wp-admin/includes/file.php' );
483
484 // If download_url() function does not exists after including file, return fallback
485 if ( ! function_exists( 'download_url' ) ) {
486 return $remote_icon_url;
487 }
488
489 // Download remote file
490 $icon_tmp = download_url( $remote_icon_url );
491 if ( is_wp_error( $icon_tmp ) ) {
492 // If WP_Error ocurred, unlink temp file and return fallback
493 @unlink( $icon_tmp );
494 return $remote_icon_url;
495 } else {
496 // Copy temp file to final destination
497 $ret = copy( $icon_tmp, $icon_path );
498 @unlink( $icon_tmp );
499 // If file can not be copied, return fallback
500 if ( false === $ret ) {
501 return $remote_icon_url;
502 }
503 // Now let we resize big image
504 // @ref: https://developer.wordpress.org/reference/functions/wp_get_image_editor/
505 $image = wp_get_image_editor( $icon_path );
506 if ( ! is_wp_error( $image ) ) {
507 // Resize to 20px
508 $image->resize( 20, 20 );
509 $image->save( $icon_path );
510 }
511 }
512 }
513 return $icon_url;
514 } // END private static function get_icon_url( $path )
515
516 } // END class Wpau_Cryptocurrency_Ticker
517} // END if ( ! class_exists( 'Wpau_Cryptocurrency_Ticker' ) )
518
519// Initialize class
520$wpau_cryptocurrency_ticker = Wpau_Cryptocurrency_Ticker::instance();