array( 'default', 'no-images', 'no-preview', ), ); /** * Constructor */ public function __construct() { $this->plugin_url = m_chart()->plugin_url(); add_action( 'admin_init', array( $this, 'admin_init' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); add_action( 'current_screen', array( $this, 'current_screen' ) ); add_action( 'admin_footer', array( $this, 'admin_footer' ) ); add_action( 'wp_ajax_m_chart_export_csv', array( $this, 'ajax_export_csv' ) ); add_action( 'wp_ajax_m_chart_get_chart_args', array( $this, 'ajax_get_chart_args' ) ); add_action( 'wp_ajax_m_chart_import_csv', array( $this, 'ajax_import_csv' ) ); } /** * Register a Shortcake ui if we can and look for save settings submissions */ public function admin_init() { $this->save_settings(); if ( ! function_exists( 'shortcode_ui_register_for_shortcode' ) ) { return; } shortcode_ui_register_for_shortcode( 'chart', array( 'label' => esc_html__( 'Charts', 'm-chart' ), 'listItemImage' => 'dashicons-chart-pie', 'attrs' => array( array( 'label' => esc_html__( 'Chart', 'm-chart' ), 'attr' => 'id', 'type' => 'post_select', 'query' => array( 'post_type' => m_chart()->slug, 'post_status' => 'publish', ), ), ), ) ); } /** * Add settings admin page */ public function admin_menu() { add_submenu_page( 'edit.php?post_type=' . m_chart()->slug, esc_html__( 'M Chart Settings', 'm-chart' ), esc_html__( 'Settings', 'm-chart' ), 'manage_options', m_chart()->slug . '-settings', array( $this, 'm_chart_settings' ) ); } /** * Display the M Chart settings admin page */ public function m_chart_settings() { $settings = m_chart()->get_settings(); require_once __DIR__ . '/templates/m-chart-settings.php'; } /** * Check for and save M Chart settings */ public function save_settings() { // Check the nonce if ( ! isset( $_POST[ m_chart()->slug ] ) || ! wp_verify_nonce( $_POST[ m_chart()->slug ]['nonce'], m_chart()->slug . '-save-settings' ) ) { return; } $validated_settings = array(); $submitted_settings = $_POST[ m_chart()->slug ]; foreach ( m_chart()->settings as $setting => $default ) { if ( ! isset( $submitted_settings[ $setting ] ) ) { $validated_settings[ $setting ] = $default; continue; } if ( isset( $safe_settings[ $setting ] ) ) { // If we've got an array of valid values lets check against that $safe_setting = $safe_settings[ $setting ]; if ( in_array( $submitted_settings[ $setting ], $safe_setting, true ) ) { $validated_settings[ $setting ] = $submitted_settings[ $setting ]; } else { $validated_settings[ $setting ] = $default; } } else { // Make sure the value is safe before attempting to save it if ( preg_match('#^[a-zA-Z0-9-_]+$#', $submitted_settings[ $setting ] ) ) { $validated_settings[ $setting ] = $submitted_settings[ $setting ]; } else { $validated_settings[ $setting ] = $default; } } } update_option( m_chart()->slug, $validated_settings ); add_action( 'admin_notices', array( $this, 'save_success' ) ); } /** * Display an admin notice that the settings have been saved */ public function save_success() { ?>

slug != $screen->post_type ) { return; } // Only load these if we are on a post page if ( 'post' == $screen->base ) { // Handsontable wp_enqueue_style( 'handsontable', $this->plugin_url . '/components/external/handsontable/handsontable.css' ); wp_enqueue_script( 'handsontable', $this->plugin_url . '/components/external/handsontable/handsontable.js', array( 'jquery' ) ); // Highcharts export.js is required for the image generation wp_enqueue_script( 'highcharts-exporting', $this->plugin_url . '/components/external/highcharts/exporting.js', array( 'highcharts', 'jquery' ) ); // canvg and rgbcolo do the SVG -> Canvas conversion wp_enqueue_script( 'rgbcolor', $this->plugin_url . '/components/external/canvg/rgbcolor.js' ); wp_enqueue_script( 'canvg', $this->plugin_url . '/components/external/canvg/canvg.js', array( 'rgbcolor' ) ); // Admin panel JS wp_enqueue_script( 'm-chart-admin', $this->plugin_url . '/components/js/m-chart-admin.js', array( 'highcharts', 'jquery' ) ); $settings = m_chart()->get_settings(); wp_localize_script( 'm-chart-admin', 'm_chart_admin', array( 'refresh_counter' => 0, 'allow_form_submission' => false, 'request' => false, 'performance' => $settings['performance'], ) ); } // Admin panel CSS wp_enqueue_style( 'm-chart-admin', $this->plugin_url . '/components/css/m-chart-admin.css' ); } /** * Add all of the metaboxes needed for the data and chart editing interface */ public function meta_boxes() { global $wp_meta_boxes; // Remove excerpt from it's normal spot in the meta_boxes array so we can put it back in after the spreadsheet // Users can move metaboxes, but this helps put things in a reasonable place on the first visit $excerpt = $wp_meta_boxes[ m_chart()->slug ]['normal']['core']['postexcerpt']; unset( $wp_meta_boxes[ m_chart()->slug ]['normal']['core']['postexcerpt'] ); add_meta_box( m_chart()->slug . '-spreadsheet', esc_html__( 'Data', 'm-chart' ), array( $this, 'spreadsheet_meta_box' ), m_chart()->slug, 'normal', 'high' ); add_meta_box( m_chart()->slug, esc_html__( 'Chart', 'm-chart' ), array( $this, 'chart_meta_box' ), m_chart()->slug, 'normal', 'high' ); $wp_meta_boxes[ m_chart()->slug ]['normal']['high']['postexcerpt'] = $excerpt; add_meta_box( m_chart()->slug . '-csv', esc_html__( 'CSV Import/Export', 'm-chart' ), array( $this, 'csv_meta_box' ), m_chart()->slug, 'normal', 'high' ); // We are using our own interface for the units so we can remove the units taxonomy metabox remove_meta_box( m_chart()->slug . '-unitsdiv', m_chart()->slug, 'side' ); } /** * Displays the spread sheet meta box * * @param object the WP post object as returned by the metabox API */ public function spreadsheet_meta_box( $post ) { $post_meta = m_chart()->get_post_meta( $post->ID ); // Setup default empty sheet data if needed $sheet_data = empty( $post_meta['data'] ) ? array( array( '' ) ) : $post_meta['data']; require_once __DIR__ . '/templates/spreadsheet-meta-box.php'; } /** * Displays the chart meta box * * @param object the WP post object as returned by the metabox API */ public function chart_meta_box( $post ) { $chart = m_chart()->get_chart( $post->ID ); $post_meta = m_chart()->get_post_meta( $post->ID ); $image = m_chart()->get_chart_image( $post->ID ); require_once __DIR__ . '/templates/chart-meta-box.php'; } /** * Displays the CSV Import/Export controls * * @param object the WP post object as returned by the metabox API */ public function csv_meta_box( $post ) { require_once __DIR__ . '/templates/csv-meta-box.php'; } /** * Insert CSV Import and Export forms into the footer when editing charts */ public function admin_footer() { $screen = get_current_screen(); if ( 'post' != $screen->base || m_chart()->slug != $screen->post_type ) { return; } ?> post_type ) || m_chart()->slug != $post->post_type ) { return; } // Don't run on post revisions (almost always happens just before the real post is saved) if ( wp_is_post_revision( $post->ID ) ) { return; } // Check the nonce if ( ! wp_verify_nonce( $_POST[ m_chart()->slug ]['nonce'], m_chart()->slug . '-save-post' ) ) { return; } unset( $_POST[ m_chart()->slug ]['nonce'] ); // Check the permissions if ( ! current_user_can( 'edit_post', $post->ID ) ) { return; } // If there's an image being passed attach it to the chart post $this->attach_image(); // Make sure we don't overrwrite existing settings in the case someone hits update too quickly if ( isset( $_POST[ m_chart()->slug ]['library'] ) // Make sure the library value is clean and valid before trying to use it && m_chart()->is_valid_library( $_POST[ m_chart()->slug ]['library'] ) ) { // Load the library in question in case there's a filter/action we'll need if ( 'highcharts' == $_POST[ m_chart()->slug ]['library'] ) { m_chart()->highcharts(); } // update_post_meta passes the $_POST values directly to validate_post_meta // validate_post_meta returns only valid post meta values and does data validation on each item m_chart()->update_post_meta( $post->ID, $_POST[ m_chart()->slug ] ); } } /** * Attach a given image to a chart post * * @param int the WP post ID of the post being saved * @param string a base64 encoded string of the image we want to attach */ public function attach_image() { $settings = m_chart()->get_settings(); // If the performance setting isn't turned to default we don't do this if ( 'default' != $settings['performance'] ) { return; } if ( ! is_numeric( $_POST['post_ID'] ) ) { return; } $post_id = absint( $_POST['post_ID'] ); if ( ! current_user_can( 'edit_post', $post_id ) ) { return false; } if ( ! $post = get_post( $post_id ) ) { return false; } if ( '' == $_POST[ m_chart()->slug ]['img'] ) { return false; } // Decode the image so we can save it $decoded_img = base64_decode( str_replace( 'data:image/png;base64,', '', $_POST[ m_chart()->slug ]['img'] ) ); if ( '' == $decoded_img ) { return false; } // Check for an existing attached image $attachments = get_posts( array( 'post_type' => 'attachment', 'posts_per_page' => 1, 'post_parent' => $post->ID, 'meta_key' => m_chart()->slug . '-image', ) ); // If an existing image was found delete it foreach ( $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Upload image to WP $file = wp_upload_bits( sanitize_title( $post->post_title . '-' . $post->ID ) . '.png', null, $decoded_img ); // START acting like media_sideload_image preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $file['file'], $matches ); $file_array['name'] = basename( $matches[0] ); $file_array['tmp_name'] = $file['file']; if ( is_wp_error( $file ) ) { @unlink( $file_array['tmp_name'] ); $file_array['tmp_name'] = ''; } $img_id = media_handle_sideload( $file_array, $post->ID, $post->post_title ); if ( is_wp_error( $img_id ) ) { @unlink( $file_array['tmp_name'] ); return $img_id; } // STOP acting like media_sideload_image // Set some meta on the attachment so we know it came from m-chart add_post_meta( $img_id, m_chart()->slug . '-image', $post->ID ); // Set the attachment as the chart's thumbnail update_post_meta( $post->ID, '_thumbnail_id', $img_id ); } /** * Parses an incoming CSV file and compiles it into an array * * @return array an array fo the data from the imported CSV file ready for use in the chart meta */ public function ajax_import_csv() { $post = get_post( absint( $_POST['post_id'] ) ); // Check post type if ( ! isset( $post->post_type ) || m_chart()->slug != $post->post_type ) { wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) ); } // Check the nonce if ( ! wp_verify_nonce( $_POST['nonce'], m_chart()->slug . '-save-post' ) ) { wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) ); } // Check the permissions if ( ! current_user_can( 'edit_post', $post->ID ) ) { wp_send_json_error( esc_html__( 'Wrong post type', 'm-chart' ) ); } // Make sure there's a CSV file if ( empty( $_FILES ) || ! isset( $_FILES['import_csv_file']['name'] ) ) { wp_send_json_error( esc_html__( 'No file to import', 'm-chart' ) ); } // Make sure the file is a CSV file $file_ext = strtolower( pathinfo( $_FILES['import_csv_file']['name'], PATHINFO_EXTENSION ) ); if ( 'csv' != $file_ext ) { wp_send_json_error( esc_html__( 'Only CSV files can be imported ', 'm-chart' ) ); } // Do some validation on the CSV file (mirroring what WP does for this sort of thing) $csv_file = realpath( $_FILES['import_csv_file']['tmp_name'] ); if ( ! $csv_file ) { wp_send_json_error( esc_html__( 'File path not found', 'm-chart' ) ); } $csv_data = file_get_contents( $csv_file ); if ( '' == $csv_data ) { wp_send_json_error( esc_html__( 'CSV file was empty', 'm-chart' ) ); } // Get parseCSV library so we can use it to convert the CSV to a nice array // Yes, PHP does this natively now but I've run into trouble with malformed CSV that parsCSV handles fine require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php'; $parse_csv = new parseCSV(); // The "\n" before and after is to deal with CSV files that don't have line breaks above and below the data // Which then seems to confuse parseCSV occasionally $parse_csv->parse( "\n" . trim( $csv_data ) . "\n" ); // This deals with Google Doc's crappy CSV exports which don't include columns at the end of a row if they are empty $data_array = $this->fix_csv_data_array( $parse_csv->data ); wp_send_json_success( $data_array ); } /** * Helper function makes sure that the data array has matching numbers of array elements for each row * 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?) * * @param array an array of data as returned from the parseCSV class * * @param array the array of data with matching array value counts */ public function fix_csv_data_array( $data_array ) { $count = 0; // Get largest row count foreach ( $data_array as $data ) { $temp_count = count( $data ); $count = ( $temp_count > $count ) ? $temp_count : $count; } // Fix arrays so value counts match foreach ( $data_array as $key => $data ) { $temp_count = count( $data ); if ( $temp_count < $count ) { $difference = $count - $temp_count; for ( $i = 0; $i < $difference; $i++ ) { $data_array[ $key ][] = ''; } } } return $data_array; } /** * Converts data array into CSV outputs it to the browser */ public function ajax_export_csv() { // Purposely using $_REQUEST here since this method can work via a GET and POST request // POST requests are used when passing the data value since it's too big to pass via GET if ( ! is_numeric( $_REQUEST['post_id'] ) || ! current_user_can( 'edit_post', absint( $_REQUEST['post_id'] ) ) ) { wp_die( 'Unauthorized access', 'You do not have permission to do that', array( 'response' => 401 ) ); } $post = get_post( absint( $_REQUEST['post_id'] ) ); // If the user passed a data value in their request we'll use it after validation if ( isset( $_POST['data'] ) && isset( $_POST['title'] ) ) { $data = m_chart()->validate_data( json_decode( stripslashes( $_POST['data'] ) ) ); $file_name = sanitize_title( $_POST['title'] ); } else { $data = m_chart()->get_post_meta( $post->ID, 'data' ); $file_name = sanitize_title( get_the_title( $post->ID ) ); } if ( empty( $data ) ) { return; } require_once __DIR__ . '/external/parsecsv/parsecsv.lib.php'; $parse_csv = new parseCSV(); $parse_csv->output( $file_name . '.csv', $data ); die; } /** * Returns JSON encoded chart args from $_POST values sent from the admin panel * * @return string a JSON encoded string containing all of the chart args needed to update an active chart */ public function ajax_get_chart_args() { // Check the nonce if ( ! wp_verify_nonce( $_POST['nonce'], m_chart()->slug . '-save-post' ) ) { wp_send_json_error( esc_html__( 'Invalid nonce', 'm-chart' ) ); } // Does the post exist? if ( ! $post = get_post( absint( $_POST['post_id'] ) ) ) { wp_send_json_error( esc_html__( 'Invalid post', 'm-chart' ) ); } // Can the user edit this post? if ( ! current_user_can( 'edit_post', $post->ID ) ) { wp_send_json_error( esc_html__( 'Permission error', 'm-chart' ) ); } // Is this a valid library? if ( ! m_chart()->is_valid_library( $_POST['library'] ) ) { wp_send_json_error( esc_html__( 'Invalid library', 'm-chart' ) ); } if ( 'highcharts' == $_POST['library'] ) { $library = m_chart()->highcharts(); } // Set these values so that get_chart_args has them already available before we call it $library->args = m_chart()->get_chart_default_args; $library->post = (object) array( 'ID' => $post->ID, 'post_title' => sanitize_text_field( $_POST['title'] ), ); // validate_post_meta returns only valid post meta values and does data validation on each item $library->post_meta = m_chart()->validate_post_meta( $_POST['post_meta'] ); wp_send_json_success( $library->get_chart_args( $library->post->ID, $library->args, true, false ) ); } /** * Return a name spaced field name * * @param string the field name we want to name space * * @param string a name spaced field name */ public function get_field_name( $field_name ) { return m_chart()->slug . '[' . $field_name . ']'; } /** * Return a name spaced field id * * @param string the field id we want to name space * * @param string a name spaced field id */ public function get_field_id( $field_name ) { return m_chart()->slug . '-' . $field_name; } }