PluginProbe
M Chart / trunk
M Chart vtrunk
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 trunk, at components/class-m-chart.php

1,484 lines 46.6 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.2';
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 // Priority 0 so is_iframe is set before anything else reads it
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 // Priority 9 so this runs before any default-priority filters other plugins add
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 * Return the plugin URL built from the home/admin URL rather than plugins_url() alone
442 *
443 * Exists because WP VIP's CDN rewrote plugin URLs in a way that broke embedded SVG handling (e.g. watermarks)
444 * Building the URL from the current host circumvents that
445 *
446 * @param string $path optional additional path to append (e.g. components)
447 *
448 * @return string URL to the plugin directory with path if parameter was passed
449 */
450 public function plugin_url( $path = '' ) {
451 if ( is_admin() ) {
452 $url_base = parse_url( admin_url() );
453 } else {
454 $url_base = parse_url( home_url() );
455 }
456
457 $url_path = parse_url( plugins_url( $path, __DIR__ ) );
458
459 // Honor a non-standard port if the base URL has one
460 $port = '';
461
462 if ( isset( $url_base['port'] ) && 80 != $url_base['port'] ) {
463 $port = ':' . $url_base['port'];
464 }
465
466 $scheme = $url_base['scheme'] ?? 'https';
467 $host = $url_base['host'] ?? '';
468 $path = $url_path['path'] ?? '';
469 $query = $url_path['query'] ?? '';
470
471 return $scheme . '://' . $host . $port . preg_replace( '#/$#', '', $path ) . ( '' === $query ? '' : '?' . $query );
472 }
473
474 /**
475 * Get chart post meta
476 *
477 * @param int $post_id WP post ID of the post you want post meta from
478 * @param string $field optional field to be returned instead of all post meta
479 *
480 * @return array $post_meta the meta for a given post ID
481 */
482 public function get_post_meta( $post_id, $field = false ) {
483 $raw_post_meta = get_post_meta( $post_id, $this->slug, true );
484
485 $post_meta = $raw_post_meta;
486
487 $defaults = $this->chart_meta_fields;
488
489 // Make sure the correct library is set in the default chart meta fields
490 if ( ! $post_meta ) {
491 // get_current_screen() is only defined in admin context
492 // This guards the call so REST and front-end contexts don't fatal
493 $current_screen = is_admin() && function_exists( 'get_current_screen' )
494 ? get_current_screen()
495 : null;
496
497 // When adding a new chart with a library specified in the query string, use that library
498 if (
499 is_admin()
500 && null != $current_screen
501 && 'post' == $current_screen->base
502 && 'add' == $current_screen->action
503 && isset( $_GET['library'] )
504 && $this->is_valid_library( $_GET['library'] )
505 ) {
506 $defaults['library'] = $_GET['library'];
507 } else {
508 $defaults['library'] = $this->get_library();
509 }
510 }
511
512 // Default chart height comes from the plugin settings so new charts honor the site default
513 // Saved charts already have a height so wp_parse_args leaves theirs alone
514 $defaults['height'] = (int) $this->get_settings( 'default_height' );
515
516 $post_meta = wp_parse_args( $post_meta, $defaults );
517
518 // Theme default is based off of an option so we'll handle that here
519 if ( ! isset( $post_meta['theme'] ) ) {
520 $settings = $this->get_settings();
521 $post_meta['theme'] = $settings['default_theme'];
522 }
523
524 // If there's no subtitle set we'll set an empty one
525 if ( ! isset( $post_meta['subtitle'] ) ) {
526 $post_meta['subtitle'] = '';
527 }
528
529 // If there's no y min value set we'll set it to 0
530 if ( ! isset( $post_meta['y_min_value'] ) ) {
531 $post_meta['y_min_value'] = 0;
532 }
533
534 // If the data has the old legacy format we need to update it
535 if ( isset( $post_meta['data'] ) && ! isset( $post_meta['data']['sets'] ) ) {
536 $data = $post_meta['data'];
537 $post_meta['data'] = [];
538 $post_meta['data']['sets'][] = $data;
539 }
540
541 // Some very old charts stored a single sheet's rows directly in the sets value
542 // A valid sets value is an array of sheets where each sheet is an array of row arrays
543 if ( isset( $post_meta['data']['sets'] ) && is_array( $post_meta['data']['sets'] ) ) {
544 $first_sheet = reset( $post_meta['data']['sets'] );
545
546 if ( is_array( $first_sheet ) && [] !== $first_sheet && ! is_array( reset( $first_sheet ) ) ) {
547 $post_meta['data'] = [ 'sets' => [ array_values( $post_meta['data']['sets'] ) ] ];
548 }
549 }
550
551 // If there's no set_names value set we'll set it to an empty array
552 if ( ! isset( $post_meta['set_names'] ) ) {
553 $post_meta['set_names'] = [];
554 }
555
556 $post_meta = apply_filters( 'm_chart_get_post_meta', $post_meta, $raw_post_meta, $post_id );
557
558 if ( $field && isset( $post_meta[ $field ] ) ) {
559 return $post_meta[ $field ];
560 } elseif ( $field ) {
561 return null;
562 }
563
564 return $post_meta;
565 }
566
567 /**
568 * Update the post meta and set unit terms if appropriate
569 *
570 * @param int $post_id WP post ID of the post you want to attach post meta to
571 * @param array $meta an array of the post meta you want to attach to the post
572 */
573 public function update_post_meta( $post_id, $meta ) {
574 // Make sure the meta is formatted correctly and validated
575 $parsed_meta = $this->validate_post_meta( $meta );
576
577 // Set unit terms
578 $terms = [];
579
580 if ( '' != $parsed_meta['y_units'] ) {
581 $terms[] = $parsed_meta['y_units'];
582 }
583
584 if ( '' != $parsed_meta['x_units'] ) {
585 $terms[] = $parsed_meta['x_units'];
586 }
587
588 wp_set_object_terms( $post_id, $terms, $this->slug . '-units' );
589
590 // Set library as a post_tag
591 wp_set_object_terms( $post_id, $parsed_meta['library'], $this->slug . '-library' );
592
593 // Save meta to the post
594 update_post_meta( $post_id, $this->slug, $parsed_meta );
595 do_action( 'm_chart_update_post_meta', $post_id, $parsed_meta, $meta );
596 }
597
598 /**
599 * Parses a $meta array and returns a cleaned and validated array
600 *
601 * @param array $meta an array of the post meta you want to attach to the post
602 *
603 * @return array of cleaned/validated post meta
604 */
605 public function validate_post_meta( $meta ) {
606 // Need to set checkboxes before checking or they can't be deselected
607 $chart_meta['labels'] = false;
608 $chart_meta['y_min'] = false;
609 $chart_meta['legend'] = false;
610 $chart_meta['data_point_colors'] = false;
611 $chart_meta['mean_point'] = false;
612 $chart_meta['sample_points'] = false;
613 $chart_meta['constrain_y_axis'] = false;
614 $chart_meta['include_source'] = false;
615
616 // Filter values so we know the data is clean
617 foreach ( $this->chart_meta_fields as $field => $default ) {
618 if ( isset( $meta[ $field ] ) ) {
619 if ( 'source_url' == $field && '' != $meta[ $field ] ) {
620 $chart_meta[ $field ] = esc_url_raw( $meta[ $field ] );
621 } elseif ( 'data' == $field ) {
622 $chart_meta[ $field ]['sets'] = $meta[ $field ];
623 } elseif ( 'set_names' == $field ) {
624 $chart_meta[ $field ] = array_values( $meta[ $field ] );
625 } elseif ( in_array( $field, [ 'labels', 'y_min', 'legend', 'data_point_colors', 'mean_point', 'sample_points', 'constrain_y_axis', 'include_source' ] ) ) {
626 $chart_meta[ $field ] = (bool) $meta[ $field ];
627 } elseif ( 'height' == $field ) {
628 $chart_meta[ $field ] = absint( $meta[ $field ] );
629
630 if ( $chart_meta[ $field ] > 1500 ) {
631 $chart_meta[ $field ] = 1500;
632 } elseif ( $chart_meta[ $field ] < 300 ) {
633 $chart_meta[ $field ] = 300;
634 }
635 } elseif ( 'y_min_value' == $field ) {
636 $chart_meta[ $field ] = floatval( $meta[ $field ] );
637 } else {
638 $chart_meta[ $field ] = wp_filter_nohtml_kses( $meta[ $field ] );
639 }
640 } elseif ( ! isset( $chart_meta[ $field ] ) ) {
641 // Fall back on the default value if there wasn't one in the given meta
642 // Height honors the default_height setting so the save path agrees with get_post_meta()
643 if ( 'height' === $field ) {
644 $chart_meta[ $field ] = (int) $this->get_settings( 'default_height' );
645 } else {
646 $chart_meta[ $field ] = $default;
647 }
648 }
649 }
650
651 // Theme isn't included in chart_meta_fields so it's validated here
652 if ( isset( $meta['theme'] ) && preg_match( '#^[a-zA-Z0-9-_]+$#', $meta['theme'] ) ) {
653 $chart_meta['theme'] = $meta['theme'];
654 }
655
656 // If the data value is not an array we assume it is JSON encoded (i.e. from Jspreadsheet CE)
657 if ( ! is_array( $chart_meta['data']['sets'] ) && '' != $chart_meta['data']['sets'] ) {
658 $decoded = json_decode( stripslashes( $chart_meta['data']['sets'] ) );
659 $chart_meta['data']['sets'] = is_array( $decoded ) ? $decoded : [];
660 }
661
662 // Validate the data array
663 foreach ( $chart_meta['data']['sets'] as $key => $data ) {
664 $chart_meta['data'][ $key ] = $this->validate_data( $data );
665 }
666
667 // Allow plugins to validate their own custom meta
668 $chart_meta = apply_filters( 'm_chart_validate_post_meta', $chart_meta, $meta );
669
670 return $chart_meta;
671 }
672
673 /**
674 * Runs all of the raw data array values through wp_filter_nohtml_kses
675 *
676 * @param array $data an array of data as passed by the user
677 *
678 * @return array of validated data
679 */
680 function validate_data( $data ) {
681 if ( ! is_array( $data ) ) {
682 return wp_filter_nohtml_kses( $data );
683 }
684
685 foreach ( $data as $key => $value ) {
686 if ( is_array( $value ) ) {
687 $data[ $key ] = $this->validate_data( $value );
688 } else {
689 $value = $value ?? '';
690 $data[ $key ] = wp_filter_nohtml_kses( $value );
691 }
692 }
693
694 return $data;
695 }
696
697
698 /**
699 * Hook to save_post action and save chart related post meta
700 *
701 * @param int $post_id WP post ID of the post
702 */
703 public function save_post( $post_id ) {
704 // save_post is hooked here because the admin class is lazy-loaded and wouldn't be instantiated in time to hook it itself
705 $this->admin()->save_post( $post_id );
706 }
707
708 /**
709 * Returns a chart
710 *
711 * @param int $post_id WP post ID of the chart you want
712 * @param array $args an array of args
713 *
714 * @return string HTML and Javascript needed to display a chart (or if appropriate an HTML image tag)
715 */
716 public function get_chart( $post_id = null, $args = [] ) {
717 if ( ! $post_id ) {
718 $post_id = get_the_ID();
719 }
720
721 do_action( 'm_chart_get_chart_start', $post_id, $args );
722
723 // Normalize historic usage of 'img' argument
724 if ( isset( $args['img'] ) && 'yes' == $args['img'] ) {
725 $args['show'] = 'image';
726 unset( $args['img'] );
727 }
728
729 $args = wp_parse_args( $args, $this->get_chart_default_args );
730
731 $library = $this->get_post_meta( $post_id, 'library' );
732
733 // If it's not a valid library we don't proceed
734 if ( ! $this->is_valid_library( $library ) ) {
735 return;
736 }
737
738 // Make sure we instantiate the library so any library specific filters/setup get run
739 $this->library( $library );
740
741 // If they want the table of data we'll return that
742 if ( 'table' == $args['show'] ) {
743 return $this->build_table( $post_id );
744 }
745
746 // If they want the image version or the request is happening from a feed we return the image tag
747 if (
748 'image' == $args['show']
749 || is_feed()
750 || $this->is_amp_endpoint()
751 || apply_filters( 'm_chart_show_image', false, $post_id, $args )
752 ) {
753 $image = $this->get_chart_image( $post_id );
754
755 // Default behavior is to return the full size image but with the width/height values halved
756 // This should result in an image that looks nice on retina or better screens
757 $image['width'] = $image['width'] / 2;
758 $image['height'] = $image['height'] / 2;
759
760 $image = apply_filters( 'm_chart_get_chart_image_tag', $image, $post_id, $args );
761
762 $classes = $this->slug . ' ' . $this->slug . '-' . $post_id;
763
764 if ( $this->is_amp_endpoint() ) {
765 ob_start();
766 ?>
767 <figure class="m-chart-image-figure">
768 <amp-img src="<?php echo esc_url( $image['url'] ); ?>"
769 width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
770 alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
771 class="<?php echo esc_attr( $classes ); ?>"></amp-img>
772 <?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
773 <?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
774 <div class="screen-reader-text">
775 <?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
776 </div>
777 <?php endif; ?>
778 </figure>
779 <?php
780 return ob_get_clean();
781 } else {
782 ob_start();
783 ?>
784 <figure class="m-chart-image-figure">
785 <img src="<?php echo esc_url( $image['url'] ); ?>"
786 width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
787 alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
788 class="<?php echo esc_attr( $classes ); ?>" />
789 <?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
790 <?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
791 <div class="screen-reader-text">
792 <?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
793 </div>
794 <?php endif; ?>
795 </figure>
796 <?php
797 return ob_get_clean();
798 }
799 }
800
801 $settings = $this->get_settings();
802
803 if (
804 ! is_admin()
805 && 'enabled' == $settings['embeds']
806 && ! $this->is_iframe
807 ) {
808 return $this->get_chart_iframe( $post_id, $args );
809 }
810
811 // If we haven't enqueued the right library yet lets do it
812 if ( ! wp_script_is( $library, 'enqueued' ) ) {
813 wp_enqueue_script( $library );
814 wp_enqueue_script( 'chartjs-helper' );
815 }
816
817 // Ship the visually hidden rule the screen reader data table relies on
818 // so it never depends on the active theme defining .screen-reader-text
819 wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version );
820
821 $template = __DIR__ . '/templates/' . $library . '-chart.php';
822
823 ob_start();
824 do_action( 'm_chart_get_chart_begin', $post_id, $args );
825 require apply_filters( 'm_chart_chart_template', $template, $library, $post_id );
826 do_action( 'm_chart_get_chart_end', $post_id, $args );
827 $this->instance++;
828 return ob_get_clean();
829 }
830
831 /**
832 * Returns a charts data as an HTML table
833 *
834 * @param int $post_id WP post ID of the chart you want
835 *
836 * @return string HTML table
837 */
838 public function build_table( $post_id ) {
839 $post = get_post( $post_id );
840 $post_meta = $this->get_post_meta( $post_id );
841 $library = m_chart()->get_post_meta( $post->ID, 'library' );
842
843 $table = '';
844
845 $multiple = count( $post_meta['data']['sets'] ) > 1 ? true : false;
846
847 foreach ( $post_meta['data']['sets'] as $set => $data ) {
848 $classes = $this->slug . '-table ' . $this->slug . '-table-' . $post_id . '-' . $set;
849
850 m_chart()->parse()->parse_data( $data, $post_meta['parse_in'] );
851
852 $template = __DIR__ . '/templates/table.php';
853
854 ob_start();
855 require apply_filters( 'm_chart_table_template', $template, $library, $post->ID );
856 $table .= ob_get_clean();
857 }
858
859 return $table;
860 }
861
862 /**
863 * Return an array of image values for a chart
864 *
865 * @param int $post_id WP post ID of the chart you want an image for
866 *
867 * @return array an array of image values url, width, height, etc...
868 */
869 public function get_chart_image( $post_id ) {
870 $settings = $this->get_settings();
871
872 // If we aren't generating images we'll return false
873 if ( 'default' != $settings['performance'] ) {
874 return false;
875 }
876
877 if ( ! $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true ) ) {
878 return false;
879 }
880
881 if ( ! $thumbnail = wp_get_attachment_image_src( $thumbnail_id, 'full' ) ) {
882 return false;
883 }
884
885 return [
886 'url' => $thumbnail[0],
887 'file' => basename( $thumbnail[0] ),
888 'width' => $thumbnail[1],
889 'height' => $thumbnail[2],
890 'name' => get_the_title( $thumbnail_id ),
891 ];
892 }
893
894 /**
895 * Filter the m_chart_get_chart_image_tag hook and return a placeholder if appropriate
896 *
897 * @param array|bool an array of image values or false if no image could be found
898 * @param int $post_id WP post ID of the chart you want an image for
899 *
900 * @return array an array of image values url, width, height, etc...
901 */
902 public function m_chart_get_chart_image_tag( $img, $post_id ) {
903 if ( $img ) {
904 return $img;
905 }
906
907 $url = $this->plugin_url . '/components/images/chart-placeholder.png';
908
909 return [
910 'url' => $url,
911 'file' => basename( $url ),
912 'width' => 640,
913 'height' => 480,
914 'name' => get_the_title( $post_id ),
915 ];
916 }
917
918 /**
919 * Filter the the_content hook and return chart code if this is a chart
920 *
921 * @param string $content content from the current post
922 *
923 * @return string original content or chart code
924 */
925 public function the_content( $content ) {
926 // Make sure we're dealing with a chart
927 if ( get_post_type() != $this->slug ) {
928 return $content;
929 }
930
931 return $this->get_chart();
932 }
933
934 /**
935 * Hook to the m_chart_image_support filter and indicate that Chart.js supports images
936 *
937 * @param string $supports_images yes/no whether the library supports image generation
938 * @param string $library the library in question
939 *
940 * @return string yes/no whether the library supports images
941 */
942 public function m_chart_image_support( $supports_images, $library ) {
943 if ( $library != $this->chart_meta_fields['library'] ) {
944 return $supports_images;
945 }
946
947 return 'yes';
948 }
949
950 /**
951 * Hook to the m_chart_instant_preview_support filter and indicate that Chart.js supports instant previews
952 *
953 * @param string $supports_images yes/no whether the library supports instant previews
954 * @param string $library the library in question
955 *
956 * @return string yes/no whether the library supports instant previews
957 */
958 public function m_chart_instant_preview_support( $supports_instant_preview, $library ) {
959 if ( $library != $this->chart_meta_fields['library'] ) {
960 return $supports_instant_preview;
961 }
962
963 return 'yes';
964 }
965
966 /**
967 * Hook to the m_chart_library_class filter and return the library class if appropriate
968 *
969 * @param string $library_class the library class
970 * @param string $library the library in question
971 *
972 * @return class the library class for this library
973 */
974 public function m_chart_library_class( $library_class, $library ) {
975 // If Chart.js wasn't requested we'll stop here
976 if ( $library != $this->chart_meta_fields['library'] ) {
977 return $library_class;
978 }
979
980 if ( ! $this->library_class instanceof M_Chart_Chartjs ) {
981 require_once __DIR__ . '/class-m-chart-chartjs.php';
982 $this->library_class = new M_Chart_Chartjs();
983 }
984
985 return $this->library_class;
986 }
987
988 /**
989 * Return an iframe for a given chart
990 *
991 * @param int $post_id WP post ID of the chart you want
992 * @param array $args an array of args
993 *
994 * @return string HTML needed to display a chart via an iframe
995 */
996 public function get_chart_iframe( $post_id, $args = [] ) {
997 $post_meta = $this->get_post_meta( $post_id );
998
999 $src_url = add_query_arg( $args, get_permalink( $post_id ) . 'embed/' );
1000
1001 $defer_lazy = 'enabled' === $this->get_settings( 'defer_rendering' );
1002
1003 ob_start();
1004 ?>
1005 <iframe id="m-chart-container-<?php echo absint( $post_id ); ?>-<?php echo absint( $this->instance ); ?>"
1006 class="m-chart-iframe" width="100%" height="<?php echo absint( $post_meta['height'] + 1 ); ?>"
1007 title="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
1008 src="<?php echo esc_url( $src_url ); ?>"<?php echo $defer_lazy ? ' loading="lazy"' : ''; ?>></iframe>
1009 <?php
1010 if ( 'show' == $args['share'] ) {
1011 unset( $args['share'] );
1012 require apply_filters( 'm_chart_share_template', __DIR__ . '/templates/share.php' );
1013 }
1014 $this->instance++;
1015 return ob_get_clean();
1016 }
1017
1018 /**
1019 * Return a chart via the [m-chart id="x"] short code
1020 *
1021 * Also registered under the legacy [chart] name for backwards compatibility
1022 *
1023 * @param array $args an array of arguments passed by the WP short code API
1024 *
1025 * @return string the chart requested in Javascript or HTML form
1026 */
1027 public function chart_shortcode( $args ) {
1028 $default_args = $this->get_chart_default_args;
1029 $default_args['id'] = '';
1030
1031 $args = shortcode_atts( $default_args, $args );
1032
1033 $post_id = $args['id'];
1034 unset( $args['id'] );
1035
1036 // Did we get what looks like a chart ID?
1037 if ( ! is_numeric( $post_id ) ) {
1038 return;
1039 }
1040
1041 // Make sure the chart actually exists and that it's a chart so we don't fatal later
1042 if ( $this->slug != get_post_type( $post_id ) ) {
1043 return;
1044 }
1045
1046 // Is it published?
1047 if ( 'publish' != get_post_status( $post_id ) ) {
1048 return;
1049 }
1050
1051 return $this->get_chart( $post_id, $args );
1052 }
1053
1054 /**
1055 * Helper function that prevents issues with stripslashes and unicode characters
1056 * stripslashes() strips the backslash before \uXXXX sequences
1057 * This re-escapes those sequences first so unicode survives the strip
1058 *
1059 * @param string $string a string that may have unicode characters as well as unnecessary escaping
1060 *
1061 * @return string a string with any unnecessary escaping removed
1062 */
1063 public function unicode_aware_stripslashes( $string ) {
1064 return stripslashes( preg_replace( '#\\\u[a-fA-F0-9]{4}#', '\\\\$0', $string ) );
1065 }
1066
1067 /**
1068 * Helper function that returns the chart unit terms
1069 *
1070 * @return array an array of generated and/or compiled unit terms
1071 */
1072 public function get_unit_terms() {
1073 $terms = get_terms( $this->slug . '-units', [ 'hide_empty' => false ] );
1074
1075 if ( empty( $terms ) ) {
1076 $terms = $this->generate_unit_terms();
1077 }
1078
1079 return $this->compile_unit_terms( $terms );
1080 }
1081
1082 /**
1083 * Helper function that compiles the chart unit terms for use
1084 *
1085 * @param array an array of unit terms
1086 *
1087 * @return array an array of compiled unit terms
1088 */
1089 public function compile_unit_terms( $terms ) {
1090 $compiled_terms = [];
1091 $parents = [];
1092
1093 foreach ( $terms as $unit ) {
1094 if ( 0 == $unit->parent ) {
1095 $parents[ $unit->term_id ] = $unit->name;
1096 }
1097 }
1098
1099 foreach ( $terms as $unit ) {
1100 if ( 0 != $unit->parent && isset( $parents[ $unit->parent ] ) ) {
1101 $compiled_terms[ $parents[ $unit->parent ] ][] = $unit;
1102 }
1103 }
1104
1105 ksort( $compiled_terms );
1106
1107 return $compiled_terms;
1108 }
1109
1110 /**
1111 * Helper function that populates the unit terms taxonomy with a default set of terms
1112 *
1113 * @return array an array of the newly generated unit terms
1114 */
1115 public function generate_unit_terms() {
1116 // Load the default terms array
1117 $default_terms = require __DIR__ . '/array-default-unit-terms.php';
1118
1119 $terms = [];
1120
1121 foreach ( $default_terms as $parent_term => $child_terms ) {
1122 $parent = (object) wp_insert_term( $parent_term, $this->slug . '-units' );
1123 $terms[] = get_term( $parent->term_id, $this->slug . '-units' );
1124
1125 foreach ( $child_terms as $child_term ) {
1126 $term = (object) wp_insert_term( $child_term, $this->slug . '-units', [ 'parent' => $parent->term_id ] );
1127 $terms[] = get_term( $term->term_id, $this->slug . '-units' );
1128 }
1129 }
1130
1131 return $terms;
1132 }
1133
1134 /**
1135 * Looks for the embed endpoint and serves up the requested chart if appropriate
1136 */
1137 public function template_redirect() {
1138 global $wp_query;
1139
1140 // Make sure this is a chart with the embed endpoint in the URL
1141 if ( ! isset( $wp_query->query['post_type'] ) || ! isset( $wp_query->query['embed'] ) || 'm-chart' != $wp_query->query['post_type'] ) {
1142 return;
1143 }
1144
1145 $post = get_post();
1146
1147 if ( ! $post ) {
1148 wp_die(
1149 esc_html__( 'The chart could not be found', 'm-chart' ),
1150 esc_html__( 'Chart not found', 'm-chart' ),
1151 [ 'response' => 404 ]
1152 );
1153 }
1154
1155 $settings = $this->get_settings();
1156
1157 if ( 'enabled' != $settings['embeds'] ) {
1158 wp_die(
1159 esc_html__( 'Embeds of this type are not enabled', 'm-chart' ),
1160 esc_html__( 'Embeds disabled', 'm-chart' ),
1161 [ 'response' => 403 ]
1162 );
1163 exit;
1164 }
1165
1166 $this->is_iframe = true;
1167
1168 $scripts = [
1169 'jquery',
1170 $this->get_post_meta( $post->ID, 'library' ),
1171 ];
1172
1173 unset( $_GET['action'], $_GET['share'] );
1174
1175 // This prevents issues when embedding with outside sites
1176 header_remove( 'X-Frame-Options' );
1177
1178 status_header( 200 );
1179
1180 require __DIR__ . '/templates/iframe.php';
1181 exit;
1182 }
1183
1184 /**
1185 * Hook to the m_chart_update_post_meta action and call the required library specific function
1186 *
1187 * @param int $post_id WP post ID of the post you want chart args for
1188 * @param array $parsed_meta the parsed chart meta passed by the action hook
1189 */
1190 public function m_chart_update_post_meta( $post_id, $parsed_meta ) {
1191 $library = $this->library( $parsed_meta['library'] );
1192
1193 // The chart's library plugin may be inactive (e.g. Highcharts charts after the
1194 // extension was removed) in which case there's no library object to notify
1195 if ( ! is_object( $library ) || ! method_exists( $library, 'm_chart_update_post_meta' ) ) {
1196 return;
1197 }
1198
1199 $library->m_chart_update_post_meta( $post_id, $parsed_meta );
1200 }
1201
1202 /**
1203 * If the passed library is valid return TRUE otherwise FALSE
1204 *
1205 * @param string library string (i.e. 'highcharts')
1206 *
1207 * @return bool
1208 */
1209 public function is_valid_library( $library ) {
1210 $libraries = $this->get_libraries();
1211
1212 if ( ! isset( $libraries[ $library ] ) ) {
1213 return false;
1214 }
1215
1216 return true;
1217 }
1218
1219 /**
1220 * If current page is an AMP page returns true
1221 *
1222 * @return bool
1223 */
1224 public function is_amp_endpoint() {
1225 if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
1226 return true;
1227 }
1228
1229 return false;
1230 }
1231
1232 /**
1233 * Return the M Chart settings
1234 *
1235 * @return array current settings
1236 */
1237 public function get_settings( $setting = false ) {
1238 // Memoize the resolved settings, get_post_meta() calls this on every invocation
1239 // and the option + filter pipeline doesn't need to re-run within a request
1240 // Invalidated via reset_settings() when the settings are saved
1241 if ( isset( $this->resolved_settings ) ) {
1242 $settings = $this->resolved_settings;
1243 } else {
1244 // Allow third party libraries to modify the default settings
1245 $default_settings = apply_filters( 'm_chart_default_settings', $this->settings );
1246
1247 $settings = (array) get_option( $this->slug, $default_settings );
1248 $settings = wp_parse_args( $settings, $default_settings );
1249
1250 // Make sure the set library is still valid
1251 if ( ! $this->is_valid_library( $settings['library'] ) ) {
1252 $settings['library'] = 'chartjs';
1253 }
1254
1255 $settings = apply_filters( 'm_chart_get_settings', $settings );
1256
1257 $this->resolved_settings = $settings;
1258 }
1259
1260 if ( $setting && isset( $settings[ $setting ] ) ) {
1261 return $settings[ $setting ];
1262 } elseif ( $setting ) {
1263 return null;
1264 }
1265
1266 return $settings;
1267 }
1268
1269 /**
1270 * Drop the memoized settings so the next get_settings() call re-resolves them
1271 *
1272 * Called after the settings option is updated
1273 */
1274 public function reset_settings() {
1275 $this->resolved_settings = null;
1276 }
1277
1278 /**
1279 * Return an array of available charting libraries
1280 *
1281 * @return array available chart libraries keyed by slug
1282 */
1283 public function get_libraries() {
1284 return apply_filters( 'm_chart_get_libraries', $this->libraries );
1285 }
1286
1287 /**
1288 * Return the chart types that support multiple data sets (multi-sheet tab bar)
1289 *
1290 * This is the single authoritative source for this list
1291 * The value is passed to window.m_chart_admin.multi_sheet_types so the React UI always stays in sync with PHP
1292 * Filtered via 'm_chart_multi_sheet_types' so library plugins can add or remove types
1293 *
1294 * @return array
1295 */
1296 public function get_multi_sheet_types() {
1297 return apply_filters( 'm_chart_multi_sheet_types', [
1298 'scatter',
1299 'bubble',
1300 'radar',
1301 'radar-area',
1302 'boxplot',
1303 'violin',
1304 ] );
1305 }
1306
1307 /**
1308 * Return the current library
1309 *
1310 * @return string current library
1311 */
1312 public function get_library() {
1313 if ( isset( $_GET['library'] ) && $this->is_valid_library( $_GET['library'] ) ) {
1314 return $_GET['library'];
1315 }
1316
1317 return $this->get_settings( 'library' );
1318 }
1319
1320 /**
1321 * Return the locale array
1322 *
1323 * When PHP's Intl extension is available, labels are generated in each locale's native language using Locale::getDisplayName()
1324 * Falls back to the English labels in array-locale-codes.php otherwise
1325 *
1326 * @return array locales as used by Intl.NumberFormat
1327 */
1328 public function get_locales() {
1329 $locales = require __DIR__ . '/array-locale-codes.php';
1330
1331 if ( ! class_exists( 'Locale' ) ) {
1332 return $locales;
1333 }
1334
1335 foreach ( $locales as $code => $english_label ) {
1336 $native = Locale::getDisplayName( $code, $code );
1337
1338 if ( $native ) {
1339 $locales[ $code ] = esc_html( $native );
1340 }
1341 }
1342
1343 return $locales;
1344 }
1345
1346 /**
1347 * Return a recursively merged array out of the two given
1348 *
1349 * @param array a multi dimensional array that will be merged into
1350 * @param array a multi dimensional array that will be merged recursively into the first one
1351 *
1352 * @return array the merged array
1353 */
1354 public function array_merge_recursive( &$a, $b ) {
1355 foreach ( $b as $child => $value ) {
1356 if ( isset( $a[ $child ] ) ) {
1357 // New value exists so we'll need to move a level down
1358 if ( is_array( $a[ $child ] ) && is_array( $value ) ) {
1359 $this->array_merge_recursive( $a[ $child ], $value );
1360 } else {
1361 // New value is not an array so we override the old value with the new one
1362 $a[ $child ] = $value;
1363 }
1364 } else {
1365 // New value doesn't exist so we can just add it
1366 $a[ $child ] = $value;
1367 }
1368 }
1369
1370 return $a;
1371 }
1372
1373 /**
1374 * Do things needed for version 1.7
1375 */
1376 public function upgrade_to_1_7() {
1377 // Get all charts
1378 $charts = get_posts(
1379 [
1380 'post_type' => m_chart()->slug,
1381 'posts_per_page' => -1,
1382 'post_status' => 'any',
1383 'tax_query' => [
1384 [
1385 'taxonomy' => 'post_tag',
1386 'field' => 'slug',
1387 'terms' => 'highcharts',
1388 'operator' => 'NOT IN',
1389 ],
1390 ],
1391 ]
1392 );
1393
1394 // Add highcharts post_tag to all charts
1395 foreach ( $charts as $chart ) {
1396 wp_set_object_terms( $chart->ID, 'highcharts', 'post_tag' );
1397 }
1398
1399 // Update version
1400 update_site_option( 'm_chart_version', '1.7' );
1401 }
1402
1403 /**
1404 * Do things needed for version 1.7.4
1405 */
1406 public function upgrade_to_1_7_4() {
1407 // Get all charts tagged with highcharts
1408 $highcharts_charts = get_posts(
1409 [
1410 'post_type' => m_chart()->slug,
1411 'posts_per_page' => -1,
1412 'post_status' => 'any',
1413 'tax_query' => [
1414 [
1415 'taxonomy' => 'post_tag',
1416 'field' => 'slug',
1417 'terms' => 'highcharts',
1418 'operator' => 'IN',
1419 ],
1420 ],
1421 ]
1422 );
1423
1424 foreach ( $highcharts_charts as $chart ) {
1425 // Add highcharts as a term to the m-chart-library taxonomy
1426 wp_set_object_terms( $chart->ID, 'highcharts', m_chart()->slug . '-library' );
1427 // Remove highcharts as a term from the post_tag taxonomy
1428 wp_remove_object_terms( $chart->ID, 'highcharts', 'post_tag' );
1429 }
1430
1431 // Get all charts tagged with chartjs
1432 $chartjs_charts = get_posts(
1433 [
1434 'post_type' => m_chart()->slug,
1435 'posts_per_page' => -1,
1436 'post_status' => 'any',
1437 'tax_query' => [
1438 [
1439 'taxonomy' => 'post_tag',
1440 'field' => 'slug',
1441 'terms' => 'chartjs',
1442 'operator' => 'IN',
1443 ],
1444 ],
1445 ]
1446 );
1447
1448 foreach ( $chartjs_charts as $chart ) {
1449 // Add chartjs as a term to the m-chart-library taxonomy
1450 wp_set_object_terms( $chart->ID, 'chartjs', m_chart()->slug . '-library' );
1451 // Remove chartjs as a term from the post_tag taxonomy
1452 wp_remove_object_terms( $chart->ID, 'chartjs', 'post_tag' );
1453 }
1454
1455 // Update version
1456 update_site_option( 'm_chart_version', '1.7.4' );
1457 }
1458 }
1459
1460 /**
1461 * Plugin object accessor
1462 */
1463 function m_chart() {
1464 global $m_chart;
1465
1466 if ( ! $m_chart instanceof M_Chart ) {
1467 $m_chart = new M_Chart();
1468 }
1469
1470 return $m_chart;
1471 }
1472
1473 /**
1474 * Freemius SDK accessor
1475 *
1476 * Thin global wrapper that delegates to the core class so Freemius ecosystem code and conventions keep working
1477 * The actual configuration lives in M_Chart::freemius()
1478 *
1479 * @return Freemius the Freemius SDK instance for M Chart
1480 */
1481 function m_chart_fs() {
1482 return m_chart()->freemius();
1483 }
1484