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

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

1,298 lines 42.0 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 // Build CSV delimiter map for React's CsvControls component
644 $csv_delimiters = [];
645 foreach ( m_chart()->csv_delimiters as $delimiter => $delimiter_name ) {
646 $csv_delimiters[ $delimiter ] = $delimiter_name;
647 }
648
649 $localize_data = [
650 'slug' => m_chart()->slug,
651 'version' => m_chart()->version,
652 'refresh_counter' => 0,
653 'allow_form_submission' => false,
654 'request' => false,
655 'performance' => m_chart()->get_settings( 'performance' ),
656 'image_support' => apply_filters( 'm_chart_image_support', 'no', $library ),
657 'instant_preview_support' => apply_filters( 'm_chart_instant_preview_support', 'no', $library ),
658 'image_multiplier' => m_chart()->get_settings( 'image_multiplier' ),
659 'image_width' => m_chart()->get_settings( 'image_width' ),
660 'library' => $library,
661 'set_names' => m_chart()->get_post_meta( $post_id, 'set_names' ),
662 'post_id' => $post_id,
663 'nonce' => wp_create_nonce( m_chart()->slug . '-save-post' ),
664 'ajax_url' => admin_url( 'admin-ajax.php' ),
665 'post_meta' => $post_meta,
666 'spreadsheet_data' => $spreadsheet_data,
667 'type_options' => $type_options,
668 'type_option_names' => $type_option_names,
669 'themes' => $themes,
670 'unit_terms' => $unit_terms,
671 'image_url' => $chart_image ? esc_url( $chart_image['url'] ) : '',
672 'chart_args' => $initial_chart_args,
673 'csv_delimiters' => $csv_delimiters,
674 'default_delimiter' => m_chart()->get_settings( 'csv_delimiter' ),
675 'multi_sheet_types' => m_chart()->get_multi_sheet_types(),
676 ];
677
678 wp_localize_script( 'm-chart-admin-ui', 'm_chart_admin', $localize_data );
679
680 do_action( 'm_chart_admin_scripts', $library, $post_id );
681 }
682
683 // Admin panel CSS
684 wp_enqueue_style(
685 'm-chart-admin',
686 $this->plugin_url . '/components/css/m-chart-admin.css',
687 [],
688 m_chart()->version
689 );
690 }
691
692 /**
693 * Add all of the metaboxes needed for the data and chart editing interface
694 */
695 public function meta_boxes() {
696 global $wp_meta_boxes;
697
698 // Remove excerpt from its normal spot in the meta_boxes array so we can put it back in after the spreadsheet
699 // Users can move metaboxes, but this helps put things in a reasonable place on the first visit
700 $excerpt = $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'core' ][ 'postexcerpt' ];
701 unset( $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'core' ][ 'postexcerpt' ] );
702
703 add_meta_box(
704 m_chart()->slug . '-spreadsheet',
705 esc_html__( 'Data', 'm-chart' ),
706 [ $this, 'spreadsheet_meta_box' ],
707 m_chart()->slug,
708 'normal',
709 'high'
710 );
711
712 add_meta_box(
713 m_chart()->slug,
714 esc_html__( 'Chart', 'm-chart' ),
715 [ $this, 'chart_meta_box' ],
716 m_chart()->slug,
717 'normal',
718 'high'
719 );
720
721 $wp_meta_boxes[ m_chart()->slug ][ 'normal' ][ 'high' ][ 'postexcerpt' ] = $excerpt;
722
723 // We are using our own interface for the units so we can remove the units taxonomy metabox
724 remove_meta_box( m_chart()->slug . '-unitsdiv', m_chart()->slug, 'side' );
725 }
726
727 /**
728 * Displays the spreadsheet meta box
729 *
730 * @param object the WP post object as returned by the metabox API
731 */
732 public function spreadsheet_meta_box( $post ) {
733 echo '<div id="m-chart-spreadsheet-root"></div>';
734 echo '<textarea name="' . esc_attr( $this->get_field_name( 'data' ) ) . '" class="data hide"></textarea>';
735 wp_nonce_field( m_chart()->slug . '-save-post', $this->get_field_name( 'nonce' ) );
736 }
737
738 /**
739 * Displays the chart meta box
740 *
741 * @param object the WP post object as returned by the metabox API
742 */
743 public function chart_meta_box( $post ) {
744 // Force an instance of 1 since we NEVER show more than one chart at a time inside the admin panel
745 m_chart()->instance = 1;
746
747 $post_meta = m_chart()->get_post_meta( $post->ID );
748 $image = m_chart()->get_chart_image( $post->ID );
749 $settings = m_chart()->get_settings();
750
751 require_once __DIR__ . '/templates/chart-meta-box.php';
752 }
753
754 /**
755 * Insert CSV Import and Export forms into the footer when editing charts
756 */
757 public function admin_footer() {
758 $screen = get_current_screen();
759
760 if ( 'post' !== $screen->base || m_chart()->slug !== $screen->post_type ) {
761 return;
762 }
763 ?>
764 <script type="text/javascript">
765 <?php do_action( 'm_chart_admin_footer_javascript' ); ?>
766 </script>
767 <?php
768 }
769
770 /**
771 * Inserts a subtitle field under the title field on the chart edit form
772 *
773 * @param object the WP post object as returned by the metabox API
774 */
775 public function edit_form_before_permalink( $post ) {
776 if ( m_chart()->slug !== $post->post_type ) {
777 return;
778 }
779
780 echo '<div id="m-chart-subtitle-root"></div>';
781 }
782
783 /**
784 * Display some additional information about a chart
785 *
786 * @param string the name of the custom column being displayed
787 * @param string the $post_id of the post being displayed in this row
788 */
789 public function manage_posts_custom_column( $column, $post_id ) {
790 if ( m_chart()->slug . '-type' !== $column && m_chart()->slug . '-library' !== $column ) {
791 return;
792 }
793
794 $library = m_chart()->get_post_meta( $post_id, 'library' );
795 $library_instance = m_chart()->library( $library );
796
797 if ( ! $library_instance || $library_instance->library !== $library ) {
798 ?>
799 <span aria-hidden="true"></span>
800 <span class="screen-reader-text"><?php echo esc_html__( 'Library not found', 'm-chart' ); ?></span>
801 <?php
802 return;
803 }
804
805 if ( m_chart()->slug . '-type' === $column ) {
806 $type = m_chart()->get_post_meta( $post_id, 'type' );
807 $type_name = $library_instance->type_option_names[ $type ];
808 ?>
809 <span class="type <?php echo esc_attr( $type ); ?>" title="<?php echo esc_attr( $type_name ); ?>">
810 <?php echo esc_html( $type_name ); ?>
811 </span>
812 <?php
813 }
814
815 if ( m_chart()->slug . '-library' === $column ) {
816 $library_name = $library_instance->library_name;
817 ?>
818 <span class="library <?php echo esc_attr( $library ); ?>" title="<?php echo esc_attr( $library_name ); ?>">
819 <?php echo esc_html( $library_name ); ?>
820 </span>
821 <?php
822 }
823 }
824
825 /**
826 * Add the Chart.js admin settings to the M Chart Settings page
827 */
828 public function m_chart_settings_admin() {
829 $settings = m_chart()->get_settings();
830 require __DIR__ . '/templates/m-chart-settings-chartjs.php';
831 }
832
833 /**
834 * Add our custom column to the array of columns for charts
835 *
836 * @param array the array of columns
837 *
838 * @return array array of columns with the custom column added
839 */
840 public function manage_posts_columns( $columns ) {
841 $new_columns = [];
842
843 foreach ( $columns as $column => $name ) {
844 $new_columns[ $column ] = $name;
845
846 if ( 'author' === $column || 'coauthors' === $column ) {
847 $new_columns[ m_chart()->slug . '-type' ] = 'Type';
848
849 if ( 'yes' === m_chart()->get_settings( 'show_library' ) ) {
850 $new_columns[ m_chart()->slug . '-library' ] = 'Library';
851 }
852 }
853 }
854
855 return $new_columns;
856 }
857
858 /**
859 * Hook to save_post action and save chart related post meta
860 *
861 * @param int the WP post ID of the post being saved
862 */
863 public function save_post( $post_id ) {
864 $post = get_post( $post_id );
865
866 // Check that this isn't an autosave
867 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
868 return;
869 }
870
871 // Check post type
872 if ( ! isset( $post->post_type ) || m_chart()->slug !== $post->post_type ) {
873 return;
874 }
875
876 // Don't run on post revisions (almost always happens just before the real post is saved)
877 if ( wp_is_post_revision( $post->ID ) ) {
878 return;
879 }
880
881 // Make sure we've got some actual M Chart related data in the $_POST array
882 if ( ! isset( $_POST[ m_chart()->slug ] ) ) {
883 return;
884 }
885
886 // Check the nonce
887 $nonce = $_POST[ m_chart()->slug ]['nonce'] ?? '';
888
889 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
890 return;
891 }
892
893 unset( $_POST[ m_chart()->slug ]['nonce'] );
894
895 // Check the permissions
896 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
897 return;
898 }
899
900 // If there's an image being passed attach it to the chart post
901 $this->attach_image();
902
903 // Make sure we don't overwrite existing settings in the case someone hits update too quickly
904 if (
905 isset( $_POST[ m_chart()->slug ]['library'] )
906 // Make sure the library value is clean and valid before trying to use it
907 && m_chart()->is_valid_library( $_POST[ m_chart()->slug ]['library'] )
908 ) {
909 $library = sanitize_key( $_POST[ m_chart()->slug ]['library'] );
910
911 // Load the library in question in case there's a filter/action we'll need
912 m_chart()->library( $library );
913
914 // update_post_meta passes the $_POST values directly to validate_post_meta
915 // validate_post_meta returns only valid post meta values and does data validation on each item
916 m_chart()->update_post_meta( $post->ID, $_POST[ m_chart()->slug ] );
917 }
918 }
919
920 /**
921 * Attach a given image to a chart post
922 *
923 * @param int the WP post ID of the post being saved
924 * @param string a base64 encoded string of the image we want to attach
925 */
926 public function attach_image() {
927 $settings = m_chart()->get_settings();
928
929 // If the performance setting isn't turned to default we don't do this
930 if ( 'default' !== $settings['performance'] ) {
931 return false;
932 }
933
934 if ( ! is_numeric( $_POST['post_ID'] ?? '' ) ) {
935 return false;
936 }
937
938 $post_id = absint( $_POST['post_ID'] );
939
940 // Make sure the library used on this post supports images
941 if ( 'no' === apply_filters( 'm_chart_image_support', 'no', m_chart()->get_post_meta( $post_id, 'library' ) ) ) {
942 return false;
943 }
944
945 if ( ! current_user_can( 'edit_post', $post_id ) ) {
946 return false;
947 }
948
949 if ( ! $post = get_post( $post_id ) ) {
950 return false;
951 }
952
953 $img_data = $_POST[ m_chart()->slug ]['img'] ?? '';
954
955 if ( '' === $img_data ) {
956 return false;
957 }
958
959 // Decode the image so we can save it
960 $decoded_img = base64_decode( str_replace( 'data:image/png;base64,', '', $img_data ) );
961
962 // Reject anything that isn't a real PNG before writing it to disk
963 if ( false === $decoded_img || '' === $decoded_img || "\x89PNG\r\n\x1a\n" !== substr( $decoded_img, 0, 8 ) ) {
964 return false;
965 }
966
967 // Cap the image size at 5MB to keep a runaway client from filling disk
968 if ( strlen( $decoded_img ) > 5 * 1024 * 1024 ) {
969 return false;
970 }
971
972 // Check for an existing attached image
973 $attachments = get_posts(
974 [
975 'post_type' => 'attachment',
976 'posts_per_page' => 1,
977 'post_parent' => $post->ID,
978 'meta_key' => m_chart()->slug . '-image',
979 ]
980 );
981
982 // If an existing image was found delete it
983 foreach ( $attachments as $attachment ) {
984 wp_delete_attachment( $attachment->ID, true );
985 }
986
987 // Upload image to WP
988 $file = wp_upload_bits( sanitize_title( $post->post_title . '-' . $post->ID ) . '.png', null, $decoded_img );
989
990 // START acting like media_sideload_image
991 preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file['file'], $matches );
992
993 $file_array['name'] = basename( $matches[0] );
994 $file_array['tmp_name'] = $file['file'];
995
996 if ( is_wp_error( $file ) ) {
997 @unlink( $file_array['tmp_name'] );
998 $file_array['tmp_name'] = '';
999 }
1000
1001 $img_id = media_handle_sideload( $file_array, $post->ID, $post->post_title );
1002
1003 if ( is_wp_error( $img_id ) ) {
1004 @unlink( $file_array['tmp_name'] );
1005 return $img_id;
1006 }
1007 // STOP acting like media_sideload_image
1008
1009 // Set some meta on the attachment so we know it came from m-chart
1010 add_post_meta( $img_id, m_chart()->slug . '-image', $post->ID );
1011
1012 // Set the attachment as the chart's thumbnail
1013 update_post_meta( $post->ID, '_thumbnail_id', $img_id );
1014 }
1015
1016 /**
1017 * Parses an incoming CSV file and compiles it into an array
1018 *
1019 * @return array an array of the data from the imported CSV file ready for use in the chart meta
1020 */
1021 public function ajax_import_csv() {
1022 $post = get_post( absint( $_POST['post_id'] ?? 0 ) );
1023
1024 // Check post type
1025 if ( ! isset( $post->post_type ) || m_chart()->slug !== $post->post_type ) {
1026 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
1027 }
1028
1029 // Check the nonce
1030 $nonce = $_POST['nonce'] ?? '';
1031
1032 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
1033 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
1034 }
1035
1036 // Check the permissions
1037 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1038 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
1039 }
1040
1041 // Make sure there's a CSV file
1042 if ( empty( $_FILES['import_csv_file']['name'] ) ) {
1043 wp_send_json_error( esc_html__( 'No file to import', 'm-chart' ) );
1044 }
1045
1046 // Check upload-level errors first
1047 if ( UPLOAD_ERR_OK !== ( $_FILES['import_csv_file']['error'] ?? UPLOAD_ERR_NO_FILE ) ) {
1048 wp_send_json_error( esc_html__( 'File upload error', 'm-chart' ) );
1049 }
1050
1051 if ( ! is_uploaded_file( $_FILES['import_csv_file']['tmp_name'] ) ) {
1052 wp_send_json_error( esc_html__( 'Invalid upload', 'm-chart' ) );
1053 }
1054
1055 // Verify both extension AND MIME (some browsers send text/plain for CSV)
1056 $file_check = wp_check_filetype_and_ext(
1057 $_FILES['import_csv_file']['tmp_name'],
1058 $_FILES['import_csv_file']['name'],
1059 [ 'csv' => 'text/csv', 'csv-alt' => 'text/plain' ]
1060 );
1061
1062 if ( 'csv' !== ( $file_check['ext'] ?? '' ) ) {
1063 wp_send_json_error( esc_html__( 'Only CSV files can be imported', 'm-chart' ) );
1064 }
1065
1066 // Do some validation on the CSV file (mirroring what WP does for this sort of thing)
1067 $csv_file = realpath( $_FILES['import_csv_file']['tmp_name'] );
1068
1069 if ( ! $csv_file ) {
1070 wp_send_json_error( esc_html__( 'File path not found', 'm-chart' ) );
1071 }
1072
1073 // Cap file size at 2MB to prevent resource exhaustion
1074 if ( filesize( $csv_file ) > 2 * 1024 * 1024 ) {
1075 wp_send_json_error( esc_html__( 'CSV file too large (max 2MB)', 'm-chart' ) );
1076 }
1077
1078 $csv_data = file_get_contents( $csv_file );
1079
1080 if ( '' === $csv_data ) {
1081 wp_send_json_error( esc_html__( 'CSV file was empty', 'm-chart' ) );
1082 }
1083
1084 // Get parseCSV library so we can use it to convert the CSV to a nice array
1085 // Yes, PHP does this natively now but I've run into trouble with malformed CSV that parsCSV handles fine
1086 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
1087
1088 $parse_csv = new parseCSV();
1089
1090 // The "\n" before and after is to deal with CSV files that don't have line breaks above and below the data
1091 // Which then seems to confuse parseCSV occasionally
1092 $csv_data = "\n" . trim( $csv_data ) . "\n";
1093
1094 // Set delimiter but check to make sure it's safe first
1095 $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' );
1096
1097 // Parse the CSV
1098 $parse_csv->parse( $csv_data );
1099
1100 // This deals with Google Doc's crappy CSV exports which don't include columns at the end of a row if they are empty
1101 $data_array = $this->fix_csv_data( $parse_csv->data );
1102
1103 wp_send_json_success( $data_array );
1104 }
1105
1106 /**
1107 * Helper function makes sure that the data array has matching numbers of array elements for each row
1108 * 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?)
1109 *
1110 * @param array an array of data as returned from the parseCSV class
1111 *
1112 * @return array the array of data with matching array value counts
1113 */
1114 public function fix_csv_data( $data_array ) {
1115 $count = 0;
1116
1117 // Get largest row count
1118 foreach ( $data_array as $data ) {
1119 $temp_count = count( $data );
1120
1121 $count = ( $temp_count > $count ) ? $temp_count : $count;
1122 }
1123
1124 // Fix arrays so value counts match
1125 foreach ( $data_array as $key => $data ) {
1126 $temp_count = count( $data );
1127
1128 if ( $temp_count < $count ) {
1129 $difference = $count - $temp_count;
1130
1131 for ( $i = 0; $i < $difference; $i++ ) {
1132 $data_array[ $key ][] = '';
1133 }
1134 }
1135 }
1136
1137 return $data_array;
1138 }
1139
1140 /**
1141 * Converts data array into CSV and outputs it to the browser
1142 */
1143 public function ajax_export_csv() {
1144 $post_id = $_REQUEST['post_id'] ?? '';
1145 $nonce = $_REQUEST['nonce'] ?? '';
1146
1147 if (
1148 ! is_numeric( $post_id )
1149 || ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' )
1150 || ! current_user_can( 'edit_post', absint( $post_id ) )
1151 ) {
1152 wp_die( esc_html__( 'Unauthorized access', 'm-chart' ), esc_html__( 'You do not have permission to do that', 'm-chart' ), [ 'response' => 401 ] );
1153 }
1154
1155 $post = get_post( absint( $post_id ) );
1156
1157 // If the user passed a data value in their request we'll use it after validation
1158 if ( isset( $_POST['data'] ) && isset( $_POST['title'] ) ) {
1159 $data = m_chart()->validate_data( json_decode( stripslashes( $_POST['data'] ) ) );
1160 $file_name = sanitize_title( $_POST['title'] );
1161 } else {
1162 $data = m_chart()->get_post_meta( $post->ID, 'data' );
1163 $file_name = sanitize_title( get_the_title( $post->ID ) );
1164 }
1165
1166 $set_name = sanitize_title( $_REQUEST['set_name'] ?? '' );
1167
1168 if ( empty( $data ) ) {
1169 return;
1170 }
1171
1172 // Prevent CSV/formula injection by prefixing any cell that begins with a formula trigger so spreadsheet apps see it as a literal string
1173 array_walk_recursive( $data, function ( &$cell ) {
1174 $cell = $this->neutralize_csv_cell( $cell );
1175 } );
1176
1177 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
1178 $parse_csv = new parseCSV();
1179
1180 // Set delimiter
1181 $parse_csv->output_delimiter = m_chart()->get_settings( 'csv_delimiter' );
1182
1183 $parse_csv->output( $file_name . '-' . $set_name . '.csv', $data );
1184 die;
1185 }
1186
1187 /**
1188 * Prefix a cell value with a single quote when it starts with a character that Excel/Sheets/Numbers interpret as a formula trigger
1189 *
1190 * @param mixed $cell The raw cell value
1191 * @return string The cell value, prefixed with ' if it would otherwise execute
1192 */
1193 public function neutralize_csv_cell( $cell ) {
1194 $cell = (string) $cell;
1195
1196 if ( '' !== $cell && in_array( $cell[0], [ '=', '+', '-', '@', "\t", "\r" ], true ) ) {
1197 return "'" . $cell;
1198 }
1199
1200 return $cell;
1201 }
1202
1203 /**
1204 * Returns JSON encoded chart args from $_POST values sent from the admin panel
1205 *
1206 * @return string a JSON encoded string containing all of the chart args needed to update an active chart
1207 */
1208 public function ajax_get_chart_args() {
1209 // Check the nonce
1210 $nonce = $_POST['nonce'] ?? '';
1211
1212 if ( ! wp_verify_nonce( $nonce, m_chart()->slug . '-save-post' ) ) {
1213 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
1214 }
1215
1216 // Does the post exist? (post_id is 0 for new charts that haven't been saved yet)
1217 $post_id = absint( $_POST['post_id'] ?? 0 );
1218
1219 if ( $post_id ) {
1220 if ( ! $post = get_post( $post_id ) ) {
1221 wp_send_json_error( esc_html__( 'Invalid post', 'm-chart' ) );
1222 }
1223
1224 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1225 wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) );
1226 }
1227 } else {
1228 // New chart — no saved post yet, build a stub so the library can compute chart args
1229 if ( ! current_user_can( 'edit_posts' ) ) {
1230 wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) );
1231 }
1232
1233 $post = new WP_Post( (object) [
1234 'ID' => 0,
1235 'post_title' => '',
1236 'post_type' => m_chart()->slug,
1237 'post_status' => 'auto-draft',
1238 ] );
1239 }
1240
1241 // Is this a valid library?
1242 $library_slug = $_POST['library'] ?? '';
1243
1244 if ( ! m_chart()->is_valid_library( $library_slug ) ) {
1245 wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) );
1246 }
1247
1248 // This does get potentially overwritten later on
1249 // However, it's necessary for initial load on a new chart
1250 if ( 'highcharts' === $library_slug ) {
1251 $library = m_chart()->library( $library_slug );
1252 }
1253
1254 $library = apply_filters( 'm_chart_library_class', m_chart()->library_class, $library_slug );
1255
1256 // Make sure a third-party filter didn't replace the library with something unusable
1257 if ( ! is_object( $library ) || ! method_exists( $library, 'get_chart_args' ) ) {
1258 wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) );
1259 }
1260
1261 // Set these values so that get_chart_args has them already available before we call it
1262 $library->args = m_chart()->get_chart_default_args;
1263 $library->post = $post;
1264 $library->post->post_title = sanitize_text_field( $_POST['title'] ?? '' );
1265
1266 // validate_post_meta returns only valid post meta values and does data validation on each item
1267 $library->post_meta = m_chart()->validate_post_meta( $_POST['post_meta'] ?? [] );
1268
1269 wp_send_json_success( $library->get_chart_args( $library->post->ID, $library->args, true, false ) );
1270 }
1271
1272 /**
1273 * Return a name spaced field name
1274 *
1275 * @param string the field name we want to name space
1276 *
1277 * @param string a name spaced field name
1278 */
1279 public function get_field_name( $field_name, $parent_field_name = '' ) {
1280 if ( '' !== $parent_field_name ) {
1281 return m_chart()->slug . '[' . $parent_field_name . ']' . '[' . $field_name . ']';
1282 }
1283
1284 return m_chart()->slug . '[' . $field_name . ']';
1285 }
1286
1287 /**
1288 * Return a name spaced field id
1289 *
1290 * @param string the field id we want to name space
1291 *
1292 * @param string a name spaced field id
1293 */
1294 public function get_field_id( $field_name ) {
1295 return m_chart()->slug . '-' . $field_name;
1296 }
1297 }
1298