PluginProbe
M Chart / 2.3.1
M Chart v2.3.1
2.3.2 2.3.1 2.3 2.2.2 2.2.1 2.2 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.10 1.10.1 1.11 1.11.1 1.11.2 1.12 1.2 1.2.1 1.3 1.3.1 1.3.2 All 53 releases
m-chart / components / class-m-chart.php

class-m-chart.php in M Chart 2.3.1, at components/class-m-chart.php

1,473 lines 46.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class M_Chart {
8 public $version = '2.3.1';
9 public $slug = 'm-chart';
10 public $plugin_name = 'Chart';
11 public $chart_meta_fields = [
12 'library' => 'chartjs',
13 'type' => 'line',
14 'parse_in' => 'rows',
15 'labels' => true,
16 'shared' => false,
17 'subtitle' => '',
18 'y_title' => '',
19 'y_units' => '',
20 'y_min' => false,
21 'y_min_value' => 0,
22 'x_title' => '',
23 'x_units' => '',
24 'height' => 600,
25 'legend' => true,
26 'source' => '',
27 'source_url' => '',
28 'data' => [],
29 'set_names' => [],
30 'data_point_colors' => false,
31 'mean_point' => true,
32 'sample_points' => false,
33 'constrain_y_axis' => false,
34 'include_source' => false,
35 ];
36 public $get_chart_default_args = [
37 'show' => 'chart',
38 'width' => 'responsive',
39 'share' => '',
40 ];
41 public $parse_options = [
42 'columns',
43 'rows',
44 ];
45 public $options_set;
46 public $plugin_url;
47 public $is_iframe = false;
48 public $instance = 1;
49 public $iframe_csp_nonce = '';
50 public $settings = [
51 'library' => 'chartjs',
52 'show_library' => 'no',
53 'performance' => 'default',
54 'image_multiplier' => '2',
55 'image_width' => '900',
56 'default_height' => '600',
57 'embeds' => '',
58 'defer_rendering' => 'enabled',
59 'default_theme' => '_default',
60 'locale' => 'en-US',
61 'csv_delimiter' => ',',
62 ];
63 public $csv_delimiters = [
64 ',' => 'Comma',
65 "\t" => 'Tab',
66 ' ' => 'Space',
67 ';' => 'Semicolon',
68 ];
69 public $library_class;
70 public $icon = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjY0IDY0IDI0IDI0IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4Ij4KICA8cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0gNzYuNjU3IDY1LjUgTCA3Ni42NTcgNzUuMzQ0IEwgODYuNSA3NS4zNDQgQyA4Ni41IDY5LjkwOSA4Mi4wOTEgNjUuNSA3Ni42NTcgNjUuNSBaIE0gNzQuNjg4IDc2LjMyOCBMIDc0LjY4OCA2Ni44MzMgQyA2OS41NTcgNjcuMTc0IDY1LjUgNzEuNDM5IDY1LjUgNzYuNjU3IEMgNjUuNSA4Mi4wOTEgNjkuOTA5IDg2LjUgNzUuMzQzIDg2LjUgQyA4MC41NjIgODYuNSA4NC44MjggODIuNDQzIDg1LjE2NyA3Ny4zMTMgTCA3NC42ODggNzcuMzEzIEwgNzQuNjg4IDc2LjMyOCBaIi8+Cjwvc3ZnPg==';
71 public $logo = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyBpZD0idXVpZC03OGNkNDE3YS0zN2NjLTQ0NTAtODU2ZC00MzBlMTliYjhmOTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDQzNyAzNzAuMDU2Ij4KICA8ZyBpZD0idXVpZC1hMDM3MTk0ZS1hN2QzLTQ4NTktOGEwZi0wMmIzMWI0YmQ3MzEiIHRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIDEsIDM4LjQwMzk5OSwgNTUuOTM3MDA0KSI+CiAgICA8cmVjdCB4PSIyODEuNDAxIiB5PSIxNDguNTExIiB3aWR0aD0iODguNzEiIGhlaWdodD0iMTQxLjQ1IiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyIvPgogICAgPHJlY3QgeD0iLTkuOTE5IiB5PSIyNjQuODIxIiB3aWR0aD0iODguNzEiIGhlaWdodD0iMjUuMTMiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSItOS45MTkiIHk9Ii0zMS43NzkiIHdpZHRoPSI4OC43MSIgaGVpZ2h0PSIyODQuMjEiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSIyODEuNDAxIiB5PSItMzEuNzc5IiB3aWR0aD0iODguNzEiIGhlaWdodD0iMTY3Ljg3IiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyIvPgogICAgPHJlY3QgeD0iMTg2LjI4MSIgeT0iMTk0LjU1MSIgd2lkdGg9IjgyLjc5IiBoZWlnaHQ9Ijk1LjQiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSI5MS4wODEiIHk9IjIzNS40MTEiIHdpZHRoPSI4Mi45NCIgaGVpZ2h0PSI1NC41NSIgc3R5bGU9InN0cm9rZS13aWR0aDogMTsiLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjgxLjQwMSAtMzEuNzc5IDE4MC4xMDEgNjUuMTExIDE4MC4xMDEgMTc2LjcwMSAyODEuNDAxIDc5LjcxMSAyODEuNDAxIC0zMS43NzkiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cG9seWdvbiBwb2ludHM9Ijc4LjgwMSA3OS43MTEgMTgwLjEwMSAxNzYuNzAxIDE4MC4xMDEgNjUuMTExIDc4LjgwMSAtMzEuNzc5IDc4LjgwMSA3OS43MTEiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgPC9nPgo8L3N2Zz4=';
72
73 private $admin;
74 private $parse;
75 private $block;
76 private $fs;
77 private $resolved_settings;
78 private $libraries = [
79 'chartjs' => 'Chart.js',
80 ];
81
82 /**
83 * Constructor
84 */
85 public function __construct() {
86 // Initiate Freemius first so it loads as early as possible
87 // Must run synchronously here at file load so Freemius detects m-chart.php as the plugin file
88 $this->freemius();
89
90 $this->plugin_url = $this->plugin_url();
91
92 add_action( 'init', [ $this, 'init' ] );
93 add_action( 'plugins_loaded', [ $this, 'plugins_loaded' ] );
94 add_action( 'save_post', [ $this, 'save_post' ] );
95 // Doing this early as possible because it sets is_iframe which we might need to use for other things
96 add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 );
97 add_action( 'm_chart_update_post_meta', [ $this, 'm_chart_update_post_meta' ], 10, 2 );
98
99 // Doing this before the default so it's already done before anything else
100 add_filter( 'm_chart_get_chart_image_tag', [ $this, 'm_chart_get_chart_image_tag' ], 9, 3 );
101 add_filter( 'the_content', [ $this, 'the_content' ] );
102 add_filter( 'm_chart_image_support', [ $this, 'm_chart_image_support' ], 10, 2 );
103 add_filter( 'm_chart_instant_preview_support', [ $this, 'm_chart_instant_preview_support' ], 10, 2 );
104 add_filter( 'm_chart_library_class', [ $this, 'm_chart_library_class' ], 10, 2 );
105
106 add_shortcode( 'm-chart', [ $this, 'chart_shortcode' ] );
107 // Backwards-compatible alias for content created before the rename
108 add_shortcode( 'chart', [ $this, 'chart_shortcode' ] );
109
110 // Initiate the block class
111 $this->block();
112 }
113
114 /**
115 * Initiate and return the Freemius SDK instance
116 *
117 * Lazily builds the instance on first call and caches it
118 * The SDK lives in components/external/freemius rather than the usual plugin root /freemius
119 * M Chart core (m-chart) is the free version so is_premium is false
120 * M Chart Pro (m-chart-pro) is a separate plugin sold under the same Freemius product via parallel activation
121 *
122 * @return Freemius the Freemius SDK instance for M Chart
123 */
124 public function freemius() {
125 if ( isset( $this->fs ) ) {
126 return $this->fs;
127 }
128
129 require_once __DIR__ . '/external/freemius/start.php';
130
131 $this->fs = fs_dynamic_init( [
132 'id' => '30258',
133 'slug' => 'm-chart',
134 'premium_slug' => 'm-chart-pro',
135 'type' => 'plugin',
136 'public_key' => 'pk_b1eafc082abd31243874041aaaaab',
137 'is_premium' => false,
138 'has_premium_version' => true,
139 'has_addons' => false,
140 'has_paid_plans' => true,
141 'is_org_compliant' => true,
142 'parallel_activation' => [
143 'enabled' => true,
144 'premium_version_basename' => 'm-chart-pro/m-chart-pro.php',
145 ],
146 'menu' => [
147 // Anchor on the Charts CPT itself so the Freemius pages are plugin-level (m-chart-account, m-chart-pricing)
148 // The CPT slug contains ".php?" so get_slug() falls back to the m-chart affix rather than an m-chart-settings- prefix
149 'slug' => 'edit.php?post_type=m-chart',
150 // Surface the Freemius Account page as a submenu link under the Charts menu
151 'account' => true,
152 'contact' => false,
153 'support' => false,
154 ],
155 ] );
156
157 $this->freemius_filters();
158
159 do_action( 'm_chart_fs_loaded' );
160
161 return $this->fs;
162 }
163
164 /**
165 * Register Freemius filters that brand the pricing and opt-in screens
166 *
167 * pricing/css_path points Freemius at our stylesheet which is enqueued on the pricing page after its own CSS
168 * pricing/show_annual_in_monthly shows the real annual price rather than the monthly equivalent of the annual plan
169 *
170 * The css_path and plugin_icon paths run through Freemius's fs_asset_url which maps a path to a URL by stripping WP_PLUGIN_DIR
171 * __DIR__ resolves to the plugin's real path which differs from WP_PLUGIN_DIR when the plugin is symlinked
172 * plugin_basename normalizes it back to the plugins-dir-relative path so the URL resolves in every install
173 *
174 * The opt-in / connect screen has no css_path filter so we inline our stylesheet on the connect/before action instead
175 * It prints after Freemius's own connect.css so equal-specificity overrides win on source order
176 */
177 private function freemius_filters() {
178 $this->fs->add_filter( 'pricing/css_path', function () {
179 return WP_PLUGIN_DIR . '/' . plugin_basename( __DIR__ . '/css/m-chart-freemius-pricing.css' );
180 } );
181
182 // Re-point the pricing page contact links at M Chart's own support/contact pages
183 // Freemius wraps the pricing output in this filter so the inline script is scoped to that screen
184 $this->fs->add_filter( 'templates/pricing.php', function ( $html ) {
185 $script_path = __DIR__ . '/js/m-chart-freemius-pricing.js';
186
187 if ( ! file_exists( $script_path ) ) {
188 return $html;
189 }
190
191 return $html
192 . "<script id=\"m-chart-freemius-pricing\">\n"
193 . file_get_contents( $script_path ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
194 . "</script>\n";
195 } );
196
197 $this->fs->add_filter( 'plugin_icon', function () {
198 return WP_PLUGIN_DIR . '/' . plugin_basename( __DIR__ . '/images/m-chart-logo-square-rounded.svg' );
199 } );
200
201 $this->fs->add_filter( 'pricing/show_annual_in_monthly', '__return_false' );
202
203 $this->fs->add_action( 'connect/before', function () {
204 $css_path = __DIR__ . '/css/m-chart-freemius-connect.css';
205
206 if ( ! file_exists( $css_path ) ) {
207 return;
208 }
209
210 echo "<style id=\"m-chart-freemius-connect\">\n";
211 echo file_get_contents( $css_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
212 echo "</style>\n";
213 } );
214
215 // Swap Freemius's casual interjection headings for professional wording
216 // These are admin-notice titles keyed by id and the trailing "!" / "..." is appended by the SDK so it carries over
217 // Values are left unescaped since Freemius escapes them at the output site
218 // Deferred to init since freemius() runs at construct and the __() calls would load the text domain before WordPress is ready
219 add_action( 'init', function () {
220 fs_override_i18n( [
221 'yee-haw' => __( 'Success', 'm-chart' ),
222 'woot' => __( 'Success', 'm-chart' ),
223 'right-on' => __( 'Success', 'm-chart' ),
224 'oops' => __( 'Error', 'm-chart' ),
225 'hmm' => __( 'Heads up', 'm-chart' ),
226 'hey' => __( 'Hello', 'm-chart' ),
227 ], $this->slug );
228 } );
229 }
230
231 /**
232 * Admin object accessor
233 */
234 public function admin() {
235 if ( ! $this->admin ) {
236 require_once __DIR__ . '/class-m-chart-admin.php';
237 $this->admin = new M_Chart_Admin();
238 }
239
240 return $this->admin;
241 }
242
243 /**
244 * Block object accessor
245 */
246 public function block() {
247 if ( ! $this->block ) {
248 require_once __DIR__ . '/class-m-chart-block.php';
249 $this->block = new M_Chart_Block();
250 }
251
252 return $this->block;
253 }
254
255 /**
256 * Library object accessor
257 */
258 public function library( $library = false ) {
259 if ( ! $library ) {
260 $library = $this->get_library();
261 }
262
263 return apply_filters( 'm_chart_library_class', $this->library_class, $library );
264 }
265
266 /**
267 * Parse object accessor
268 */
269 public function parse() {
270 if ( ! $this->parse ) {
271 require_once __DIR__ . '/class-m-chart-parsed-data-point.php';
272 require_once __DIR__ . '/class-m-chart-parse.php';
273 $this->parse = new M_Chart_Parse();
274 }
275
276 return $this->parse;
277 }
278
279 /**
280 * Do init stuff
281 */
282 public function init() {
283 // Register the units taxonomy
284 register_taxonomy(
285 $this->slug . '-units',
286 [ $this->slug ],
287 [
288 'hierarchical' => true,
289 'labels' => [
290 'name' => esc_html__( 'Chart Units', 'm-chart' ),
291 'singular_name' => esc_html__( 'Chart Unit', 'm-chart' ),
292 'search_items' => esc_html__( 'Search Chart Units', 'm-chart' ),
293 'all_items' => esc_html__( 'All Chart Units', 'm-chart' ),
294 'parent_item' => esc_html__( 'Parent Chart Unit', 'm-chart' ),
295 'parent_item_colon' => esc_html__( 'Parent Chart Unit:', 'm-chart' ),
296 'edit_item' => esc_html__( 'Edit Chart Unit', 'm-chart' ),
297 'update_item' => esc_html__( 'Update Chart Unit', 'm-chart' ),
298 'add_new_item' => esc_html__( 'Add New Chart Unit', 'm-chart' ),
299 'new_item_name' => esc_html__( 'Chart Unit Name', 'm-chart' ),
300 'menu_name' => esc_html__( 'Chart Units', 'm-chart' ),
301 ],
302 'show_ui' => true,
303 'query_var' => true,
304 'rewrite' => [
305 'slug' => $this->slug . '-units',
306 ],
307 ]
308 );
309
310 // Register the library taxonomy
311 register_taxonomy(
312 $this->slug . '-library',
313 [ $this->slug ],
314 [
315 'hierarchical' => false,
316 'show_ui' => false,
317 'query_var' => true,
318 'rewrite' => [
319 'slug' => $this->slug . '-library',
320 ],
321 ]
322 );
323
324 // Register the charts custom post type
325 register_post_type(
326 $this->slug,
327 [
328 'labels' => [
329 'name' => esc_html__( 'Charts', 'm-chart' ),
330 // Top-level admin menu label defaults to 'name' so we set it explicitly to brand the menu as M Chart
331 'menu_name' => esc_html__( 'M Chart', 'm-chart' ),
332 // First submenu item label defaults to menu_name so we set it back to Charts (parent reads M Chart, list reads Charts)
333 'all_items' => esc_html__( 'Charts', 'm-chart' ),
334 'singular_name' => esc_html__( 'Chart', 'm-chart' ),
335 'add_new' => esc_html__( 'Add Chart', 'm-chart' ),
336 'add_new_item' => esc_html__( 'Add Chart', 'm-chart' ),
337 'edit' => esc_html__( 'Edit', 'm-chart' ),
338 'edit_item' => esc_html__( 'Edit Chart', 'm-chart' ),
339 'new_item' => esc_html__( 'New Chart', 'm-chart' ),
340 'view' => esc_html__( 'View', 'm-chart' ),
341 'view_item' => esc_html__( 'View Chart', 'm-chart' ),
342 'search_items' => esc_html__( 'Search Charts', 'm-chart' ),
343 'not_found' => esc_html__( 'No Charts found', 'm-chart' ),
344 'not_found_in_trash' => esc_html__( 'No Charts found in the Trash', 'm-chart' ),
345 ],
346 'register_meta_box_cb' => is_admin() ? [ $this->admin(), 'meta_boxes' ] : null,
347 'public' => true,
348 'show_in_rest' => true,
349 'hierarchical' => false,
350 'exclude_from_search' => false,
351 'menu_position' => 9,
352 'menu_icon' => 'data:image/svg+xml;base64,' . $this->logo,
353 'query_var' => true,
354 'can_export' => true,
355 'has_archive' => 'charts',
356 'description' => esc_html__( 'Manage data sets and display them as charts in WordPress.', 'm-chart' ),
357 'rewrite' => [
358 'slug' => 'chart',
359 ],
360 'supports' => [
361 'author',
362 'title',
363 'excerpt',
364 'comments',
365 ],
366 'taxonomies' => [
367 'category',
368 'post_tag',
369 $this->slug . '-units',
370 ],
371 ]
372 );
373
374 // Register the graphing library scripts
375 wp_register_script(
376 'chartjs',
377 $this->plugin_url . '/components/external/chartjs/chart.js',
378 [],
379 $this->version
380 );
381
382 wp_register_script(
383 'chartjs-helper',
384 $this->plugin_url . '/components/js/m-chart-chartjs-helper.min.js',
385 [ 'chartjs' ],
386 $this->version
387 );
388
389 wp_register_script(
390 'chartjs-datalabels',
391 $this->plugin_url . '/components/external/chartjs/chartjs-plugin-datalabels.js',
392 [ 'chartjs' ],
393 $this->version
394 );
395
396 wp_register_script(
397 'chartjs-treemap',
398 $this->plugin_url . '/components/external/chartjs/chartjs-chart-treemap.min.js',
399 [ 'chartjs' ],
400 $this->version
401 );
402
403 wp_register_script(
404 'chartjs-boxplot',
405 $this->plugin_url . '/components/external/chartjs/chartjs-chart-boxplot.min.js',
406 [ 'chartjs' ],
407 $this->version
408 );
409
410 // The venn UMD build reads the global Chart at load time so the chartjs dependency is required
411 wp_register_script(
412 'chartjs-venn',
413 $this->plugin_url . '/components/external/chartjs/chartjs-chart-venn.min.js',
414 [ 'chartjs' ],
415 $this->version
416 );
417
418 // Add endpoint needed for iframe embed support
419 add_rewrite_endpoint( 'embed', EP_PERMALINK );
420
421 // Check if we need to run any upgrades
422 $current_version = get_site_option( 'm_chart_version' );
423
424 if ( version_compare( $current_version, '1.7', 'lt' ) ) {
425 $this->upgrade_to_1_7();
426 }
427
428 if ( version_compare( $current_version, '1.7.4', 'lt' ) ) {
429 $this->upgrade_to_1_7_4();
430 }
431 }
432
433 /**
434 * Do plugins loaded stuff
435 */
436 public function plugins_loaded() {
437 load_plugin_textdomain( 'm-chart', false, plugin_basename( dirname( __FILE__ ) ) . '/languages/' );
438 }
439
440 /**
441 * WP VIP's CDN was breaking the chart library's ability to handle embedded SVGs so this should circumvent that
442 * If you wanted to say, watermark your charts, SVGs suddenly become very important
443 *
444 * @param string $path option additional path to be used (e.g. components)
445 *
446 * @return string URL to the plugin directory with path if parameter was passed
447 */
448 public function plugin_url( $path = '' ) {
449 if ( is_admin() ) {
450 $url_base = parse_url( admin_url() );
451 } else {
452 $url_base = parse_url( home_url() );
453 }
454
455 $url_path = parse_url( plugins_url( $path, __DIR__ ) );
456
457 // Check for a port value if one exists we make sure it's honored
458 $port = '';
459
460 if ( isset( $url_base['port'] ) && 80 != $url_base['port'] ) {
461 $port = ':' . $url_base['port'];
462 }
463
464 $scheme = $url_base['scheme'] ?? 'https';
465 $host = $url_base['host'] ?? '';
466 $path = $url_path['path'] ?? '';
467 $query = $url_path['query'] ?? '';
468
469 return $scheme . '://' . $host . $port . preg_replace( '#/$#', '', $path ) . ( '' === $query ? '' : '?' . $query );
470 }
471
472 /**
473 * Get chart post meta
474 *
475 * @param int $post_id WP post ID of the post you want post meta from
476 * @param string $field optional field to be returend instead of all post meta
477 *
478 * @return array $post_meta the meta for a give post ID
479 */
480 public function get_post_meta( $post_id, $field = false ) {
481 $raw_post_meta = get_post_meta( $post_id, $this->slug, true );
482
483 $post_meta = $raw_post_meta;
484
485 $defaults = $this->chart_meta_fields;
486
487 // Make sure the correct library is set in the default chart meta fields
488 if ( ! $post_meta ) {
489 // get_current_screen() is only defined in admin context
490 // This guards the call so REST and front-end contexts don't fatal
491 $current_screen = is_admin() && function_exists( 'get_current_screen' )
492 ? get_current_screen()
493 : null;
494
495 // If we're we're adding a new chart and a library is specified in the get vars we use it
496 if (
497 is_admin()
498 && null != $current_screen
499 && 'post' == $current_screen->base
500 && 'add' == $current_screen->action
501 && isset( $_GET['library'] )
502 && $this->is_valid_library( $_GET['library'] )
503 ) {
504 $defaults['library'] = $_GET['library'];
505 } else {
506 $defaults['library'] = $this->get_library();
507 }
508 }
509
510 // Default chart height comes from the plugin settings so new charts honor the site default
511 // Saved charts already have a height so wp_parse_args leaves theirs alone
512 $defaults['height'] = (int) $this->get_settings( 'default_height' );
513
514 $post_meta = wp_parse_args( $post_meta, $defaults );
515
516 // Theme default is based off of an option so we'll handle that here
517 if ( ! isset( $post_meta['theme'] ) ) {
518 $settings = $this->get_settings();
519 $post_meta['theme'] = $settings['default_theme'];
520 }
521
522 // If there's no subtitle set we'll set an empty one
523 if ( ! isset( $post_meta['subtitle'] ) ) {
524 $post_meta['subtitle'] = '';
525 }
526
527 // If there's no y min value set we'll set it to 0
528 if ( ! isset( $post_meta['y_min_value'] ) ) {
529 $post_meta['y_min_value'] = 0;
530 }
531
532 // If the data has the old legacy format we need to update it
533 if ( isset( $post_meta['data'] ) && ! isset( $post_meta['data']['sets'] ) ) {
534 $data = $post_meta['data'];
535 $post_meta['data'] = [];
536 $post_meta['data']['sets'][] = $data;
537 }
538
539 // If there's no set_names value set we'll set it to an empty array
540 if ( ! isset( $post_meta['set_names'] ) ) {
541 $post_meta['set_names'] = [];
542 }
543
544 $post_meta = apply_filters( 'm_chart_get_post_meta', $post_meta, $raw_post_meta, $post_id );
545
546 if ( $field && isset( $post_meta[ $field ] ) ) {
547 return $post_meta[ $field ];
548 } elseif ( $field ) {
549 return null;
550 }
551
552 return $post_meta;
553 }
554
555 /**
556 * Update the post meta and set unit terms if appropriate
557 *
558 * @param int $post_id WP post ID of the post you want to attach post meta to
559 * @param array $meta an array of the post meta you want to attach to the post
560 */
561 public function update_post_meta( $post_id, $meta ) {
562 // Make sure the meta is formatted correctly and validated
563 $parsed_meta = $this->validate_post_meta( $meta );
564
565 // Set unit terms
566 $terms = [];
567
568 if ( '' != $parsed_meta['y_units'] ) {
569 $terms[] = $parsed_meta['y_units'];
570 }
571
572 if ( '' != $parsed_meta['x_units'] ) {
573 $terms[] = $parsed_meta['x_units'];
574 }
575
576 wp_set_object_terms( $post_id, $terms, $this->slug . '-units' );
577
578 // Set library as a post_tag
579 wp_set_object_terms( $post_id, $parsed_meta['library'], $this->slug . '-library' );
580
581 // Save meta to the post
582 update_post_meta( $post_id, $this->slug, $parsed_meta );
583 do_action( 'm_chart_update_post_meta', $post_id, $parsed_meta, $meta );
584 }
585
586 /**
587 * Parses a $meta array and returns a cleaned and validated array
588 *
589 * @param array $meta an array of the post meta you want to attach to the post
590 *
591 * @return array of cleaned/validated post meta
592 */
593 public function validate_post_meta( $meta ) {
594 // Need to set checkboxes before checking or they can't be deselected
595 $chart_meta['labels'] = false;
596 $chart_meta['y_min'] = false;
597 $chart_meta['legend'] = false;
598 $chart_meta['data_point_colors'] = false;
599 $chart_meta['mean_point'] = false;
600 $chart_meta['sample_points'] = false;
601 $chart_meta['constrain_y_axis'] = false;
602 $chart_meta['include_source'] = false;
603
604 // Filter values so we know the data is clean
605 foreach ( $this->chart_meta_fields as $field => $default ) {
606 if ( isset( $meta[ $field ] ) ) {
607 if ( 'source_url' == $field && '' != $meta[ $field ] ) {
608 $chart_meta[ $field ] = esc_url_raw( $meta[ $field ] );
609 } elseif ( 'data' == $field ) {
610 $chart_meta[ $field ]['sets'] = $meta[ $field ];
611 } elseif ( 'set_names' == $field ) {
612 $chart_meta[ $field ] = array_values( $meta[ $field ] );
613 } elseif ( in_array( $field, [ 'labels', 'y_min', 'legend', 'data_point_colors', 'mean_point', 'sample_points', 'constrain_y_axis', 'include_source' ] ) ) {
614 $chart_meta[ $field ] = (bool) $meta[ $field ];
615 } elseif ( 'height' == $field ) {
616 $chart_meta[ $field ] = absint( $meta[ $field ] );
617
618 if ( $chart_meta[ $field ] > 1500 ) {
619 $chart_meta[ $field ] = 1500;
620 } elseif ( $chart_meta[ $field ] < 300 ) {
621 $chart_meta[ $field ] = 300;
622 }
623 } elseif ( 'y_min_value' == $field ) {
624 $chart_meta[ $field ] = floatval( $meta[ $field ] );
625 } else {
626 $chart_meta[ $field ] = wp_filter_nohtml_kses( $meta[ $field ] );
627 }
628 } elseif ( ! isset( $chart_meta[ $field ] ) ) {
629 // Fall back on the default value if there wasn't one in the given meta
630 // Height honors the default_height setting so the save path agrees with get_post_meta()
631 if ( 'height' === $field ) {
632 $chart_meta[ $field ] = (int) $this->get_settings( 'default_height' );
633 } else {
634 $chart_meta[ $field ] = $default;
635 }
636 }
637 }
638
639 // The theme meta it isn't included in the chart_meta_fields class var so we handle it here
640 if ( isset( $meta['theme'] ) && preg_match( '#^[a-zA-Z0-9-_]+$#', $meta['theme'] ) ) {
641 $chart_meta['theme'] = $meta['theme'];
642 }
643
644 // If the data value is not an array we asume it is JSON encoded (i.e. from Jspreadsheet CE)
645 if ( ! is_array( $chart_meta['data']['sets'] ) && '' != $chart_meta['data']['sets'] ) {
646 $decoded = json_decode( stripslashes( $chart_meta['data']['sets'] ) );
647 $chart_meta['data']['sets'] = is_array( $decoded ) ? $decoded : [];
648 }
649
650 // Validate the data array
651 foreach ( $chart_meta['data']['sets'] as $key => $data ) {
652 $chart_meta['data'][ $key ] = $this->validate_data( $data );
653 }
654
655 // Allow plugins to validate their own custom meta
656 $chart_meta = apply_filters( 'm_chart_validate_post_meta', $chart_meta, $meta );
657
658 return $chart_meta;
659 }
660
661 /**
662 * Runs all of the raw data array values through wp_filter_nohtml_kses
663 *
664 * @param array $data an array of data as passed by the user
665 *
666 * @return array of validated data
667 */
668 function validate_data( $data ) {
669 if ( ! is_array( $data ) ) {
670 return wp_filter_nohtml_kses( $data );
671 }
672
673 foreach ( $data as $key => $value ) {
674 if ( is_array( $value ) ) {
675 $data[ $key ] = $this->validate_data( $value );
676 } else {
677 $value = $value ?? '';
678 $data[ $key ] = wp_filter_nohtml_kses( $value );
679 }
680 }
681
682 return $data;
683 }
684
685
686 /**
687 * Hook to save_post action and save chart related post meta
688 *
689 * @param int $post_id WP post ID of the post
690 */
691 public function save_post( $post_id ) {
692 // We do this in the main class because otherwise it won't get hooked soon enough
693 $this->admin()->save_post( $post_id );
694 }
695
696 /**
697 * Returns a chart
698 *
699 * @param int $post_id WP post ID of the chart you want
700 * @param array $args an array of args
701 *
702 * @return string HTML and Javascript needed to display a chart (or if appropriate an HTML image tag)
703 */
704 public function get_chart( $post_id = null, $args = [] ) {
705 if ( ! $post_id ) {
706 $post_id = get_the_ID();
707 }
708
709 do_action( 'm_chart_get_chart_start', $post_id, $args );
710
711 // Normalize historic usage of 'img' argument
712 if ( isset( $args['img'] ) && 'yes' == $args['img'] ) {
713 $args['show'] = 'image';
714 unset( $args['img'] );
715 }
716
717 $args = wp_parse_args( $args, $this->get_chart_default_args );
718
719 $library = $this->get_post_meta( $post_id, 'library' );
720
721 // If it's not a valid library we don't proceed
722 if ( ! $this->is_valid_library( $library ) ) {
723 return;
724 }
725
726 // Make sure we isntantiate the library so any library specific filters/setup get run
727 $this->library( $library );
728
729 // If they want the table of data we'll return that
730 if ( 'table' == $args['show'] ) {
731 return $this->build_table( $post_id );
732 }
733
734 // If they want the image version or the request is happening from a feed we return the image tag
735 if (
736 'image' == $args['show']
737 || is_feed()
738 || $this->is_amp_endpoint()
739 || apply_filters( 'm_chart_show_image', false, $post_id, $args )
740 ) {
741 $image = $this->get_chart_image( $post_id );
742
743 // Default behavior is to return the full size image but with the width/height values halved
744 // This should result in an image that looks nice on retina or better screens
745 $image['width'] = $image['width'] / 2;
746 $image['height'] = $image['height'] / 2;
747
748 $image = apply_filters( 'm_chart_get_chart_image_tag', $image, $post_id, $args );
749
750 $classes = $this->slug . ' ' . $this->slug . '-' . $post_id;
751
752 if ( $this->is_amp_endpoint() ) {
753 ob_start();
754 ?>
755 <figure class="m-chart-image-figure">
756 <amp-img src="<?php echo esc_url( $image['url'] ); ?>"
757 width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
758 alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
759 class="<?php echo esc_attr( $classes ); ?>"></amp-img>
760 <?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
761 <?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
762 <div class="screen-reader-text">
763 <?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
764 </div>
765 <?php endif; ?>
766 </figure>
767 <?php
768 return ob_get_clean();
769 } else {
770 ob_start();
771 ?>
772 <figure class="m-chart-image-figure">
773 <img src="<?php echo esc_url( $image['url'] ); ?>"
774 width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
775 alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
776 class="<?php echo esc_attr( $classes ); ?>" />
777 <?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
778 <?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
779 <div class="screen-reader-text">
780 <?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
781 </div>
782 <?php endif; ?>
783 </figure>
784 <?php
785 return ob_get_clean();
786 }
787 }
788
789 $settings = $this->get_settings();
790
791 if (
792 ! is_admin()
793 && 'enabled' == $settings['embeds']
794 && ! $this->is_iframe
795 ) {
796 return $this->get_chart_iframe( $post_id, $args );
797 }
798
799 // If we haven't enqueued the right library yet lets do it
800 if ( ! wp_script_is( $library, 'enqueued' ) ) {
801 wp_enqueue_script( $library );
802 wp_enqueue_script( 'chartjs-helper' );
803 }
804
805 // Ship the visually hidden rule the screen reader data table relies on
806 // so it never depends on the active theme defining .screen-reader-text
807 wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version );
808
809 $template = __DIR__ . '/templates/' . $library . '-chart.php';
810
811 ob_start();
812 do_action( 'm_chart_get_chart_begin', $post_id, $args );
813 require apply_filters( 'm_chart_chart_template', $template, $library, $post_id );
814 do_action( 'm_chart_get_chart_end', $post_id, $args );
815 $this->instance++;
816 return ob_get_clean();
817 }
818
819 /**
820 * Returns a charts data as an HTML table
821 *
822 * @param int $post_id WP post ID of the chart you want
823 *
824 * @return string HTML table
825 */
826 public function build_table( $post_id ) {
827 $post = get_post( $post_id );
828 $post_meta = $this->get_post_meta( $post_id );
829 $library = m_chart()->get_post_meta( $post->ID, 'library' );
830
831 $table = '';
832
833 $multiple = count( $post_meta['data']['sets'] ) > 1 ? true : false;
834
835 foreach ( $post_meta['data']['sets'] as $set => $data ) {
836 $classes = $this->slug . '-table ' . $this->slug . '-table-' . $post_id . '-' . $set;
837
838 m_chart()->parse()->parse_data( $data, $post_meta['parse_in'] );
839
840 $template = __DIR__ . '/templates/table.php';
841
842 ob_start();
843 require apply_filters( 'm_chart_table_template', $template, $library, $post->ID );
844 $table .= ob_get_clean();
845 }
846
847 return $table;
848 }
849
850 /**
851 * Return an array of image values for a chart
852 *
853 * @param int $post_id WP post ID of the chart you want an image for
854 *
855 * @return array an array of image values url, width, height, etc...
856 */
857 public function get_chart_image( $post_id ) {
858 $settings = $this->get_settings();
859
860 // If we aren't generating images we'll return false
861 if ( 'default' != $settings['performance'] ) {
862 return false;
863 }
864
865 if ( ! $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true ) ) {
866 return false;
867 }
868
869 if ( ! $thumbnail = wp_get_attachment_image_src( $thumbnail_id, 'full' ) ) {
870 return false;
871 }
872
873 return [
874 'url' => $thumbnail[0],
875 'file' => basename( $thumbnail[0] ),
876 'width' => $thumbnail[1],
877 'height' => $thumbnail[2],
878 'name' => get_the_title( $thumbnail_id ),
879 ];
880 }
881
882 /**
883 * Filter the m_chart_get_chart_image_tag hook and return a plaecholder if appropriate
884 *
885 * @param array|bool an array of image values or false if no image could be found
886 * @param int $post_id WP post ID of the chart you want an image for
887 *
888 * @return array an array of image values url, width, height, etc...
889 */
890 public function m_chart_get_chart_image_tag( $img, $post_id ) {
891 if ( $img ) {
892 return $img;
893 }
894
895 $url = $this->plugin_url . '/components/images/chart-placeholder.png';
896
897 return [
898 'url' => $url,
899 'file' => basename( $url ),
900 'width' => 640,
901 'height' => 480,
902 'name' => get_the_title( $post_id ),
903 ];
904 }
905
906 /**
907 * Filter the the_content hook and return chart code if this is a chart
908 *
909 * @param string $content content from the current post
910 *
911 * @return string original content or chart code
912 */
913 public function the_content( $content ) {
914 // Make sure we're dealing with a chart
915 if ( get_post_type() != $this->slug ) {
916 // We aren't dealing with a chart so we'll just stop here
917 return $content;
918 }
919
920 // Call the get_chart method and let it do it's thing
921 return $this->get_chart();
922 }
923
924 /**
925 * Hook to the m_chart_image_support filter and indicate that Chart.js supports images
926 *
927 * @param string $supports_images yes/no whether the library supports image generation
928 * @param string $library the library in question
929 *
930 * @return string yes/no whether the library supports images
931 */
932 public function m_chart_image_support( $supports_images, $library ) {
933 if ( $library != $this->chart_meta_fields['library'] ) {
934 return $supports_images;
935 }
936
937 return 'yes';
938 }
939
940 /**
941 * Hook to the m_chart_instant_preview_support filter and indicate that Chart.js supports instant previews
942 *
943 * @param string $supports_images yes/no whether the library supports instant previews
944 * @param string $library the library in question
945 *
946 * @return string yes/no whether the library supports instant previews
947 */
948 public function m_chart_instant_preview_support( $supports_instant_preview, $library ) {
949 if ( $library != $this->chart_meta_fields['library'] ) {
950 return $supports_instant_preview;
951 }
952
953 return 'yes';
954 }
955
956 /**
957 * Hook to the m_chart_library_class filter and return the library class if appropriate
958 *
959 * @param string $library_class the library class
960 * @param string $library the library in question
961 *
962 * @return class the library class for this library
963 */
964 public function m_chart_library_class( $library_class, $library ) {
965 // If Chart.js wasn't requested we'll stop here ()
966 if ( $library != $this->chart_meta_fields['library'] ) {
967 return $library_class;
968 }
969
970 if ( ! $this->library_class instanceof M_Chart_Chartjs ) {
971 require_once __DIR__ . '/class-m-chart-chartjs.php';
972 $this->library_class = new M_Chart_Chartjs();
973 }
974
975 return $this->library_class;
976 }
977
978 /**
979 * Return an iframe for a given chart
980 *
981 * @param int $post_id WP post ID of the chart you want
982 * @param array $args an array of args
983 *
984 * @return string HTML needed to display a chart via an iframe
985 */
986 public function get_chart_iframe( $post_id, $args = [] ) {
987 $post_meta = $this->get_post_meta( $post_id );
988
989 $src_url = add_query_arg( $args, get_permalink( $post_id ) . 'embed/' );
990
991 $defer_lazy = 'enabled' === $this->get_settings( 'defer_rendering' );
992
993 ob_start();
994 ?>
995 <iframe id="m-chart-container-<?php echo absint( $post_id ); ?>-<?php echo absint( $this->instance ); ?>"
996 class="m-chart-iframe" width="100%" height="<?php echo absint( $post_meta['height'] + 1 ); ?>"
997 title="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
998 src="<?php echo esc_url( $src_url ); ?>"<?php echo $defer_lazy ? ' loading="lazy"' : ''; ?>></iframe>
999 <?php
1000 if ( 'show' == $args['share'] ) {
1001 unset( $args['share'] );
1002 require apply_filters( 'm_chart_share_template', __DIR__ . '/templates/share.php' );
1003 }
1004 $this->instance++;
1005 return ob_get_clean();
1006 }
1007
1008 /**
1009 * Return a chart via the [m-chart id="x"] short code
1010 *
1011 * Also registered under the legacy [chart] name for backwards compatibility
1012 *
1013 * @param array $args an array of arguments passed by the WP short code API
1014 *
1015 * @return string the chart requested in Javascript or HTML form
1016 */
1017 public function chart_shortcode( $args ) {
1018 $default_args = $this->get_chart_default_args;
1019 $default_args['id'] = '';
1020
1021 $args = shortcode_atts( $default_args, $args );
1022
1023 $post_id = $args['id'];
1024 unset( $args['id'] );
1025
1026 // Did we get what looks like a chart ID?
1027 if ( ! is_numeric( $post_id ) ) {
1028 return;
1029 }
1030
1031 // Make sure the chart actually exists and that it's a chart so we don't fatal later
1032 if ( $this->slug != get_post_type( $post_id ) ) {
1033 return;
1034 }
1035
1036 // Is it published?
1037 if ( 'publish' != get_post_status( $post_id ) ) {
1038 return;
1039 }
1040
1041 return $this->get_chart( $post_id, $args );
1042 }
1043
1044 /**
1045 * Helper function that prevents issues with stripslashes and unicode characters
1046 * stripslashes will strip off the slash before unicode characters which is sucky, this prevents that
1047 *
1048 * @param string $string a string that may have unicode characters as well as unecessary escaping
1049 *
1050 * @return string a string with any unecessary escaping removed
1051 */
1052 public function unicode_aware_stripslashes( $string ) {
1053 return stripslashes( preg_replace( '#\\\u[a-fA-F0-9]{4}#', '\\\\$0', $string ) );
1054 }
1055
1056 /**
1057 * Helper function that returns the chart unit terms
1058 *
1059 * @return array an array of generated and/or compiled unit terms
1060 */
1061 public function get_unit_terms() {
1062 $terms = get_terms( $this->slug . '-units', [ 'hide_empty' => false ] );
1063
1064 if ( empty( $terms ) ) {
1065 $terms = $this->generate_unit_terms();
1066 }
1067
1068 return $this->compile_unit_terms( $terms );
1069 }
1070
1071 /**
1072 * Helper function that compiles the chart unit terms for use
1073 *
1074 * @param array an array of unit terms
1075 *
1076 * @return array an array of compiled unit terms
1077 */
1078 public function compile_unit_terms( $terms ) {
1079 $compiled_terms = [];
1080 $parents = [];
1081
1082 foreach ( $terms as $unit ) {
1083 if ( 0 == $unit->parent ) {
1084 $parents[ $unit->term_id ] = $unit->name;
1085 }
1086 }
1087
1088 foreach ( $terms as $unit ) {
1089 if ( 0 != $unit->parent && isset( $parents[ $unit->parent ] ) ) {
1090 $compiled_terms[ $parents[ $unit->parent ] ][] = $unit;
1091 }
1092 }
1093
1094 ksort( $compiled_terms );
1095
1096 return $compiled_terms;
1097 }
1098
1099 /**
1100 * Helper function that populates the unit terms taxonomy with a default set of terms
1101 *
1102 * @return array an array of the newly generated unit terms
1103 */
1104 public function generate_unit_terms() {
1105 // Load the default terms array
1106 $default_terms = require __DIR__ . '/array-default-unit-terms.php';
1107
1108 $terms = [];
1109
1110 foreach ( $default_terms as $parent_term => $child_terms ) {
1111 $parent = (object) wp_insert_term( $parent_term, $this->slug . '-units' );
1112 $terms[] = get_term( $parent->term_id, $this->slug . '-units' );
1113
1114 foreach ( $child_terms as $child_term ) {
1115 $term = (object) wp_insert_term( $child_term, $this->slug . '-units', [ 'parent' => $parent->term_id ] );
1116 $terms[] = get_term( $term->term_id, $this->slug . '-units' );
1117 }
1118 }
1119
1120 return $terms;
1121 }
1122
1123 /**
1124 * Looks for the embed endpoint and serves up the requested chart if appropriate
1125 */
1126 public function template_redirect() {
1127 global $wp_query;
1128
1129 // Make sure this is a chart with the embed endpoint in the URL
1130 if ( ! isset( $wp_query->query['post_type'] ) || ! isset( $wp_query->query['embed'] ) || 'm-chart' != $wp_query->query['post_type'] ) {
1131 return;
1132 }
1133
1134 $post = get_post();
1135
1136 if ( ! $post ) {
1137 wp_die(
1138 esc_html__( 'The chart could not be found', 'm-chart' ),
1139 esc_html__( 'Chart not found', 'm-chart' ),
1140 [ 'response' => 404 ]
1141 );
1142 }
1143
1144 $settings = $this->get_settings();
1145
1146 if ( 'enabled' != $settings['embeds'] ) {
1147 wp_die(
1148 esc_html__( 'Embeds of this type are not enabled', 'm-chart' ),
1149 esc_html__( 'Embeds disabled', 'm-chart' ),
1150 [ 'response' => 403 ]
1151 );
1152 exit;
1153 }
1154
1155 $this->is_iframe = true;
1156
1157 $scripts = [
1158 'jquery',
1159 $this->get_post_meta( $post->ID, 'library' ),
1160 ];
1161
1162 unset( $_GET['action'], $_GET['share'] );
1163
1164 // This prevents issues when embedding with outside sites
1165 header_remove( 'X-Frame-Options' );
1166
1167 status_header( 200 );
1168
1169 require __DIR__ . '/templates/iframe.php';
1170 exit;
1171 }
1172
1173 /**
1174 * Hook to the m_chart_update_post_meta action and call the required library specific function
1175 *
1176 * @param int $post_id WP post ID of the post you want chart args for
1177 * @param array $parsed_meta the parsed chart meta passed by the action hook
1178 */
1179 public function m_chart_update_post_meta( $post_id, $parsed_meta ) {
1180 $library = $this->library( $parsed_meta['library'] );
1181
1182 // The chart's library plugin may be inactive (e.g. Highcharts charts after the
1183 // extension was removed) in which case there's no library object to notify
1184 if ( ! is_object( $library ) || ! method_exists( $library, 'm_chart_update_post_meta' ) ) {
1185 return;
1186 }
1187
1188 $library->m_chart_update_post_meta( $post_id, $parsed_meta );
1189 }
1190
1191 /**
1192 * If the passed library is valid return TRUE otherwise FALSE
1193 *
1194 * @param string library string (i.e. 'highcharts')
1195 *
1196 * @return bool
1197 */
1198 public function is_valid_library( $library ) {
1199 $libraries = $this->get_libraries();
1200
1201 if ( ! isset( $libraries[ $library ] ) ) {
1202 return false;
1203 }
1204
1205 return true;
1206 }
1207
1208 /**
1209 * If current page is an AMP page returns true
1210 *
1211 * @return bool
1212 */
1213 public function is_amp_endpoint() {
1214 if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
1215 return true;
1216 }
1217
1218 return false;
1219 }
1220
1221 /**
1222 * Return the M Chart settings
1223 *
1224 * @return array current settings
1225 */
1226 public function get_settings( $setting = false ) {
1227 // Memoize the resolved settings, get_post_meta() calls this on every invocation
1228 // and the option + filter pipeline doesn't need to re-run within a request
1229 // Invalidated via reset_settings() when the settings are saved
1230 if ( isset( $this->resolved_settings ) ) {
1231 $settings = $this->resolved_settings;
1232 } else {
1233 // Allow third party libraries to modify the default settings
1234 $default_settings = apply_filters( 'm_chart_default_settings', $this->settings );
1235
1236 $settings = (array) get_option( $this->slug, $default_settings );
1237 $settings = wp_parse_args( $settings, $default_settings );
1238
1239 // Make sure the set library is still valid
1240 if ( ! $this->is_valid_library( $settings['library'] ) ) {
1241 $settings['library'] = 'chartjs';
1242 }
1243
1244 $settings = apply_filters( 'm_chart_get_settings', $settings );
1245
1246 $this->resolved_settings = $settings;
1247 }
1248
1249 if ( $setting && isset( $settings[ $setting ] ) ) {
1250 return $settings[ $setting ];
1251 } elseif ( $setting ) {
1252 return null;
1253 }
1254
1255 return $settings;
1256 }
1257
1258 /**
1259 * Drop the memoized settings so the next get_settings() call re-resolves them
1260 *
1261 * Called after the settings option is updated
1262 */
1263 public function reset_settings() {
1264 $this->resolved_settings = null;
1265 }
1266
1267 /**
1268 * Return an array of available charting libraries
1269 *
1270 * @return array current settings
1271 */
1272 public function get_libraries() {
1273 return apply_filters( 'm_chart_get_libraries', $this->libraries );
1274 }
1275
1276 /**
1277 * Return the chart types that support multiple data sets (multi-sheet tab bar)
1278 *
1279 * This is the single authoritative source for this list
1280 * The value is passed to window.m_chart_admin.multi_sheet_types so the React UI always stays in sync with PHP
1281 * Filtered via 'm_chart_multi_sheet_types' so library plugins can add or remove types
1282 *
1283 * @return array
1284 */
1285 public function get_multi_sheet_types() {
1286 return apply_filters( 'm_chart_multi_sheet_types', [
1287 'scatter',
1288 'bubble',
1289 'radar',
1290 'radar-area',
1291 'boxplot',
1292 'violin',
1293 ] );
1294 }
1295
1296 /**
1297 * Return the current library
1298 *
1299 * @return string current library
1300 */
1301 public function get_library() {
1302 if ( isset( $_GET['library'] ) && $this->is_valid_library( $_GET['library'] ) ) {
1303 return $_GET['library'];
1304 }
1305
1306 return $this->get_settings( 'library' );
1307 }
1308
1309 /**
1310 * Return the locale array
1311 *
1312 * When PHP's Intl extension is available, labels are generated in each locale's native language using Locale::getDisplayName()
1313 * Falls back to the English labels in array-locale-codes.php otherwise
1314 *
1315 * @return array locales as used by Intl.NumberFormat
1316 */
1317 public function get_locales() {
1318 $locales = require __DIR__ . '/array-locale-codes.php';
1319
1320 if ( ! class_exists( 'Locale' ) ) {
1321 return $locales;
1322 }
1323
1324 foreach ( $locales as $code => $english_label ) {
1325 $native = Locale::getDisplayName( $code, $code );
1326
1327 if ( $native ) {
1328 $locales[ $code ] = esc_html( $native );
1329 }
1330 }
1331
1332 return $locales;
1333 }
1334
1335 /**
1336 * Return a recursively merged array out of the two given
1337 *
1338 * @param array a multi dimensional array that will be merged into
1339 * @param array a multi dimensional array that will be merged recursively into the first one
1340 *
1341 * @return array the merged array
1342 */
1343 public function array_merge_recursive( &$a, $b ) {
1344 foreach ( $b as $child => $value ) {
1345 if ( isset( $a[ $child ] ) ) {
1346 // New value exists so we'll need to move a level down
1347 if ( is_array( $a[ $child ] ) && is_array( $value ) ) {
1348 $this->array_merge_recursive( $a[ $child ], $value );
1349 } else {
1350 // New value is not an array so we override the old value with the new one
1351 $a[ $child ] = $value;
1352 }
1353 } else {
1354 // New value doesn't exist so we can just add it
1355 $a[ $child ] = $value;
1356 }
1357 }
1358
1359 return $a;
1360 }
1361
1362 /**
1363 * Do things needed for version 1.7
1364 */
1365 public function upgrade_to_1_7() {
1366 // Get all charts
1367 $charts = get_posts(
1368 [
1369 'post_type' => m_chart()->slug,
1370 'posts_per_page' => -1,
1371 'post_status' => 'any',
1372 'tax_query' => [
1373 [
1374 'taxonomy' => 'post_tag',
1375 'field' => 'slug',
1376 'terms' => 'highcharts',
1377 'operator' => 'NOT IN',
1378 ],
1379 ],
1380 ]
1381 );
1382
1383 // Add highcharts post_tag to all charts
1384 foreach ( $charts as $chart ) {
1385 wp_set_object_terms( $chart->ID, 'highcharts', 'post_tag' );
1386 }
1387
1388 // Update version
1389 update_site_option( 'm_chart_version', '1.7' );
1390 }
1391
1392 /**
1393 * Do things needed for version 1.7.4
1394 */
1395 public function upgrade_to_1_7_4() {
1396 // Get all charts tagged with highcharts
1397 $highcharts_charts = get_posts(
1398 [
1399 'post_type' => m_chart()->slug,
1400 'posts_per_page' => -1,
1401 'post_status' => 'any',
1402 'tax_query' => [
1403 [
1404 'taxonomy' => 'post_tag',
1405 'field' => 'slug',
1406 'terms' => 'highcharts',
1407 'operator' => 'IN',
1408 ],
1409 ],
1410 ]
1411 );
1412
1413 foreach ( $highcharts_charts as $chart ) {
1414 // Add highcharts as a term to the m-chart-library taxonomy
1415 wp_set_object_terms( $chart->ID, 'highcharts', m_chart()->slug . '-library' );
1416 // Remove highcharts as a term from the post_tag taxonomy
1417 wp_remove_object_terms( $chart->ID, 'highcharts', 'post_tag' );
1418 }
1419
1420 // Get all charts tagged with chartjs
1421 $chartjs_charts = get_posts(
1422 [
1423 'post_type' => m_chart()->slug,
1424 'posts_per_page' => -1,
1425 'post_status' => 'any',
1426 'tax_query' => [
1427 [
1428 'taxonomy' => 'post_tag',
1429 'field' => 'slug',
1430 'terms' => 'chartjs',
1431 'operator' => 'IN',
1432 ],
1433 ],
1434 ]
1435 );
1436
1437 foreach ( $chartjs_charts as $chart ) {
1438 // Add chartjs as a term to the m-chart-library taxonomy
1439 wp_set_object_terms( $chart->ID, 'chartjs', m_chart()->slug . '-library' );
1440 // Remove chartjs as a term from the post_tag taxonomy
1441 wp_remove_object_terms( $chart->ID, 'chartjs', 'post_tag' );
1442 }
1443
1444 // Update version
1445 update_site_option( 'm_chart_version', '1.7.4' );
1446 }
1447 }
1448
1449 /**
1450 * Plugin object accessor
1451 */
1452 function m_chart() {
1453 global $m_chart;
1454
1455 if ( ! $m_chart instanceof M_Chart ) {
1456 $m_chart = new M_Chart();
1457 }
1458
1459 return $m_chart;
1460 }
1461
1462 /**
1463 * Freemius SDK accessor
1464 *
1465 * Thin global wrapper that delegates to the core class so Freemius ecosystem code and conventions keep working
1466 * The actual configuration lives in M_Chart::freemius()
1467 *
1468 * @return Freemius the Freemius SDK instance for M Chart
1469 */
1470 function m_chart_fs() {
1471 return m_chart()->freemius();
1472 }
1473