PluginProbe
M Chart / 2.2.2
M Chart v2.2.2
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-admin.php

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

1,301 lines 42.2 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_Admin {
8 // Used by both the Docs submenu entry and the footer script that adds target="_blank" to it
9 const DOCS_URL = 'https://docs.mch.art';
10
11 private $safe_settings = [
12 'performance' => [
13 'default',
14 'no-images',
15 'no-preview',
16 ],
17 'csv_delimiter' => [
18 ',',
19 "\t",
20 ' ',
21 ';',
22 ],
23 'defer_rendering' => [
24 '',
25 'enabled',
26 ],
27 ];
28 private $plugin_url;
29
30 /**
31 * Constructor
32 */
33 public function __construct() {
34 $this->plugin_url = m_chart()->plugin_url();
35
36 add_action( 'admin_init', [ $this, 'admin_init' ] );
37 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
38 // Runs after Freemius adds its own submenu links so we can relabel/reposition the Upgrade link
39 // Freemius hooks its menu at WP_FS__LOWEST_PRIORITY so we go one higher
40 add_action( 'admin_menu', [ $this, 'admin_submenu_links' ], ( defined( 'WP_FS__LOWEST_PRIORITY' ) ? WP_FS__LOWEST_PRIORITY : 999999998 ) + 1 );
41 add_action( 'admin_print_footer_scripts', [ $this, 'admin_print_footer_scripts' ] );
42 add_action( 'admin_head', [ $this, 'admin_head' ] );
43 add_action( 'current_screen', [ $this, 'current_screen' ] );
44 add_action( 'admin_footer', [ $this, 'admin_footer' ] );
45 add_action( 'wp_ajax_m_chart_export_csv', [ $this, 'ajax_export_csv' ] );
46 add_action( 'wp_ajax_m_chart_get_chart_args', [ $this, 'ajax_get_chart_args' ] );
47 add_action( 'wp_ajax_m_chart_import_csv', [ $this, 'ajax_import_csv' ] );
48 add_action( 'edit_form_before_permalink', [ $this, 'edit_form_before_permalink' ] );
49 add_action( 'manage_' . m_chart()->slug . '_posts_custom_column', [ $this, 'manage_posts_custom_column' ], 10, 2 );
50 add_action( 'm_chart_settings_admin', [ $this, 'm_chart_settings_admin' ] );
51
52 add_filter( 'manage_' . m_chart()->slug . '_posts_columns', [ $this, 'manage_posts_columns' ] );
53 }
54
55 /**
56 * Look for save settings submissions
57 */
58 public function admin_init() {
59 $this->save_settings();
60
61 add_action( 'admin_notices', [ $this, 'library_warning' ] );
62 }
63
64 /**
65 * Add settings admin page
66 */
67 public function admin_menu() {
68 add_submenu_page(
69 'edit.php?post_type=' . m_chart()->slug,
70 esc_html__( 'M Chart Settings', 'm-chart' ),
71 esc_html__( 'Settings', 'm-chart' ),
72 'manage_options',
73 m_chart()->slug . '-settings',
74 [ $this, 'm_chart_settings' ]
75 );
76 }
77
78 /**
79 * Parse the admin page slug out of a Freemius page URL
80 *
81 * Lets us find/style Freemius's submenu entries without relying on its internals
82 *
83 * @param string $url a Freemius admin page URL (upgrade, account, etc)
84 *
85 * @return string the page slug, or '' when the URL has none
86 */
87 private function freemius_page_slug( $url ) {
88 $query = wp_parse_url( $url, PHP_URL_QUERY );
89
90 if ( ! $query ) {
91 return '';
92 }
93
94 parse_str( $query, $query_args );
95
96 return $query_args['page'] ?? '';
97 }
98
99 /**
100 * Find the first free submenu position at or after the requested one
101 *
102 * Keeps our hardcoded ordering positions from silently clobbering entries another plugin may have placed there
103 *
104 * @param string $menu_slug the parent menu slug
105 * @param int $position the preferred position
106 *
107 * @return int the first unoccupied position
108 */
109 private function free_submenu_position( $menu_slug, $position ) {
110 global $submenu;
111
112 while ( isset( $submenu[ $menu_slug ][ $position ] ) ) {
113 $position++;
114 }
115
116 return $position;
117 }
118
119 /**
120 * Arrange the extra Charts submenu links
121 *
122 * Runs at a priority above Freemius so its pricing link already exists when we relabel and reposition it
123 * Handles the Upgrade link, the Docs link, and the per library Add Chart links all in one place so ordering is predictable
124 */
125 public function admin_submenu_links() {
126 global $submenu;
127
128 $menu_slug = 'edit.php?post_type=' . m_chart()->slug;
129
130 // Freemius adds its own pricing/upgrade submenu link for free users
131 // We relabel it to Upgrade and move it just above the Docs link
132 // Repositioning leaves the page route registered so the link still works
133 $pricing_slug = $this->freemius_page_slug( m_chart()->freemius()->get_upgrade_url() );
134
135 if ( '' !== $pricing_slug && ! empty( $submenu[ $menu_slug ] ) ) {
136 foreach ( $submenu[ $menu_slug ] as $position => $item ) {
137 if ( isset( $item[2] ) && $item[2] === $pricing_slug ) {
138 unset( $submenu[ $menu_slug ][ $position ] );
139
140 $item[0] = esc_html__( 'Upgrade', 'm-chart' );
141
142 $submenu[ $menu_slug ][ $this->free_submenu_position( $menu_slug, 99 ) ] = $item;
143
144 break;
145 }
146 }
147 }
148
149 // Pin the Freemius Account link to a predictable order just below Settings
150 // Freemius's own ordering can drift against our ksort so we set an explicit position
151 $account_slug = $this->freemius_page_slug( m_chart()->freemius()->get_account_url() );
152
153 if ( '' !== $account_slug && ! empty( $submenu[ $menu_slug ] ) ) {
154 foreach ( $submenu[ $menu_slug ] as $position => $item ) {
155 if ( isset( $item[2] ) && $item[2] === $account_slug ) {
156 unset( $submenu[ $menu_slug ][ $position ] );
157
158 $submenu[ $menu_slug ][ $this->free_submenu_position( $menu_slug, 96 ) ] = $item;
159
160 break;
161 }
162 }
163 }
164
165 // Docs link — sits at the bottom of the Charts submenu
166 // The third array element is the href; WordPress treats it as a full URL when it includes a scheme
167 // target="_blank" is added by admin_print_footer_scripts() since WP's $submenu API doesn't accept link attributes
168 $submenu[ $menu_slug ][ $this->free_submenu_position( $menu_slug, 100 ) ] = [
169 esc_html__( 'Docs', 'm-chart' ),
170 'edit_posts',
171 self::DOCS_URL,
172 ];
173
174 // If multiple libraries are active we'll give you the option of using each one
175 // @TODO As written this will break if there's ever more than 10 active libraries... so yeah
176 $libraries = m_chart()->get_libraries();
177
178 if ( 1 < count( $libraries ) ) {
179 // Put the default library into the admin menu first
180 $args = [
181 'post_type' => m_chart()->slug,
182 'library' => m_chart()->get_library(),
183 ];
184
185 $submenu[ $menu_slug ][10] = [
186 'Add ' . $libraries[ m_chart()->get_library() ] . ' Chart',
187 'edit_posts',
188 add_query_arg( $args, admin_url( 'post-new.php' ) ),
189 ];
190
191 unset( $libraries[ m_chart()->get_library() ] );
192
193 // Add a Add Chart option for each active library that isn't the current default
194 $key = 11;
195
196 foreach ( $libraries as $library => $library_name ) {
197 $args = [
198 'post_type' => m_chart()->slug,
199 'library' => $library,
200 ];
201
202 $submenu[ $menu_slug ][ $key ] = [
203 'Add ' . $library_name . ' Chart',
204 'edit_posts',
205 add_query_arg( $args, admin_url( 'post-new.php' ) ),
206 ];
207
208 $key++;
209 }
210 }
211
212 // Gotta sort them so they're in the right order
213 if ( ! empty( $submenu[ $menu_slug ] ) ) {
214 ksort( $submenu[ $menu_slug ] );
215 }
216 }
217
218 /**
219 * Add target="_blank" to the Docs link in the Charts submenu
220 *
221 * WordPress's add_submenu_page() / $submenu API has no concept of link attributes
222 * So we do it via a tiny footer script that runs on every admin page since the menu is global
223 */
224 public function admin_print_footer_scripts() {
225 // The Charts menu only renders for users who can edit posts so everyone else can skip this
226 if ( ! current_user_can( 'edit_posts' ) ) {
227 return;
228 }
229
230 ?>
231 <script>
232 ( () => {
233 const link = document.querySelector( '#adminmenu a[href="<?php echo esc_url( self::DOCS_URL ); ?>"]' );
234
235 if ( link ) {
236 link.setAttribute( 'target', '_blank' );
237 link.setAttribute( 'rel', 'noopener noreferrer' );
238 }
239 } )();
240 </script>
241 <?php
242 }
243
244 /**
245 * Hook: admin_head — print inline <style> for the Charts admin menu
246 *
247 * Lives inline rather than in the main SCSS bundle because the sidebar renders on every admin page
248 * and that bundle only enqueues on chart screens
249 *
250 * First block hides Freemius's "↳" sub-item arrow on the Account link
251 * Printed for every user since the Account link shows for paying users too
252 *
253 * Second block renders the Upgrade submenu link as a filled pill button with a trailing trendingUp icon
254 * Only printed for free users since that is when the Upgrade link exists
255 * The pill mimics the Pro plugin's menu badge - accent fill via --wp-admin-theme-color, white text, darker accent on hover
256 * The icon is the trendingUp icon from @wordpress/icons masked so background-color: currentColor tints it white to match the text
257 */
258 public function admin_head() {
259 // The Charts menu only renders for users who can edit posts so everyone else can skip all of this
260 if ( ! current_user_can( 'edit_posts' ) ) {
261 return;
262 }
263
264 // Freemius prefixes its sub-submenu items with a "↳" arrow via span.fs-submenu-item.fs-sub:before
265 // The #adminmenu prefix beats that selector so we can drop the glyph on our Account link
266 ?>
267 <style id="m-chart-menu-account">
268 #adminmenu .fs-submenu-item.account.fs-sub::before {
269 display: none;
270 }
271 </style>
272 <?php
273
274 // Recolor the top-level menu logo so it adapts to the admin color scheme
275 // WP prints the menu_icon SVG as a non-recolorable background-image and our logo has no fill so it renders black
276 // Masking it with currentColor makes it follow the menu link color - white on the active/hover item, scheme gray otherwise
277 ?>
278 <style id="m-chart-menu-icon">
279 #adminmenu #menu-posts-<?php echo esc_attr( m_chart()->slug ); ?> .wp-menu-image.svg {
280 /* WP sets the background-image via an inline style attribute so this needs !important */
281 background-image: none !important;
282 background-color: currentColor;
283 -webkit-mask: url("data:image/svg+xml;base64,<?php echo esc_attr( m_chart()->logo ); ?>") no-repeat center;
284 mask: url("data:image/svg+xml;base64,<?php echo esc_attr( m_chart()->logo ); ?>") no-repeat center;
285 /* Match WP's .wp-menu-image.svg background-size */
286 -webkit-mask-size: 20px auto;
287 mask-size: 20px auto;
288 }
289 </style>
290 <?php
291
292 if ( ! m_chart()->freemius()->is_free_plan() ) {
293 return;
294 }
295
296 // Parse the pricing page slug from the upgrade URL so the selector tracks the Freemius menu slug scheme
297 // Matching the full page slug avoids also styling the Settings link
298 $pricing_slug = $this->freemius_page_slug( m_chart()->freemius()->get_upgrade_url() );
299
300 // No pricing slug means there's no Upgrade link to style
301 if ( '' === $pricing_slug ) {
302 return;
303 }
304
305 $pricing_attr = esc_attr( $pricing_slug );
306 ?>
307 <style id="m-chart-menu-upgrade">
308 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"] {
309 display: inline-block;
310 margin: 4px 0 4px 13px;
311 /*
312 Using !important to beat WordPress's own submenu link padding (5px 12px)
313 the mobile media query overrides this again under 782px so the button can still grow
314 */
315 padding: 2px 10px !important;
316 border-radius: 2px;
317 font-weight: 600;
318 background: var( --wp-admin-theme-color, #3858e9 ) !important;
319 color: #fff !important;
320 }
321
322 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"]:hover,
323 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"]:focus {
324 background: color-mix( in srgb, var( --wp-admin-theme-color, #3858e9 ), black 10% ) !important;
325 color: #fff !important;
326 /*
327 WordPress paints an inset 4px left bar on submenu hover/focus
328 We remove it so the button stays clean
329 */
330 box-shadow: none !important;
331 }
332
333 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"]::after {
334 content: "";
335 display: inline-block;
336 width: 18px;
337 height: 18px;
338 margin-left: 4px;
339 vertical-align: middle;
340 background-color: currentColor;
341 -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M3.445%2016.505a.75.75%200%20001.06.05l5.005-4.55%204.024%203.521%204.716-4.715V14h1.5V8.25H14v1.5h3.19l-3.724%203.723L9.49%209.995l-5.995%205.45a.75.75%200%2000-.05%201.06z'/%3E%3C/svg%3E") no-repeat center / contain;
342 mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M3.445%2016.505a.75.75%200%20001.06.05l5.005-4.55%204.024%203.521%204.716-4.715V14h1.5V8.25H14v1.5h3.19l-3.724%203.723L9.49%209.995l-5.995%205.45a.75.75%200%2000-.05%201.06z'/%3E%3C/svg%3E") no-repeat center / contain;
343 }
344
345 /*
346 Under 782px WordPress enlarges submenu links for touch with an asymmetric 10px 10px 10px 20px padding
347 This keeps the bigger size but matches the left padding to the right so the button looks normal
348 */
349 @media screen and ( max-width: 782px ) {
350 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"] {
351 padding: 10px !important;
352 }
353
354 #adminmenu a[href*="page=<?php echo $pricing_attr; ?>"]::after {
355 width: 23px;
356 height: 23px;
357 }
358 }
359 </style>
360 <?php
361 }
362
363 /**
364 * Display the M Chart settings admin page
365 */
366 public function m_chart_settings() {
367 $settings = m_chart()->get_settings();
368 require_once __DIR__ . '/templates/m-chart-settings.php';
369 }
370
371 /**
372 * Check for and save M Chart settings
373 */
374 public function save_settings() {
375 if ( ! current_user_can( 'manage_options' ) ) {
376 return;
377 }
378
379 // Check the nonce
380 $nonce = $_POST[ m_chart()->slug ]['nonce'] ?? '';
381
382 if (
383 ! isset( $_POST[ m_chart()->slug ] )
384 || ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-settings' )
385 ) {
386 return;
387 }
388
389 $previous_settings = m_chart()->get_settings();
390 $validated_settings = [];
391 $submitted_settings = $_POST[ m_chart()->slug ];
392
393 $default_settings = apply_filters( 'm_chart_default_settings', m_chart()->settings );
394
395 foreach ( $default_settings as $setting => $default ) {
396 if ( ! isset( $submitted_settings[ $setting ] ) ) {
397 $validated_settings[ $setting ] = $default;
398 continue;
399 }
400
401 // Default chart height is numeric so clamp it to the same range as the per-chart height field
402 // Non-scalar submissions fall back to the default rather than tripping absint's array warning
403 if ( 'default_height' === $setting ) {
404 $validated_settings[ $setting ] = is_scalar( $submitted_settings[ $setting ] )
405 ? min( 1500, max( 300, absint( $submitted_settings[ $setting ] ) ) )
406 : $default;
407 continue;
408 }
409
410 if ( isset( $this->safe_settings[ $setting ] ) ) {
411 // If we've got an array of valid values lets check against that
412 $safe_setting = $this->safe_settings[ $setting ];
413
414 if ( in_array( $submitted_settings[ $setting ], $safe_setting, true ) ) {
415 $validated_settings[ $setting ] = $submitted_settings[ $setting ];
416 } else {
417 $validated_settings[ $setting ] = $default;
418 }
419 } else {
420 // Make sure the value is a string and matches the safe pattern before saving it
421 // Non-scalar submissions (e.g. an array from a library-plugin-added setting) fall back o the default
422 // Plugins that need to persist complex shapes should hook 'm_chart_validated_settings' below to inject their own validated value
423 $value = $submitted_settings[ $setting ];
424
425 if ( is_string( $value ) && preg_match( '#^[a-zA-Z0-9-_]+$#', $value ) ) {
426 $validated_settings[ $setting ] = $value;
427 } else {
428 $validated_settings[ $setting ] = $default;
429 }
430 }
431 }
432
433 // Allow third party libraries to further validate the settings
434 $validated_settings = apply_filters( 'm_chart_validated_settings', $validated_settings, $submitted_settings );
435
436 update_option( m_chart()->slug, $validated_settings );
437
438 // Drop the memoized settings so anything later in this request sees the new values
439 m_chart()->reset_settings();
440
441 // Only flush rewrite rules when the embed endpoint is actually being toggled
442 $previous_embeds = $previous_settings['embeds'] ?? '';
443 $current_embeds = $validated_settings['embeds'] ?? '';
444
445 if ( $previous_embeds !== $current_embeds ) {
446 flush_rewrite_rules();
447 }
448
449 add_action( 'admin_notices', [ $this, 'save_success' ] );
450 }
451
452 /**
453 * Display an admin notice that the settings have been saved
454 */
455 public function save_success() {
456 ?>
457 <div class="updated notice notice-success">
458 <p><?php esc_html_e( 'Settings saved', 'm-chart' ); ?></p>
459 </div>
460 <?php
461 }
462
463 /**
464 * Display an admin notice when the site has charts that use Highcharts but M Chart Highcharts Library is not active/installed
465 */
466 public function library_warning() {
467 if ( is_plugin_active( 'm-chart-highcharts-library/m-chart-highcharts-library.php' ) ) {
468 return;
469 }
470
471 $highcharts_check = get_posts(
472 [
473 'post_type' => m_chart()->slug,
474 'posts_per_page' => 1,
475 'post_status' => 'any',
476 'tax_query' => [
477 [
478 'taxonomy' => m_chart()->slug . '-library',
479 'field' => 'slug',
480 'terms' => 'highcharts',
481 ],
482 ],
483 ]
484 );
485
486 if ( ! $highcharts_check ) {
487 return;
488 }
489 ?>
490 <div class="warning notice notice-warning">
491 <p>
492 <?php
493 echo str_replace(
494 esc_html__( 'M Chart Highcharts Library', 'm-chart' ),
495 '<strong>' . esc_html__( 'M Chart Highcharts Library', 'm-chart' ) . '</strong>',
496 esc_html__( 'You have charts that require the M Chart Highcharts Library plugin.', 'm-chart' )
497 );
498 ?>
499 </p>
500 <p><?php esc_html_e( 'These charts will no longer display unless you install the plugin:', 'm-chart' ); ?></p>
501 <p><a href="https://github.com/methnen/m-chart-highcharts-library/"
502 class="button-primary"><?php esc_html_e( 'Learn More', 'm-chart' ); ?></a></p>
503 </div>
504 <?php
505 }
506
507 /**
508 * Load CSS/Javascript necessary for the interface
509 *
510 * @param object the current screen object as passed by the current_screen action hook
511 */
512 public function current_screen( $screen ) {
513 if ( m_chart()->slug !== $screen->post_type ) {
514 return;
515 }
516
517 // Only load these if we are on a post page
518 if ( 'post' === $screen->base ) {
519 // Jspreadsheet CE — needed by both chartjs (React) and other libraries (jQuery)
520 wp_enqueue_style(
521 'jspreadsheet',
522 $this->plugin_url . '/components/external/jspreadsheet/jspreadsheet.css',
523 [],
524 m_chart()->version
525 );
526
527 wp_enqueue_script(
528 'jspreadsheet',
529 $this->plugin_url . '/components/external/jspreadsheet/jspreadsheet.js',
530 [ 'jsuites' ],
531 m_chart()->version
532 );
533
534 // jSuites — required by Jspreadsheet
535 wp_enqueue_style(
536 'jsuites',
537 $this->plugin_url . '/components/external/jsuites/jsuites.css',
538 [],
539 m_chart()->version
540 );
541
542 wp_enqueue_script(
543 'jsuites',
544 $this->plugin_url . '/components/external/jsuites/jsuites.js',
545 [],
546 m_chart()->version
547 );
548
549 // Admin UI React app
550 $admin_app_asset = require __DIR__ . '/admin-ui/index.asset.php';
551
552 wp_enqueue_script(
553 'm-chart-admin-ui',
554 $this->plugin_url . '/components/admin-ui/index.js',
555 array_merge( $admin_app_asset['dependencies'], [ 'wp-hooks' ] ),
556 $admin_app_asset['version'],
557 [ 'strategy' => 'defer' ]
558 );
559
560 wp_set_script_translations(
561 'm-chart-admin-ui',
562 'm-chart',
563 plugin_dir_path( __DIR__ ) . 'components/languages/'
564 );
565
566 // We need the library and post ID for a bunch of stuff below
567 $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : '';
568 $library = m_chart()->get_library();
569
570 if ( ! empty( $post_id ) ) {
571 $library = m_chart()->get_post_meta( absint( $post_id ), 'library' );
572 } elseif (
573 'post' === $screen->base
574 && 'add' === $screen->action
575 && isset( $_GET['library'] )
576 && m_chart()->is_valid_library( $_GET['library'] )
577 ) {
578 $library = $_GET['library'];
579 }
580
581 if ( 'chartjs' === $library ) {
582 // Chart.js libs — enqueued explicitly so the React preview has window.Chart and window
583 // MChartHelper available before m-chart-admin-ui runs its plugin registration
584 // We load every plugin regardless of immediate need when in the edit view since the user can switch chart types from the picker
585 wp_enqueue_script( 'chartjs-helper' );
586 wp_enqueue_script( 'chartjs-datalabels' );
587 wp_enqueue_script( 'chartjs-treemap' );
588 wp_enqueue_script( 'chartjs-boxplot' );
589 }
590
591 $post_meta = m_chart()->get_post_meta( $post_id );
592 $spreadsheet_data = empty( $post_meta['data'] ) ? [ [ '' ] ] : $post_meta['data']['sets'];
593 unset( $post_meta['data'] ); // passed separately as spreadsheet_data
594
595 // Collect library-specific config for the React admin app
596 $type_options = [];
597 $type_option_names = [];
598 $themes = [];
599
600 if ( m_chart()->library( $library ) ) {
601 $library_class = m_chart()->library( $library );
602 $type_options = $library_class->type_options;
603 $type_option_names = $library_class->type_option_names;
604
605 foreach ( $library_class->get_themes() as $theme ) {
606 $themes[] = [
607 'slug' => $theme->slug,
608 'name' => $theme->name,
609 ];
610 }
611 }
612
613 // Format unit terms as an array of {group, units} for easy JS mapping
614 $unit_terms = [];
615
616 foreach ( m_chart()->get_unit_terms() as $group => $units ) {
617 $group_units = [];
618
619 foreach ( $units as $unit ) {
620 $group_units[] = [ 'name' => $unit->name, 'slug' => $unit->slug ];
621 }
622
623 $unit_terms[] = [
624 'group' => $group,
625 'units' => $group_units,
626 ];
627 }
628
629 $chart_image = m_chart()->get_chart_image( $post_id );
630
631 // Compute initial chart args for the React preview (React-enabled libraries, existing posts only)
632 $initial_chart_args = null;
633
634 if ( $post_id && m_chart()->library( $library ) ) {
635 $initial_chart_args = m_chart()->library( $library )->get_chart_args(
636 $post_id,
637 m_chart()->get_chart_default_args,
638 true, // force recompute
639 false // don't store in cache
640 );
641 }
642
643 // Allow extensions to modify or replace the chart args used for the editor's initial preview render
644 $initial_chart_args = apply_filters( 'm_chart_admin_initial_chart_args', $initial_chart_args, $post_id, $library );
645
646 // Build CSV delimiter map for React's CsvControls component
647 $csv_delimiters = [];
648 foreach ( m_chart()->csv_delimiters as $delimiter => $delimiter_name ) {
649 $csv_delimiters[ $delimiter ] = $delimiter_name;
650 }
651
652 $localize_data = [
653 'slug' => m_chart()->slug,
654 'version' => m_chart()->version,
655 'refresh_counter' => 0,
656 'allow_form_submission' => false,
657 'request' => false,
658 'performance' => m_chart()->get_settings( 'performance' ),
659 'image_support' => apply_filters( 'm_chart_image_support', 'no', $library ),
660 'instant_preview_support' => apply_filters( 'm_chart_instant_preview_support', 'no', $library ),
661 'image_multiplier' => m_chart()->get_settings( 'image_multiplier' ),
662 'image_width' => m_chart()->get_settings( 'image_width' ),
663 'library' => $library,
664 'set_names' => m_chart()->get_post_meta( $post_id, 'set_names' ),
665 'post_id' => $post_id,
666 'nonce' => wp_create_nonce( m_chart()->slug . '-save-post' ),
667 'ajax_url' => admin_url( 'admin-ajax.php' ),
668 'post_meta' => $post_meta,
669 'spreadsheet_data' => $spreadsheet_data,
670 'type_options' => $type_options,
671 'type_option_names' => $type_option_names,
672 'themes' => $themes,
673 'unit_terms' => $unit_terms,
674 'image_url' => $chart_image ? esc_url( $chart_image['url'] ) : '',
675 'chart_args' => $initial_chart_args,
676 'csv_delimiters' => $csv_delimiters,
677 'default_delimiter' => m_chart()->get_settings( 'csv_delimiter' ),
678 'multi_sheet_types' => m_chart()->get_multi_sheet_types(),
679 ];
680
681 wp_localize_script( 'm-chart-admin-ui', 'm_chart_admin', $localize_data );
682
683 do_action( 'm_chart_admin_scripts', $library, $post_id );
684 }
685
686 // Admin panel CSS
687 wp_enqueue_style(
688 'm-chart-admin',
689 $this->plugin_url . '/components/css/m-chart-admin.css',
690 [],
691 m_chart()->version
692 );
693 }
694
695 /**
696 * Add all of the metaboxes needed for the data and chart editing interface
697 */
698 public function meta_boxes() {
699 global $wp_meta_boxes;
700
701 // Remove excerpt from its normal spot in the meta_boxes array so we can put it back in after the spreadsheet
702 // Users can move metaboxes, but this helps put things in a reasonable place on the first visit
703 $excerpt = $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'core' ][ 'postexcerpt' ];
704 unset( $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'core' ][ 'postexcerpt' ] );
705
706 add_meta_box(
707 m_chart()->slug . '-spreadsheet',
708 esc_html__( 'Data', 'm-chart' ),
709 [ $this, 'spreadsheet_meta_box' ],
710 m_chart()->slug,
711 'normal',
712 'high'
713 );
714
715 add_meta_box(
716 m_chart()->slug,
717 esc_html__( 'Chart', 'm-chart' ),
718 [ $this, 'chart_meta_box' ],
719 m_chart()->slug,
720 'normal',
721 'high'
722 );
723
724 $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'high' ][ 'postexcerpt' ] = $excerpt;
725
726 // We are using our own interface for the units so we can remove the units taxonomy metabox
727 remove_meta_box( m_chart()->slug . '-unitsdiv', m_chart()->slug, 'side' );
728 }
729
730 /**
731 * Displays the spreadsheet meta box
732 *
733 * @param object the WP post object as returned by the metabox API
734 */
735 public function spreadsheet_meta_box( $post ) {
736 echo '<div id="m-chart-spreadsheet-root"></div>';
737 echo '<textarea name="' . esc_attr( $this->get_field_name( 'data' ) ) . '" class="data hide"></textarea>';
738 wp_nonce_field( m_chart()->slug . '-save-post', $this->get_field_name( 'nonce' ) );
739 }
740
741 /**
742 * Displays the chart meta box
743 *
744 * @param object the WP post object as returned by the metabox API
745 */
746 public function chart_meta_box( $post ) {
747 // Force an instance of 1 since we NEVER show more than one chart at a time inside the admin panel
748 m_chart()->instance = 1;
749
750 $post_meta = m_chart()->get_post_meta( $post->ID );
751 $image = m_chart()->get_chart_image( $post->ID );
752 $settings = m_chart()->get_settings();
753
754 require_once __DIR__ . '/templates/chart-meta-box.php';
755 }
756
757 /**
758 * Insert CSV Import and Export forms into the footer when editing charts
759 */
760 public function admin_footer() {
761 $screen = get_current_screen();
762
763 if ( 'post' !== $screen->base || m_chart()->slug !== $screen->post_type ) {
764 return;
765 }
766 ?>
767 <script type="text/javascript">
768 <?php do_action( 'm_chart_admin_footer_javascript' ); ?>
769 </script>
770 <?php
771 }
772
773 /**
774 * Inserts a subtitle field under the title field on the chart edit form
775 *
776 * @param object the WP post object as returned by the metabox API
777 */
778 public function edit_form_before_permalink( $post ) {
779 if ( m_chart()->slug !== $post->post_type ) {
780 return;
781 }
782
783 echo '<div id="m-chart-subtitle-root"></div>';
784 }
785
786 /**
787 * Display some additional information about a chart
788 *
789 * @param string the name of the custom column being displayed
790 * @param string the $post_id of the post being displayed in this row
791 */
792 public function manage_posts_custom_column( $column, $post_id ) {
793 if ( m_chart()->slug . '-type' !== $column && m_chart()->slug . '-library' !== $column ) {
794 return;
795 }
796
797 $library = m_chart()->get_post_meta( $post_id, 'library' );
798 $library_instance = m_chart()->library( $library );
799
800 if ( ! $library_instance || $library_instance->library !== $library ) {
801 ?>
802 <span aria-hidden="true"></span>
803 <span class="screen-reader-text"><?php echo esc_html__( 'Library not found', 'm-chart' ); ?></span>
804 <?php
805 return;
806 }
807
808 if ( m_chart()->slug . '-type' === $column ) {
809 $type = m_chart()->get_post_meta( $post_id, 'type' );
810 $type_name = $library_instance->type_option_names[ $type ];
811 ?>
812 <span class="type <?php echo esc_attr( $type ); ?>" title="<?php echo esc_attr( $type_name ); ?>">
813 <?php echo esc_html( $type_name ); ?>
814 </span>
815 <?php
816 }
817
818 if ( m_chart()->slug . '-library' === $column ) {
819 $library_name = $library_instance->library_name;
820 ?>
821 <span class="library <?php echo esc_attr( $library ); ?>" title="<?php echo esc_attr( $library_name ); ?>">
822 <?php echo esc_html( $library_name ); ?>
823 </span>
824 <?php
825 }
826 }
827
828 /**
829 * Add the Chart.js admin settings to the M Chart Settings page
830 */
831 public function m_chart_settings_admin() {
832 $settings = m_chart()->get_settings();
833 require __DIR__ . '/templates/m-chart-settings-chartjs.php';
834 }
835
836 /**
837 * Add our custom column to the array of columns for charts
838 *
839 * @param array the array of columns
840 *
841 * @return array array of columns with the custom column added
842 */
843 public function manage_posts_columns( $columns ) {
844 $new_columns = [];
845
846 foreach ( $columns as $column => $name ) {
847 $new_columns[ $column ] = $name;
848
849 if ( 'author' === $column || 'coauthors' === $column ) {
850 $new_columns[ m_chart()->slug . '-type' ] = 'Type';
851
852 if ( 'yes' === m_chart()->get_settings( 'show_library' ) ) {
853 $new_columns[ m_chart()->slug . '-library' ] = 'Library';
854 }
855 }
856 }
857
858 return $new_columns;
859 }
860
861 /**
862 * Hook to save_post action and save chart related post meta
863 *
864 * @param int the WP post ID of the post being saved
865 */
866 public function save_post( $post_id ) {
867 $post = get_post( $post_id );
868
869 // Check that this isn't an autosave
870 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
871 return;
872 }
873
874 // Check post type
875 if ( ! isset( $post->post_type ) || m_chart()->slug !== $post->post_type ) {
876 return;
877 }
878
879 // Don't run on post revisions (almost always happens just before the real post is saved)
880 if ( wp_is_post_revision( $post->ID ) ) {
881 return;
882 }
883
884 // Make sure we've got some actual M Chart related data in the $_POST array
885 if ( ! isset( $_POST[ m_chart()->slug ] ) ) {
886 return;
887 }
888
889 // Check the nonce
890 $nonce = $_POST[ m_chart()->slug ]['nonce'] ?? '';
891
892 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
893 return;
894 }
895
896 unset( $_POST[ m_chart()->slug ]['nonce'] );
897
898 // Check the permissions
899 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
900 return;
901 }
902
903 // If there's an image being passed attach it to the chart post
904 $this->attach_image();
905
906 // Make sure we don't overwrite existing settings in the case someone hits update too quickly
907 if (
908 isset( $_POST[ m_chart()->slug ]['library'] )
909 // Make sure the library value is clean and valid before trying to use it
910 && m_chart()->is_valid_library( $_POST[ m_chart()->slug ]['library'] )
911 ) {
912 $library = sanitize_key( $_POST[ m_chart()->slug ]['library'] );
913
914 // Load the library in question in case there's a filter/action we'll need
915 m_chart()->library( $library );
916
917 // update_post_meta passes the $_POST values directly to validate_post_meta
918 // validate_post_meta returns only valid post meta values and does data validation on each item
919 m_chart()->update_post_meta( $post->ID, $_POST[ m_chart()->slug ] );
920 }
921 }
922
923 /**
924 * Attach a given image to a chart post
925 *
926 * @param int the WP post ID of the post being saved
927 * @param string a base64 encoded string of the image we want to attach
928 */
929 public function attach_image() {
930 $settings = m_chart()->get_settings();
931
932 // If the performance setting isn't turned to default we don't do this
933 if ( 'default' !== $settings['performance'] ) {
934 return false;
935 }
936
937 if ( ! is_numeric( $_POST['post_ID'] ?? '' ) ) {
938 return false;
939 }
940
941 $post_id = absint( $_POST['post_ID'] );
942
943 // Make sure the library used on this post supports images
944 if ( 'no' === apply_filters( 'm_chart_image_support', 'no', m_chart()->get_post_meta( $post_id, 'library' ) ) ) {
945 return false;
946 }
947
948 if ( ! current_user_can( 'edit_post', $post_id ) ) {
949 return false;
950 }
951
952 if ( ! $post = get_post( $post_id ) ) {
953 return false;
954 }
955
956 $img_data = $_POST[ m_chart()->slug ]['img'] ?? '';
957
958 if ( '' === $img_data ) {
959 return false;
960 }
961
962 // Decode the image so we can save it
963 $decoded_img = base64_decode( str_replace( 'data:image/png;base64,', '', $img_data ) );
964
965 // Reject anything that isn't a real PNG before writing it to disk
966 if ( false === $decoded_img || '' === $decoded_img || "\x89PNG\r\n\x1a\n" !== substr( $decoded_img, 0, 8 ) ) {
967 return false;
968 }
969
970 // Cap the image size at 5MB to keep a runaway client from filling disk
971 if ( strlen( $decoded_img ) > 5 * 1024 * 1024 ) {
972 return false;
973 }
974
975 // Check for an existing attached image
976 $attachments = get_posts(
977 [
978 'post_type' => 'attachment',
979 'posts_per_page' => 1,
980 'post_parent' => $post->ID,
981 'meta_key' => m_chart()->slug . '-image',
982 ]
983 );
984
985 // If an existing image was found delete it
986 foreach ( $attachments as $attachment ) {
987 wp_delete_attachment( $attachment->ID, true );
988 }
989
990 // Upload image to WP
991 $file = wp_upload_bits( sanitize_title( $post->post_title . '-' . $post->ID ) . '.png', null, $decoded_img );
992
993 // START acting like media_sideload_image
994 preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file['file'], $matches );
995
996 $file_array['name'] = basename( $matches[0] );
997 $file_array['tmp_name'] = $file['file'];
998
999 if ( is_wp_error( $file ) ) {
1000 @unlink( $file_array['tmp_name'] );
1001 $file_array['tmp_name'] = '';
1002 }
1003
1004 $img_id = media_handle_sideload( $file_array, $post->ID, $post->post_title );
1005
1006 if ( is_wp_error( $img_id ) ) {
1007 @unlink( $file_array['tmp_name'] );
1008 return $img_id;
1009 }
1010 // STOP acting like media_sideload_image
1011
1012 // Set some meta on the attachment so we know it came from m-chart
1013 add_post_meta( $img_id, m_chart()->slug . '-image', $post->ID );
1014
1015 // Set the attachment as the chart's thumbnail
1016 update_post_meta( $post->ID, '_thumbnail_id', $img_id );
1017 }
1018
1019 /**
1020 * Parses an incoming CSV file and compiles it into an array
1021 *
1022 * @return array an array of the data from the imported CSV file ready for use in the chart meta
1023 */
1024 public function ajax_import_csv() {
1025 $post = get_post( absint( $_POST['post_id'] ?? 0 ) );
1026
1027 // Check post type
1028 if ( ! isset( $post->post_type ) || m_chart()->slug !== $post->post_type ) {
1029 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
1030 }
1031
1032 // Check the nonce
1033 $nonce = $_POST['nonce'] ?? '';
1034
1035 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
1036 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
1037 }
1038
1039 // Check the permissions
1040 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1041 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
1042 }
1043
1044 // Make sure there's a CSV file
1045 if ( empty( $_FILES['import_csv_file']['name'] ) ) {
1046 wp_send_json_error( esc_html__( 'No file to import', 'm-chart' ) );
1047 }
1048
1049 // Check upload-level errors first
1050 if ( UPLOAD_ERR_OK !== ( $_FILES['import_csv_file']['error'] ?? UPLOAD_ERR_NO_FILE ) ) {
1051 wp_send_json_error( esc_html__( 'File upload error', 'm-chart' ) );
1052 }
1053
1054 if ( ! is_uploaded_file( $_FILES['import_csv_file']['tmp_name'] ) ) {
1055 wp_send_json_error( esc_html__( 'Invalid upload', 'm-chart' ) );
1056 }
1057
1058 // Verify both extension AND MIME (some browsers send text/plain for CSV)
1059 $file_check = wp_check_filetype_and_ext(
1060 $_FILES['import_csv_file']['tmp_name'],
1061 $_FILES['import_csv_file']['name'],
1062 [ 'csv' => 'text/csv', 'csv-alt' => 'text/plain' ]
1063 );
1064
1065 if ( 'csv' !== ( $file_check['ext'] ?? '' ) ) {
1066 wp_send_json_error( esc_html__( 'Only CSV files can be imported', 'm-chart' ) );
1067 }
1068
1069 // Do some validation on the CSV file (mirroring what WP does for this sort of thing)
1070 $csv_file = realpath( $_FILES['import_csv_file']['tmp_name'] );
1071
1072 if ( ! $csv_file ) {
1073 wp_send_json_error( esc_html__( 'File path not found', 'm-chart' ) );
1074 }
1075
1076 // Cap file size at 2MB to prevent resource exhaustion
1077 if ( filesize( $csv_file ) > 2 * 1024 * 1024 ) {
1078 wp_send_json_error( esc_html__( 'CSV file too large (max 2MB)', 'm-chart' ) );
1079 }
1080
1081 $csv_data = file_get_contents( $csv_file );
1082
1083 if ( '' === $csv_data ) {
1084 wp_send_json_error( esc_html__( 'CSV file was empty', 'm-chart' ) );
1085 }
1086
1087 // Get parseCSV library so we can use it to convert the CSV to a nice array
1088 // Yes, PHP does this natively now but I've run into trouble with malformed CSV that parsCSV handles fine
1089 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
1090
1091 $parse_csv = new parseCSV();
1092
1093 // The "\n" before and after is to deal with CSV files that don't have line breaks above and below the data
1094 // Which then seems to confuse parseCSV occasionally
1095 $csv_data = "\n" . trim( $csv_data ) . "\n";
1096
1097 // Set delimiter but check to make sure it's safe first
1098 $parse_csv->delimiter = isset( $_POST['csv_delimiter'] ) && in_array( $_POST['csv_delimiter'], $this->safe_settings[ 'csv_delimiter' ] ) ? $_POST['csv_delimiter'] : m_chart()->get_settings( 'csv_delimiter' );
1099
1100 // Parse the CSV
1101 $parse_csv->parse( $csv_data );
1102
1103 // This deals with Google Doc's crappy CSV exports which don't include columns at the end of a row if they are empty
1104 $data_array = $this->fix_csv_data( $parse_csv->data );
1105
1106 wp_send_json_success( $data_array );
1107 }
1108
1109 /**
1110 * Helper function makes sure that the data array has matching numbers of array elements for each row
1111 * CSV from some sources (Google Docs) doesn't include columns that are empty when they are at the end of a row (Why Google? WHY?)
1112 *
1113 * @param array an array of data as returned from the parseCSV class
1114 *
1115 * @return array the array of data with matching array value counts
1116 */
1117 public function fix_csv_data( $data_array ) {
1118 $count = 0;
1119
1120 // Get largest row count
1121 foreach ( $data_array as $data ) {
1122 $temp_count = count( $data );
1123
1124 $count = ( $temp_count > $count ) ? $temp_count : $count;
1125 }
1126
1127 // Fix arrays so value counts match
1128 foreach ( $data_array as $key => $data ) {
1129 $temp_count = count( $data );
1130
1131 if ( $temp_count < $count ) {
1132 $difference = $count - $temp_count;
1133
1134 for ( $i = 0; $i < $difference; $i++ ) {
1135 $data_array[ $key ][] = '';
1136 }
1137 }
1138 }
1139
1140 return $data_array;
1141 }
1142
1143 /**
1144 * Converts data array into CSV and outputs it to the browser
1145 */
1146 public function ajax_export_csv() {
1147 $post_id = $_REQUEST['post_id'] ?? '';
1148 $nonce = $_REQUEST['nonce'] ?? '';
1149
1150 if (
1151 ! is_numeric( $post_id )
1152 || ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' )
1153 || ! current_user_can( 'edit_post', absint( $post_id ) )
1154 ) {
1155 wp_die( esc_html__( 'Unauthorized access', 'm-chart' ), esc_html__( 'You do not have permission to do that', 'm-chart' ), [ 'response' => 401 ] );
1156 }
1157
1158 $post = get_post( absint( $post_id ) );
1159
1160 // If the user passed a data value in their request we'll use it after validation
1161 if ( isset( $_POST['data'] ) && isset( $_POST['title'] ) ) {
1162 $data = m_chart()->validate_data( json_decode( stripslashes( $_POST['data'] ) ) );
1163 $file_name = sanitize_title( $_POST['title'] );
1164 } else {
1165 $data = m_chart()->get_post_meta( $post->ID, 'data' );
1166 $file_name = sanitize_title( get_the_title( $post->ID ) );
1167 }
1168
1169 $set_name = sanitize_title( $_REQUEST['set_name'] ?? '' );
1170
1171 if ( empty( $data ) ) {
1172 return;
1173 }
1174
1175 // Prevent CSV/formula injection by prefixing any cell that begins with a formula trigger so spreadsheet apps see it as a literal string
1176 array_walk_recursive( $data, function ( &$cell ) {
1177 $cell = $this->neutralize_csv_cell( $cell );
1178 } );
1179
1180 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
1181 $parse_csv = new parseCSV();
1182
1183 // Set delimiter
1184 $parse_csv->output_delimiter = m_chart()->get_settings( 'csv_delimiter' );
1185
1186 $parse_csv->output( $file_name . '-' . $set_name . '.csv', $data );
1187 die;
1188 }
1189
1190 /**
1191 * Prefix a cell value with a single quote when it starts with a character that Excel/Sheets/Numbers interpret as a formula trigger
1192 *
1193 * @param mixed $cell The raw cell value
1194 * @return string The cell value, prefixed with ' if it would otherwise execute
1195 */
1196 public function neutralize_csv_cell( $cell ) {
1197 $cell = (string) $cell;
1198
1199 if ( '' !== $cell && in_array( $cell[0], [ '=', '+', '-', '@', "\t", "\r" ], true ) ) {
1200 return "'" . $cell;
1201 }
1202
1203 return $cell;
1204 }
1205
1206 /**
1207 * Returns JSON encoded chart args from $_POST values sent from the admin panel
1208 *
1209 * @return string a JSON encoded string containing all of the chart args needed to update an active chart
1210 */
1211 public function ajax_get_chart_args() {
1212 // Check the nonce
1213 $nonce = $_POST['nonce'] ?? '';
1214
1215 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
1216 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
1217 }
1218
1219 // Does the post exist? (post_id is 0 for new charts that haven't been saved yet)
1220 $post_id = absint( $_POST['post_id'] ?? 0 );
1221
1222 if ( $post_id ) {
1223 if ( ! $post = get_post( $post_id ) ) {
1224 wp_send_json_error( esc_html__( 'Invalid post', 'm-chart' ) );
1225 }
1226
1227 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1228 wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) );
1229 }
1230 } else {
1231 // New chart — no saved post yet, build a stub so the library can compute chart args
1232 if ( ! current_user_can( 'edit_posts' ) ) {
1233 wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) );
1234 }
1235
1236 $post = new WP_Post( (object) [
1237 'ID' => 0,
1238 'post_title' => '',
1239 'post_type' => m_chart()->slug,
1240 'post_status' => 'auto-draft',
1241 ] );
1242 }
1243
1244 // Is this a valid library?
1245 $library_slug = $_POST['library'] ?? '';
1246
1247 if ( ! m_chart()->is_valid_library( $library_slug ) ) {
1248 wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) );
1249 }
1250
1251 // This does get potentially overwritten later on
1252 // However, it's necessary for initial load on a new chart
1253 if ( 'highcharts' === $library_slug ) {
1254 $library = m_chart()->library( $library_slug );
1255 }
1256
1257 $library = apply_filters( 'm_chart_library_class', m_chart()->library_class, $library_slug );
1258
1259 // Make sure a third-party filter didn't replace the library with something unusable
1260 if ( ! is_object( $library ) || ! method_exists( $library, 'get_chart_args' ) ) {
1261 wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) );
1262 }
1263
1264 // Set these values so that get_chart_args has them already available before we call it
1265 $library->args = m_chart()->get_chart_default_args;
1266 $library->post = $post;
1267 $library->post->post_title = sanitize_text_field( $_POST['title'] ?? '' );
1268
1269 // validate_post_meta returns only valid post meta values and does data validation on each item
1270 $library->post_meta = m_chart()->validate_post_meta( $_POST['post_meta'] ?? [] );
1271
1272 wp_send_json_success( $library->get_chart_args( $library->post->ID, $library->args, true, false ) );
1273 }
1274
1275 /**
1276 * Return a name spaced field name
1277 *
1278 * @param string the field name we want to name space
1279 *
1280 * @param string a name spaced field name
1281 */
1282 public function get_field_name( $field_name, $parent_field_name = '' ) {
1283 if ( '' !== $parent_field_name ) {
1284 return m_chart()->slug . '[' . $parent_field_name . ']' . '[' . $field_name . ']';
1285 }
1286
1287 return m_chart()->slug . '[' . $field_name . ']';
1288 }
1289
1290 /**
1291 * Return a name spaced field id
1292 *
1293 * @param string the field id we want to name space
1294 *
1295 * @param string a name spaced field id
1296 */
1297 public function get_field_id( $field_name ) {
1298 return m_chart()->slug . '-' . $field_name;
1299 }
1300 }
1301