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

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

670 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class M_Chart_Admin {
4 private $safe_settings = array(
5 'performance' => array(
6 'default',
7 'no-images',
8 'no-preview',
9 ),
10 );
11
12 /**
13 * Constructor
14 */
15 public function __construct() {
16 $this->plugin_url = m_chart()->plugin_url();
17
18 add_action( 'admin_init', array( $this, 'admin_init' ) );
19 add_action( 'admin_menu', array( $this, 'admin_menu' ) );
20 add_action( 'current_screen', array( $this, 'current_screen' ) );
21 add_action( 'admin_footer', array( $this, 'admin_footer' ) );
22 add_action( 'wp_ajax_m_chart_export_csv', array( $this, 'ajax_export_csv' ) );
23 add_action( 'wp_ajax_m_chart_get_chart_args', array( $this, 'ajax_get_chart_args' ) );
24 add_action( 'wp_ajax_m_chart_import_csv', array( $this, 'ajax_import_csv' ) );
25 add_action( 'edit_form_before_permalink', array( $this, 'edit_form_before_permalink' ) );
26 }
27
28 /**
29 * Register a Shortcake ui if we can and look for save settings submissions
30 */
31 public function admin_init() {
32 $this->save_settings();
33
34 if ( ! function_exists( 'shortcode_ui_register_for_shortcode' ) ) {
35 return;
36 }
37
38 shortcode_ui_register_for_shortcode(
39 'chart',
40 array(
41 'label' => esc_html__( 'Charts', 'm-chart' ),
42 'listItemImage' => 'dashicons-chart-pie',
43 'attrs' => array(
44 array(
45 'label' => esc_html__( 'Chart', 'm-chart' ),
46 'attr' => 'id',
47 'type' => 'post_select',
48 'query' => array(
49 'post_type' => m_chart()->slug,
50 'post_status' => 'publish',
51 ),
52 ),
53 ),
54 )
55 );
56 }
57
58 /**
59 * Add settings admin page
60 */
61 public function admin_menu() {
62 add_submenu_page(
63 'edit.php?post_type=' . m_chart()->slug,
64 esc_html__( 'M Chart Settings', 'm-chart' ),
65 esc_html__( 'Settings', 'm-chart' ),
66 'manage_options',
67 m_chart()->slug . '-settings',
68 array( $this, 'm_chart_settings' )
69 );
70 }
71
72 /**
73 * Display the M Chart settings admin page
74 */
75 public function m_chart_settings() {
76 $settings = m_chart()->get_settings();
77 require_once __DIR__ . '/templates/m-chart-settings.php';
78 }
79
80 /**
81 * Check for and save M Chart settings
82 */
83 public function save_settings() {
84 if ( ! current_user_can( 'manage_options' ) ) {
85 return;
86 }
87
88 // Check the nonce
89 if (
90 ! isset( $_POST[ m_chart()->slug ] )
91 || ! wp_verify_nonce( $_POST[ m_chart()->slug ]['nonce'], m_chart()->slug . '-save-settings' )
92 ) {
93 return;
94 }
95
96 $validated_settings = array();
97 $submitted_settings = $_POST[ m_chart()->slug ];
98
99 foreach ( m_chart()->settings as $setting => $default ) {
100 if ( ! isset( $submitted_settings[ $setting ] ) ) {
101 $validated_settings[ $setting ] = $default;
102 continue;
103 }
104
105 if ( isset( $safe_settings[ $setting ] ) ) {
106 // If we've got an array of valid values lets check against that
107 $safe_setting = $safe_settings[ $setting ];
108
109 if ( in_array( $submitted_settings[ $setting ], $safe_setting, true ) ) {
110 $validated_settings[ $setting ] = $submitted_settings[ $setting ];
111 } else {
112 $validated_settings[ $setting ] = $default;
113 }
114 } else {
115 // Make sure the value is safe before attempting to save it
116 if ( preg_match('#^[a-zA-Z0-9-_]+$#', $submitted_settings[ $setting ] ) ) {
117 $validated_settings[ $setting ] = $submitted_settings[ $setting ];
118 } else {
119 $validated_settings[ $setting ] = $default;
120 }
121 }
122 }
123
124 update_option( m_chart()->slug, $validated_settings );
125
126 add_action( 'admin_notices', array( $this, 'save_success' ) );
127 }
128
129 /**
130 * Display an admin notice that the settings have been saved
131 */
132 public function save_success() {
133 ?>
134 <div class="updated notice">
135 <p><?php esc_html_e( 'Settings saved', 'm-chart' ); ?></p>
136 </div>
137 <?php
138 }
139
140 /**
141 * Load CSS/Javascript necessary for the interface
142 *
143 * @param object the current screen object as passed by the current_screen action hook
144 */
145 public function current_screen( $screen ) {
146 if ( m_chart()->slug != $screen->post_type ) {
147 return;
148 }
149
150 // Only load these if we are on a post page
151 if ( 'post' == $screen->base ) {
152 // Handsontable
153 wp_enqueue_style(
154 'handsontable',
155 $this->plugin_url . '/components/external/handsontable/handsontable.css'
156 );
157
158 wp_enqueue_script(
159 'handsontable',
160 $this->plugin_url . '/components/external/handsontable/handsontable.js',
161 array( 'jquery' )
162 );
163
164 // Highcharts export.js is required for the image generation
165 wp_enqueue_script(
166 'highcharts-exporting',
167 $this->plugin_url . '/components/external/highcharts/exporting.js',
168 array( 'highcharts', 'jquery' )
169 );
170
171 // canvg and rgbcolo do the SVG -> Canvas conversion
172 wp_enqueue_script(
173 'rgbcolor',
174 $this->plugin_url . '/components/external/canvg/rgbcolor.js'
175 );
176
177 wp_enqueue_script(
178 'canvg',
179 $this->plugin_url . '/components/external/canvg/canvg.js',
180 array( 'rgbcolor' )
181 );
182
183 // Admin panel JS
184 wp_enqueue_script(
185 'm-chart-admin',
186 $this->plugin_url . '/components/js/m-chart-admin.js',
187 array( 'highcharts', 'jquery' )
188 );
189
190 $settings = m_chart()->get_settings();
191
192 wp_localize_script(
193 'm-chart-admin',
194 'm_chart_admin',
195 array(
196 'refresh_counter' => 0,
197 'allow_form_submission' => false,
198 'request' => false,
199 'performance' => $settings['performance'],
200 )
201 );
202 }
203
204 // Admin panel CSS
205 wp_enqueue_style(
206 'm-chart-admin',
207 $this->plugin_url . '/components/css/m-chart-admin.css'
208 );
209 }
210
211 /**
212 * Add all of the metaboxes needed for the data and chart editing interface
213 */
214 public function meta_boxes() {
215 global $wp_meta_boxes;
216
217 // Remove excerpt from it's normal spot in the meta_boxes array so we can put it back in after the spreadsheet
218 // Users can move metaboxes, but this helps put things in a reasonable place on the first visit
219 $excerpt = $wp_meta_boxes[ m_chart()->slug ]['normal']['core']['postexcerpt'];
220 unset( $wp_meta_boxes[ m_chart()->slug ]['normal']['core']['postexcerpt'] );
221
222 add_meta_box(
223 m_chart()->slug . '-spreadsheet',
224 esc_html__( 'Data', 'm-chart' ),
225 array( $this, 'spreadsheet_meta_box' ),
226 m_chart()->slug,
227 'normal',
228 'high'
229 );
230
231 add_meta_box(
232 m_chart()->slug,
233 esc_html__( 'Chart', 'm-chart' ),
234 array( $this, 'chart_meta_box' ),
235 m_chart()->slug,
236 'normal',
237 'high'
238 );
239
240 $wp_meta_boxes[ m_chart()->slug ]['normal']['high']['postexcerpt'] = $excerpt;
241
242 add_meta_box(
243 m_chart()->slug . '-csv',
244 esc_html__( 'CSV Import/Export', 'm-chart' ),
245 array( $this, 'csv_meta_box' ),
246 m_chart()->slug,
247 'normal',
248 'high'
249 );
250
251 // We are using our own interface for the units so we can remove the units taxonomy metabox
252 remove_meta_box( m_chart()->slug . '-unitsdiv', m_chart()->slug, 'side' );
253 }
254
255 /**
256 * Displays the spread sheet meta box
257 *
258 * @param object the WP post object as returned by the metabox API
259 */
260 public function spreadsheet_meta_box( $post ) {
261 $post_meta = m_chart()->get_post_meta( $post->ID );
262
263 // Setup default empty sheet data if needed
264 $sheet_data = empty( $post_meta['data'] ) ? array( array( '' ) ) : $post_meta['data'];
265
266 require_once __DIR__ . '/templates/spreadsheet-meta-box.php';
267 }
268
269 /**
270 * Displays the chart meta box
271 *
272 * @param object the WP post object as returned by the metabox API
273 */
274 public function chart_meta_box( $post ) {
275 $chart = m_chart()->get_chart( $post->ID );
276 $post_meta = m_chart()->get_post_meta( $post->ID );
277 $image = m_chart()->get_chart_image( $post->ID );
278 $settings = m_chart()->get_settings();
279
280 require_once __DIR__ . '/templates/chart-meta-box.php';
281 }
282
283 /**
284 * Displays the CSV Import/Export controls
285 *
286 * @param object the WP post object as returned by the metabox API
287 */
288 public function csv_meta_box( $post ) {
289 require_once __DIR__ . '/templates/csv-meta-box.php';
290 }
291
292 /**
293 * Insert CSV Import and Export forms into the footer when editing charts
294 */
295 public function admin_footer() {
296 $screen = get_current_screen();
297
298 if ( 'post' != $screen->base || m_chart()->slug != $screen->post_type ) {
299 return;
300 }
301 ?>
302 <form id="<?php echo esc_attr( $this->get_field_id( 'csv-import-form' ) ); ?>" style="display: none;">
303 <input type="file" name="import_csv_file" id="<?php echo esc_attr( $this->get_field_id( 'csv-file' ) ); ?>" class="hide" />
304 </form>
305 <form action="<?php echo esc_url( admin_url( 'admin-ajax.php?action=m_chart_export_csv' ) ); ?>" id="<?php echo esc_attr( $this->get_field_id( 'csv-export-form' ) ); ?>" style="display: none;" method="post">
306 <input type="hidden" name="post_id" value="" id="<?php echo esc_attr( $this->get_field_id( 'csv-post-id' ) ); ?>" />
307 <input type="hidden" name="data" value="" id="<?php echo esc_attr( $this->get_field_id( 'csv-data' ) ); ?>" />
308 <input type="hidden" name="title" value="" id="<?php echo esc_attr( $this->get_field_id( 'csv-title' ) ); ?>" />
309 </form>
310 <script type="text/javascript">
311 <?php do_action( 'm_chart_admin_footer_javascript' ); ?>
312 </script>
313 <?php
314 }
315
316 /**
317 * Inserts a subtitle field under the title field on the chart edit form
318 *
319 * @param object the WP post object as returned by the metabox API
320 */
321 public function edit_form_before_permalink( $post ) {
322 if ( m_chart()->slug != $post->post_type ) {
323 return;
324 }
325
326 $post_meta = m_chart()->get_post_meta( $post->ID );
327
328 require_once __DIR__ . '/templates/subtitle-field.php';
329 }
330
331 /**
332 * Hook to save_post action and save chart related post meta
333 *
334 * @param int the WP post ID of the post being saved
335 */
336 public function save_post( $post_id ) {
337 $post = get_post( $post_id );
338
339 // Check that this isn't an autosave
340 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
341 return;
342 }
343
344 // Check post type
345 if ( ! isset( $post->post_type ) || m_chart()->slug != $post->post_type ) {
346 return;
347 }
348
349 // Don't run on post revisions (almost always happens just before the real post is saved)
350 if ( wp_is_post_revision( $post->ID ) ) {
351 return;
352 }
353
354 // Make sure we've got some actual M Chart related data in the $_POST array
355 if ( ! isset( $_POST[ m_chart()->slug ] ) ) {
356 return;
357 }
358
359 // Check the nonce
360 if ( ! wp_verify_nonce( $_POST[ m_chart()->slug ]['nonce'], m_chart()->slug . '-save-post' ) ) {
361 return;
362 }
363
364 unset( $_POST[ m_chart()->slug ]['nonce'] );
365
366 // Check the permissions
367 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
368 return;
369 }
370
371 // If there's an image being passed attach it to the chart post
372 $this->attach_image();
373
374 // Make sure we don't overrwrite existing settings in the case someone hits update too quickly
375 if (
376 isset( $_POST[ m_chart()->slug ]['library'] )
377 // Make sure the library value is clean and valid before trying to use it
378 && m_chart()->is_valid_library( $_POST[ m_chart()->slug ]['library'] )
379 ) {
380 // Load the library in question in case there's a filter/action we'll need
381 if ( 'highcharts' == $_POST[ m_chart()->slug ]['library'] ) {
382 m_chart()->highcharts();
383 }
384
385 // update_post_meta passes the $_POST values directly to validate_post_meta
386 // validate_post_meta returns only valid post meta values and does data validation on each item
387 m_chart()->update_post_meta( $post->ID, $_POST[ m_chart()->slug ] );
388 }
389 }
390
391 /**
392 * Attach a given image to a chart post
393 *
394 * @param int the WP post ID of the post being saved
395 * @param string a base64 encoded string of the image we want to attach
396 */
397 public function attach_image() {
398 $settings = m_chart()->get_settings();
399
400 // If the performance setting isn't turned to default we don't do this
401 if ( 'default' != $settings['performance'] ) {
402 return;
403 }
404
405 if ( ! is_numeric( $_POST['post_ID'] ) ) {
406 return;
407 }
408
409 $post_id = absint( $_POST['post_ID'] );
410
411 if ( ! current_user_can( 'edit_post', $post_id ) ) {
412 return false;
413 }
414
415 if ( ! $post = get_post( $post_id ) ) {
416 return false;
417 }
418
419 if ( '' == $_POST[ m_chart()->slug ]['img'] ) {
420 return false;
421 }
422
423 // Decode the image so we can save it
424 $decoded_img = base64_decode( str_replace( 'data:image/png;base64,', '', $_POST[ m_chart()->slug ]['img'] ) );
425
426 if ( '' == $decoded_img ) {
427 return false;
428 }
429
430 // Check for an existing attached image
431 $attachments = get_posts(
432 array(
433 'post_type' => 'attachment',
434 'posts_per_page' => 1,
435 'post_parent' => $post->ID,
436 'meta_key' => m_chart()->slug . '-image',
437 )
438 );
439
440 // If an existing image was found delete it
441 foreach ( $attachments as $attachment ) {
442 wp_delete_attachment( $attachment->ID, true );
443 }
444
445 // Upload image to WP
446 $file = wp_upload_bits( sanitize_title( $post->post_title . '-' . $post->ID ) . '.png', null, $decoded_img );
447
448 // START acting like media_sideload_image
449 preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file['file'], $matches );
450
451 $file_array['name'] = basename( $matches[0] );
452 $file_array['tmp_name'] = $file['file'];
453
454 if ( is_wp_error( $file ) ) {
455 @unlink( $file_array['tmp_name'] );
456 $file_array['tmp_name'] = '';
457 }
458
459 $img_id = media_handle_sideload( $file_array, $post->ID, $post->post_title );
460
461 if ( is_wp_error( $img_id ) ) {
462 @unlink( $file_array['tmp_name'] );
463 return $img_id;
464 }
465 // STOP acting like media_sideload_image
466
467 // Set some meta on the attachment so we know it came from m-chart
468 add_post_meta( $img_id, m_chart()->slug . '-image', $post->ID );
469
470 // Set the attachment as the chart's thumbnail
471 update_post_meta( $post->ID, '_thumbnail_id', $img_id );
472 }
473
474 /**
475 * Parses an incoming CSV file and compiles it into an array
476 *
477 * @return array an array fo the data from the imported CSV file ready for use in the chart meta
478 */
479 public function ajax_import_csv() {
480 $post = get_post( absint( $_POST['post_id'] ) );
481
482 // Check post type
483 if ( ! isset( $post->post_type ) || m_chart()->slug != $post->post_type ) {
484 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
485 }
486
487 // Check the nonce
488 if ( ! wp_verify_nonce( $_POST['nonce'], m_chart()->slug . '-save-post' ) ) {
489 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
490 }
491
492 // Check the permissions
493 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
494 wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) );
495 }
496
497 // Make sure there's a CSV file
498 if ( empty( $_FILES ) || ! isset( $_FILES['import_csv_file']['name'] ) ) {
499 wp_send_json_error( esc_html__( 'No file to import', 'm-chart' ) );
500 }
501
502 // Make sure the file is a CSV file
503 $file_ext = strtolower( pathinfo( $_FILES['import_csv_file']['name'], PATHINFO_EXTENSION ) );
504
505 if ( 'csv' != $file_ext ) {
506 wp_send_json_error( esc_html__( 'Only CSV files can be imported ', 'm-chart' ) );
507 }
508
509 // Do some validation on the CSV file (mirroring what WP does for this sort of thing)
510 $csv_file = realpath( $_FILES['import_csv_file']['tmp_name'] );
511
512 if ( ! $csv_file ) {
513 wp_send_json_error( esc_html__( 'File path not found', 'm-chart' ) );
514 }
515
516 $csv_data = file_get_contents( $csv_file );
517
518 if ( '' == $csv_data ) {
519 wp_send_json_error( esc_html__( 'CSV file was empty', 'm-chart' ) );
520 }
521
522 // Get parseCSV library so we can use it to convert the CSV to a nice array
523 // Yes, PHP does this natively now but I've run into trouble with malformed CSV that parsCSV handles fine
524 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
525
526 $parse_csv = new parseCSV();
527
528 // The "\n" before and after is to deal with CSV files that don't have line breaks above and below the data
529 // Which then seems to confuse parseCSV occasionally
530 $parse_csv->parse( "\n" . trim( $csv_data ) . "\n" );
531
532 // This deals with Google Doc's crappy CSV exports which don't include columns at the end of a row if they are empty
533 $data_array = $this->fix_csv_data_array( $parse_csv->data );
534
535 wp_send_json_success( $data_array );
536 }
537
538 /**
539 * Helper function makes sure that the data array has matching numbers of array elements for each row
540 * 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?)
541 *
542 * @param array an array of data as returned from the parseCSV class
543 *
544 * @param array the array of data with matching array value counts
545 */
546 public function fix_csv_data_array( $data_array ) {
547 $count = 0;
548
549 // Get largest row count
550 foreach ( $data_array as $data ) {
551 $temp_count = count( $data );
552
553 $count = ( $temp_count > $count ) ? $temp_count : $count;
554 }
555
556 // Fix arrays so value counts match
557 foreach ( $data_array as $key => $data ) {
558 $temp_count = count( $data );
559
560 if ( $temp_count < $count ) {
561 $difference = $count - $temp_count;
562
563 for ( $i = 0; $i < $difference; $i++ ) {
564 $data_array[ $key ][] = '';
565 }
566 }
567 }
568
569 return $data_array;
570 }
571
572 /**
573 * Converts data array into CSV outputs it to the browser
574 */
575 public function ajax_export_csv() {
576 // Purposely using $_REQUEST here since this method can work via a GET and POST request
577 // POST requests are used when passing the data value since it's too big to pass via GET
578 if ( ! is_numeric( $_REQUEST['post_id'] ) || ! current_user_can( 'edit_post', absint( $_REQUEST['post_id'] ) ) ) {
579 wp_die( 'Unauthorized access', 'You do not have permission to do that', array( 'response' => 401 ) );
580 }
581
582 $post = get_post( absint( $_REQUEST['post_id'] ) );
583
584 // If the user passed a data value in their request we'll use it after validation
585 if ( isset( $_POST['data'] ) && isset( $_POST['title'] ) ) {
586 $data = m_chart()->validate_data( json_decode( stripslashes( $_POST['data'] ) ) );
587 $file_name = sanitize_title( $_POST['title'] );
588 }
589 else {
590 $data = m_chart()->get_post_meta( $post->ID, 'data' );
591 $file_name = sanitize_title( get_the_title( $post->ID ) );
592 }
593
594 if ( empty( $data ) ) {
595 return;
596 }
597
598 require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php';
599 $parse_csv = new parseCSV();
600
601 $parse_csv->output( $file_name . '.csv', $data );
602 die;
603 }
604
605 /**
606 * Returns JSON encoded chart args from $_POST values sent from the admin panel
607 *
608 * @return string a JSON encoded string containing all of the chart args needed to update an active chart
609 */
610 public function ajax_get_chart_args() {
611 // Check the nonce
612 if ( ! wp_verify_nonce( $_POST['nonce'], m_chart()->slug . '-save-post' ) ) {
613 wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) );
614 }
615
616 // Does the post exist?
617 if ( ! $post = get_post( absint( $_POST['post_id'] ) ) ) {
618 wp_send_json_error( esc_html__( 'Invalid post', 'm-chart' ) );
619 }
620
621 // Can the user edit this post?
622 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
623 wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) );
624 }
625
626 // Is this a valid library?
627 if ( ! m_chart()->is_valid_library( $_POST['library'] ) ) {
628 wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) );
629 }
630
631 if ( 'highcharts' == $_POST['library'] ) {
632 $library = m_chart()->highcharts();
633 }
634
635 // Set these values so that get_chart_args has them already available before we call it
636 $library->args = m_chart()->get_chart_default_args;
637 $library->post = (object) array(
638 'ID' => $post->ID,
639 'post_title' => sanitize_text_field( $_POST['title'] ),
640 );
641
642 // validate_post_meta returns only valid post meta values and does data validation on each item
643 $library->post_meta = m_chart()->validate_post_meta( $_POST['post_meta'] );
644
645 wp_send_json_success( $library->get_chart_args( $library->post->ID, $library->args, true, false ) );
646 }
647
648 /**
649 * Return a name spaced field name
650 *
651 * @param string the field name we want to name space
652 *
653 * @param string a name spaced field name
654 */
655 public function get_field_name( $field_name ) {
656 return m_chart()->slug . '[' . $field_name . ']';
657 }
658
659 /**
660 * Return a name spaced field id
661 *
662 * @param string the field id we want to name space
663 *
664 * @param string a name spaced field id
665 */
666 public function get_field_id( $field_name ) {
667 return m_chart()->slug . '-' . $field_name;
668 }
669 }
670