lename = sprintf(
'%s - %s.csv',
Admin::init()->get_export_filename(),
gmdate( 'Y-m-d H:i' )
);
}
/**
* Extract field names from `$data` for later use.
*/
$fields = array_keys( $data );
/**
* Count how many rows will be exported.
*/
$row_count = count( reset( $data ) );
// Forces the download of the CSV instead of echoing
header( 'Content-Disposition: attachment; filename=' . $filename );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
header( 'Content-Type: text/csv; charset=utf-8' );
$output = fopen( 'php://output', 'w' );
/**
* Print CSV headers
*/
// @todo When we drop support for PHP <7.4, consider passing empty-string for `$escape` here for better spec compatibility.
fputcsv( $output, $fields, ',', '"', '\\' );
/**
* Print rows to the output.
*/
for ( $i = 0; $i < $row_count; $i++ ) {
$current_row = array();
/**
* Put all the fields in `$current_row` array.
*/
foreach ( $fields as $single_field_name ) {
$current_row[] = $this->esc_csv( $data[ $single_field_name ][ $i ] );
}
/**
* Output the complete CSV row
*/
// @todo When we drop support for PHP <7.4, consider passing empty-string for `$escape` here for better spec compatibility.
fputcsv( $output, $current_row, ',', '"', '\\' );
}
fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
$this->record_tracks_event( 'forms_export_responses', array( 'format' => 'csv' ) );
exit( 0 );
}
/**
* Create a new post with a Form block
*/
public function create_new_form() {
$post_id = wp_insert_post(
array(
'post_title' => esc_html__( 'Jetpack Forms', 'jetpack-forms' ),
'post_content' => '
',
)
);
if ( ! is_wp_error( $post_id ) ) {
$array_result = array(
'post_url' => admin_url( 'post.php?post=' . intval( $post_id ) . '&action=edit' ),
);
wp_send_json( $array_result );
}
wp_die();
}
/**
* Send an event to Tracks
*
* @param string $event_name - the name of the event.
* @param array $event_props - event properties to send.
*
* @return null|void
*/
public function record_tracks_event( $event_name, $event_props ) {
/*
* Event details.
*/
$event_user = wp_get_current_user();
/*
* Record event.
* We use different libs on wpcom and Jetpack.
*/
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$event_name = 'wpcom_' . $event_name;
$event_props['blog_id'] = get_current_blog_id();
// logged out visitor, record event with blog owner.
if ( empty( $event_user->ID ) ) {
$event_user_id = wpcom_get_blog_owner( $event_props['blog_id'] );
$event_user = get_userdata( $event_user_id );
}
require_lib( 'tracks/client' );
tracks_record_event( $event_user, $event_name, $event_props );
} else {
$user_connected = ( new \Automattic\Jetpack\Connection\Manager( 'jetpack-forms' ) )->is_user_connected( get_current_user_id() );
if ( ! $user_connected ) {
return;
}
// logged out visitor, record event with Jetpack master user.
if ( empty( $event_user->ID ) ) {
$master_user_id = Jetpack_Options::get_option( 'master_user' );
if ( ! empty( $master_user_id ) ) {
$event_user = get_userdata( $master_user_id );
}
}
$tracking = new Tracking();
$tracking->record_user_event( $event_name, $event_props, $event_user );
}
}
/**
* Escape a string to be used in a CSV context
*
* Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks and
* disclosure of sensitive information.
*
* Additionally, Excel exposes the ability to launch arbitrary commands through the DDE protocol.
*
* @see https://www.contextis.com/en/blog/comma-separated-vulnerabilities
*
* @param string $field - the CSV field.
*
* @return string
*/
public function esc_csv( $field ) {
$active_content_triggers = array( '=', '+', '-', '@' );
if ( in_array( mb_substr( $field, 0, 1 ), $active_content_triggers, true ) ) {
$field = "'" . $field;
}
return $field;
}
/**
* Returns an array of parent post IDs for the user.
* The parent posts are those posts where forms have been published.
*
* @param array $query_args A WP_Query compatible array of query args.
*
* @return array The array of post IDs
*/
public static function get_all_parent_post_ids( $query_args = array() ) {
$default_query_args = array(
'fields' => 'id=>parent',
'posts_per_page' => 100000, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
'post_type' => 'feedback',
'post_status' => 'publish',
'suppress_filters' => false,
);
$args = array_merge( $default_query_args, $query_args );
// Get the feedbacks' parents' post IDs
$feedbacks = get_posts( $args );
return array_values( array_unique( array_values( $feedbacks ) ) );
}
/**
* Returns a string of HTML ',
esc_attr( $parent_id ),
$selected_id === $parent_id ? 'selected' : '',
esc_html( basename( $parsed_url['path'] ) )
);
}
return $options;
}
/**
* Get the names of all the form's fields
*
* @param array|int $posts the post we want the fields of.
*
* @return array the array of fields
*
* @deprecated As this is no longer necessary as of the CSV export rewrite. - 2015-12-29
*/
protected function get_field_names( $posts ) {
$posts = (array) $posts;
$all_fields = array();
foreach ( $posts as $post ) {
$fields = self::parse_fields_from_content( $post );
if ( isset( $fields['_feedback_all_fields'] ) ) {
$extra_fields = array_keys( $fields['_feedback_all_fields'] );
$all_fields = array_merge( $all_fields, $extra_fields );
}
}
$all_fields = array_unique( $all_fields );
return $all_fields;
}
/**
* Returns if the feedback post has JSON data
*
* @param int $post_id The feedback post ID to check.
* @return bool
*/
public function has_json_data( $post_id ) {
$post_content = get_post_field( 'post_content', $post_id );
$content = explode( "\nJSON_DATA", $post_content );
if ( empty( $content[1] ) ) {
return false;
}
$json_data = json_decode( $content[1], true );
return is_array( $json_data ) && ! empty( $json_data );
}
/**
* Parse the contact form fields.
*
* @param int $post_id - the post ID.
* @return array Fields.
*/
public static function parse_fields_from_content( $post_id ) {
static $post_fields;
if ( ! is_array( $post_fields ) ) {
$post_fields = array();
}
if ( isset( $post_fields[ $post_id ] ) ) {
return $post_fields[ $post_id ];
}
$all_values = array();
$post_content = get_post_field( 'post_content', $post_id );
$content = explode( '', $post_content );
$lines = array();
if ( count( $content ) > 1 ) {
$content = str_ireplace( array( ' ', ')' ), '', $content[1] );
if ( str_contains( $content, 'JSON_DATA' ) ) {
$chunks = explode( "\nJSON_DATA", $content );
$all_values = json_decode( $chunks[1], true );
$lines = array_filter( explode( "\n", $chunks[0] ) );
} else {
$fields_array = preg_replace( '/.*Array\s\( (.*)\)/msx', '$1', $content );
// This line of code is used to parse a string containing key-value pairs formatted as [Key] => Value and extract the keys and values into an array.
// The regular expression ensures that each key-value pair is correctly identified and captured.
// Given an input string
// [Key1] => Value1
// [Key2] => Value2
// it $matches[1]: The keys (e.g., Key1, Key2 ).
// and $matches[2]: The values (e.g., Value1, Value2 ).
preg_match_all( '/^\s*\[([^\]]+)\] =\>\; (.*)(?=^\s*(\[[^\]]+\] =\>\;)|\z)/msU', $fields_array, $matches );
if ( count( $matches ) > 1 ) {
$all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) );
}
$lines = array_filter( explode( "\n", $content ) );
}
}
$var_map = array(
'AUTHOR' => '_feedback_author',
'AUTHOR EMAIL' => '_feedback_author_email',
'AUTHOR URL' => '_feedback_author_url',
'SUBJECT' => '_feedback_subject',
'IP' => '_feedback_ip',
);
$fields = array();
foreach ( $lines as $line ) {
$vars = explode( ': ', $line, 2 );
if ( ! empty( $vars ) ) {
if ( isset( $var_map[ $vars[0] ] ) ) {
$fields[ $var_map[ $vars[0] ] ] = self::strip_tags( trim( $vars[1] ) );
}
}
}
$fields['_feedback_all_fields'] = $all_values;
$post_fields[ $post_id ] = $fields;
return $fields;
}
/**
* Creates a valid csv row from a post id
*
* @param int $post_id The id of the post.
* @param array $fields An array containing the names of all the fields of the csv.
*
* @return String The csv row
*
* @deprecated This is no longer needed, as of the CSV export rewrite.
*/
protected static function make_csv_row_from_feedback( $post_id, $fields ) {
$content_fields = self::parse_fields_from_content( $post_id );
$all_fields = array();
if ( isset( $content_fields['_feedback_all_fields'] ) ) {
$all_fields = $content_fields['_feedback_all_fields'];
}
// Overwrite the parsed content with the content we stored in post_meta in a better format.
$extra_fields = get_post_meta( $post_id, '_feedback_extra_fields', true );
foreach ( $extra_fields as $extra_field => $extra_value ) {
$all_fields[ $extra_field ] = $extra_value;
}
// The first element in all of the exports will be the subject
$row_items = array();
$row_items[] = $content_fields['_feedback_subject'];
// Loop the fields array in order to fill the $row_items array correctly
foreach ( $fields as $field ) {
if ( $field === __( 'Contact Form', 'jetpack-forms' ) ) { // the first field will ever be the contact form, so we can continue
continue;
} elseif ( array_key_exists( $field, $all_fields ) ) {
$row_items[] = $all_fields[ $field ];
} else {
$row_items[] = '';
}
}
return $row_items;
}
/**
* Get the IP address.
*
* @return string|null IP address.
*/
public static function get_ip_address() {
return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : null;
}
/**
* Disable Block Editor for feedbacks.
*
* @param bool $can_edit Whether the post type can be edited or not.
* @param string $post_type The post type being checked.
* @return bool
*/
public function use_block_editor_for_post_type( $can_edit, $post_type ) {
return 'feedback' === $post_type ? false : $can_edit;
}
/**
* Kludge method: reverses the output of a standard print_r( $array ).
* Sort of what unserialize does to a serialized object.
* This is here while we work on a better data storage inside the posts. See:
* - p1675781140892129-slack-C01CSBEN0QZ
* - https://www.php.net/manual/en/function.print-r.php#93529
*
* @param string $print_r_output The array string to be reverted. Needs to being with 'Array'.
* @param bool $parse_html Whether to run html_entity_decode on each line.
* As strings are stored right now, they are all escaped, so '=>' are '>'.
* @return array|string Array when succesfully reconstructed, string otherwise. Output will always be esc_html'd.
*/
public static function reverse_that_print( $print_r_output, $parse_html = false ) {
$lines = explode( "\n", trim( $print_r_output ) );
if ( $parse_html ) {
$lines = array_map( 'html_entity_decode', $lines );
}
if ( trim( $lines[0] ) !== 'Array' ) {
// bottomed out to something that isn't an array, escape it and be done
return esc_html( $print_r_output );
} else {
// this is an array, lets parse it
if ( preg_match( '/(\s{5,})\(/', $lines[1], $match ) ) {
// this is a tested array/recursive call to this function
// take a set of spaces off the beginning
$spaces = $match[1];
$spaces_length = strlen( $spaces );
$lines_total = count( $lines );
for ( $i = 0; $i < $lines_total; $i++ ) {
if ( substr( $lines[ $i ], 0, $spaces_length ) === $spaces ) {
$lines[ $i ] = substr( $lines[ $i ], $spaces_length );
}
}
}
array_shift( $lines ); // Array
array_shift( $lines ); // (
array_pop( $lines ); // )
$print_r_output = implode( "\n", $lines );
// make sure we only match stuff with 4 preceding spaces (stuff for this array and not a nested one
preg_match_all( '/^\s{4}\[(.+?)\] \=\> /m', $print_r_output, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
$pos = array();
$previous_key = '';
$in_length = strlen( $print_r_output );
// store the following in $pos:
// array with key = key of the parsed array's item
// value = array(start position in $print_r_output, $end position in $print_r_output)
foreach ( $matches as $match ) {
$key = $match[1][0];
$start = $match[0][1] + strlen( $match[0][0] );
$pos[ $key ] = array( $start, $in_length );
if ( $previous_key !== '' ) {
$pos[ $previous_key ][1] = $match[0][1] - 1;
}
$previous_key = $key;
}
$ret = array();
foreach ( $pos as $key => $where ) {
// recursively see if the parsed out value is an array too
$ret[ $key ] = self::reverse_that_print( substr( $print_r_output, $where[0], $where[1] - $where[0] ), $parse_html );
}
return $ret;
}
}
/**
* Method untrash_feedback_status_handler
* wp_untrash_post filter handler.
*
* @param string $current_status The status to be set.
* @param int $post_id The post ID.
* @param string $previous_status The previous status.
*/
public function untrash_feedback_status_handler( $current_status, $post_id, $previous_status ) {
$post = get_post( $post_id );
if ( 'feedback' === $post->post_type ) {
if ( in_array( $previous_status, array( 'spam', 'publish' ), true ) ) {
return $previous_status;
}
return 'publish';
}
return $current_status;
}
/**
* Returns whether we are in condition to track and use
* analytics functionality like Tracks.
*
* @return bool Returns true if we can track analytics, else false.
*/
public static function can_use_analytics() {
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
$status = new Status();
$connection = new Connection_Manager();
$tracking = new Tracking( 'jetpack', $connection );
$should_enable_tracking = $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
return $is_wpcom || $should_enable_tracking;
}
/**
* Check if the form modal interface should be enabled.
* This is a development-only feature flag.
*
* @return bool
*/
public static function is_form_modal_enabled() {
return defined( 'JETPACK_IS_FORM_MODAL_ENABLED' ) && JETPACK_IS_FORM_MODAL_ENABLED;
}
}