<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Hustle_Entries_Admin * * @package Hustle */ /** * Class Hustle_Entries_Admin * Handle the email lists. * * @since 4.0 */ class Hustle_Entries_Admin extends Hustle_Admin_Page_Abstract { /** * Module types with titles. * * @since 4.3.1 * @var array */ private $module_types; /** * Merged default parameter with $_REQUEST * * @since 4.0 * @var array */ private $screen_params = array(); /** * Current module model * * @since 4.0 * @var null|Hustle_Module_Model */ private $module = null; /** * Current module_id * * @since 4.0 * @var int */ protected $module_id = 0; /** * Entries array. * * @since 4.0 * @var string */ private $entries = array(); /** * Page number * * @since 4.0 * @var int */ protected $page_number = 1; /** * Get pagination limit * * @var int */ protected $per_page; /** * Total Entries * * @since 4.0 * @var int */ protected $total_entries = 0; /** * Total filterd Entries * * @since 4.0 * @var int */ protected $filtered_total_entries = 0; /** * Registered addons * * @since 4.0 * @var Hustle_Provider_Abstract[] */ private static $registered_addons = null; /** * Filters to be used * * [key=>value] * ['search'=>'search term'] * * @since 4.0 * @var array */ public $filters = array(); /** * Order to be used * * [key=>order] * ['entry_date' => 'ASC'] * * @since 4.0 * @var array */ public $order = array(); /** * Nested Mappers * * @since 4.0 * @var array */ protected $fields_mappers = array(); /** * Init */ public function init() { $this->page = 'hustle_entries'; /* translators: Plugin name */ $this->page_title = sprintf( __( '%s Email Lists', 'hustle' ), Opt_In_Utils::get_plugin_name() ); $this->page_menu_title = __( 'Email Lists', 'hustle' ); $this->page_capability = 'hustle_access_emails'; $this->page_template_path = 'admin/entries'; // Show the first page if current page doesn't have entries. add_filter( 'removable_query_args', array( $this, 'maybe_remove_paged' ) ); } /** * Remove paged get attribute if there aren't entries and it's not the first page * * @since 4.3.1 * @param array $removable_query_args URL query args to be removed. * @return array */ public function maybe_remove_paged( $removable_query_args ) { $paged = filter_input( INPUT_GET, 'paged', FILTER_VALIDATE_INT ); if ( $paged && 1 !== $paged && 'hustle_entries' === $this->current_page ) { $per_page = Hustle_Settings_Admin::get_per_page( 'submission' ); $offset = ( $paged - 1 ) * $per_page; $module_id = filter_input( INPUT_GET, 'module_id', FILTER_VALIDATE_INT ); $total_entries = Hustle_Entry_Model::count_entries( $module_id ); if ( $total_entries <= $offset ) { $_SERVER['REQUEST_URI'] = remove_query_arg( 'paged' ); $removable_query_args[] = 'paged'; unset( $_GET['paged'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } } return $removable_query_args; } /** * Get the arguments used when rendering the main page. * * @since 4.0.1 * @return array */ public function get_page_template_args() { $types = $this->get_module_types(); $module = $this->get_module_model(); $filter_types = array( 'search_email', 'order_by', 'date_range', ); $is_filtered = false; foreach ( $filter_types as $type ) { $is_filtered = $is_filtered || filter_input( INPUT_GET, $type, FILTER_SANITIZE_SPECIAL_CHARS ); } if ( $module && $module->active ) { $integrations = $module->get_integrations_settings()->to_array(); $no_local_list = false === strpos( $integrations['active_integrations'], 'local_list' ); } else { $no_local_list = false; } return array( 'module' => $module, 'entries' => $this->get_entries(), 'global_entries' => Hustle_Entry_Model::global_count_entries(), 'module_name' => ! empty( $module->module_type ) && isset( $types[ $module->module_type ] ) ? $types[ $module->module_type ] : '', 'is_module_selected' => (bool) $this->get_current_module_id(), 'is_filtered' => $is_filtered, 'no_local_list' => $no_local_list, ); } /** * Enqueue scripts */ public function current_page_loaded() { parent::current_page_loaded(); $this->before_render(); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); } /** * Enqueue scripts for the submissions page. * * @since 4.2.0 * * @return void */ public function enqueue_scripts() { wp_enqueue_script( 'hustle-entries-moment', Opt_In::$plugin_url . 'assets/js/vendor/moment.min.js', array( 'jquery' ), Opt_In::VERSION, true ); wp_enqueue_script( 'hustle-entries-datepicker-range', Opt_In::$plugin_url . 'assets/js/vendor/daterangepicker.min.js', array( 'jquery', 'hustle-entries-moment' ), '3.0.5', true ); } /** * Register the js variables to be localized for this page. * * @since 4.3.1 * * @return array */ protected function get_vars_to_localize() { $current_array = parent::get_vars_to_localize(); // These labels are used in getDaterangepickerRanges(), entries.js. // These keys must match the keys from there. $datepicker_ranges = array( 'today' => __( 'Today', 'hustle' ), 'yesterday' => __( 'Yesterday', 'hustle' ), 'last_seven_days' => __( 'Last 7 Days', 'hustle' ), 'last_thirty_days' => __( 'Last 30 Days', 'hustle' ), 'this_month' => __( 'This Month', 'hustle' ), 'last_month' => __( 'Last Month', 'hustle' ), ); $current_array['daterangepicker'] = array( 'daysOfWeek' => Hustle_Time_Helper::get_week_days( 'min' ), 'monthNames' => Hustle_Time_Helper::get_months(), 'ranges' => $datepicker_ranges, ); return $current_array; } /** * Populating the current page parameters * * @since 4.0.0 */ public function populate_screen_params() { $screen_params = array( 'module_type' => 'popup', 'module_id' => 0, ); $this->screen_params = array_merge( $screen_params, $_REQUEST );// phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Executed Action before rendering the page. * * @since 4.0 */ public function before_render() { $this->populate_screen_params(); $this->prepare_entries_page(); $this->export(); } /** * Get the module types for the entries page. * * @since 4.0.0 * * @return array */ public function get_module_types() { if ( empty( $this->module_types ) ) { $module_types = Hustle_Data::get_module_types(); $types_with_title = array(); foreach ( $module_types as $type ) { if ( Hustle_Model::SOCIAL_SHARING_MODULE === $type ) { continue; } $types_with_title[ $type ] = Opt_In_Utils::get_module_type_display_name( $type, false, true ); } $this->module_types = $types_with_title; } return $this->module_types; } /** * Prepare entries * * @since 4.0 */ private function prepare_entries_page() { $this->module = $this->get_module_model(); // Module not found. if ( ! $this->module ) { // if module_id available remove it from request, and redirect. if ( $this->get_current_module_id() ) { $url = remove_query_arg( 'module_id' ); if ( wp_safe_redirect( $url ) ) { exit; } } } else { // as page's before_render(). $this->prepare_page(); } } /** * Return the modules of the current type. * * @since 4.0 * * @return array Hustle_Module_Model[] */ public function get_modules() { $module_types = $this->get_module_types(); $current_type = $this->get_current_module_type(); $module_type = isset( $module_types[ $current_type ] ) ? $current_type : 'popup'; $args = array( 'module_type' => $module_type, 'module_mode' => 'optin', ); $modules = Hustle_Module_Collection::instance()->get_all( null, $args ); return $modules; } /** * Get module model if the requested module_id is available and matches module_type * * @since 4.0 * * @return bool|Hustle_Module_Model|null */ public function get_module_model() { if ( $this->get_current_module_id() ) { $module = new Hustle_Module_Model( $this->get_current_module_id() ); if ( is_wp_error( $module ) ) { return null; } if ( ! $module instanceof Hustle_Module_Model ) { return null; } if ( $module->module_type !== $this->get_current_module_type() ) { return null; } return $module; } return null; } /** * Get current module type * * @since 4.0 * * @return mixed */ public function get_current_module_type() { return $this->screen_params['module_type']; } /** * Get current module id * * @since 4.0 * * @return mixed */ public function get_current_module_id() { return $this->screen_params['module_id']; } // ==================== /** * Get Entries * * @since 4.0 * @return array */ public function get_entries() { return $this->entries; } /** * Get Page Number * * @since 4.0 * @return int */ public function get_page_number() { return $this->page_number; } /** * Get Per Page * * @since 1.0 * @return int */ public function get_per_page() { return $this->per_page; } /** * The total filtered entries * * @since 4.0 * @return int */ public function filtered_total_entries() { return $this->filtered_total_entries; } /** * Prepare email list page * admin_page_entries as 'before_render' */ private function prepare_page() { $this->module_id = (int) $this->module->module_id; $this->parse_filters(); $this->parse_order(); $this->per_page = Hustle_Settings_Admin::get_per_page( 'submission' ); // don't use filter_input() here, because of see maybe_remove_paged() method. $pagenum = ! empty( $_GET['paged'] ) ? (int) $_GET['paged'] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->page_number = max( 1, $pagenum ); /** * Fires on custom form page entries render before request and result processed * * @since 4.0 */ do_action( 'hustle_admin_page_entries', $this->module_id, $this->module, $pagenum ); $this->process_request(); $this->prepare_results(); } /** * Process the current request. * * @since 4.0 */ private function process_request() { $nonce = filter_input( INPUT_POST, 'hustle_nonce', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! wp_verify_nonce( $nonce, 'hustle_entries_request' ) ) { return; } if ( ! current_user_can( 'hustle_access_emails' ) ) { return; } $action = filter_input( INPUT_POST, 'hustle_action', FILTER_SANITIZE_SPECIAL_CHARS ); if ( empty( $action ) ) { $action = filter_input( INPUT_POST, 'hustle_action_bottom', FILTER_SANITIZE_SPECIAL_CHARS ); } switch ( $action ) { case 'delete': $entry_id = filter_input( INPUT_POST, 'id', FILTER_VALIDATE_INT ); if ( ! $entry_id ) { return; } Hustle_Entry_Model::delete_by_entry( $this->module_id, $entry_id ); break; case 'delete-all': $entries = filter_input( INPUT_POST, 'ids', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! empty( $entries ) ) { $entries = explode( ',', $entries ); Hustle_Entry_Model::delete_by_entries( $this->module_id, $entries ); } break; default: return; } $url_params = array( 'page' => isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : 'hustle_entries', 'module_type' => isset( $_REQUEST['module_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['module_type'] ) ) : '', 'module_id' => $this->module_id, 'paged' => isset( $_REQUEST['paged'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['paged'] ) ) : 1, ); $url = add_query_arg( $url_params, 'admin.php' ); wp_safe_redirect( $url ); // Redirect to the first entry page. exit; } /** * Prepare the entries to be shown. * * @since 4.0 */ private function prepare_results() { if ( is_object( $this->module ) ) { $paged = $this->page_number; $per_page = $this->per_page; $offset = ( $paged - 1 ) * $per_page; $this->total_entries = Hustle_Entry_Model::count_entries( $this->module_id ); $args = array( 'module_id' => $this->module_id, 'per_page' => $per_page, 'offset' => $offset, 'order_by' => 'entries.date_created', 'order' => 'ASC', ); $args = wp_parse_args( $this->filters, $args ); $args = wp_parse_args( $this->order, $args ); $count = 0; $this->entries = Hustle_Entry_Model::query_entries( $args, $count ); $this->filtered_total_entries = $count; } } /** * Get entries iterator * * @return array */ public function entries_iterator() { /** * Example * * @example * { * id => 'ENTRY_ID' * summary = [ * 'num_fields_left' => true/false, * 'items' => [ * [ * 'colspan' => 2/..., * 'value' => '----', * ] * [ * 'colspan' => 2/... * value' => '----', * ] * ], * ], * detail = [ * 'colspan' => '', * 'items' => [ * [ * 'label' => '----', * 'value' => '-----' * 'sub_entries' => [ * [ * 'label' => '----', * 'value' => '-----' * ] * ] * ] * [ * 'label' => '----', * 'value' => '-----' * ] * ], * ] * } */ $entries_iterator = array(); $total_colspan = 5; // Colspan for ID + Date Submitted + Active Integrations + Email + Accordion chevron. $fields_mappers = $this->get_fields_mappers(); // Start from 4, since first four are ID, Date, Active Integrations, and Email. $fields_left = count( $fields_mappers ) - 4; // All headers including ID + Date, start from 0 and max is 4. $headers = array_slice( $fields_mappers, 0, 4 ); $numerator_id = $this->total_entries; if ( $this->page_number > 1 ) { $numerator_id = $this->total_entries - ( ( $this->page_number - 1 ) * $this->per_page ); } foreach ( $this->entries as $entry ) { /** Hustle_Entry_Model $entry */ // create placeholder. $iterator = array( 'id' => $numerator_id, 'entry_id' => $entry->entry_id, 'summary' => array(), 'detail' => array(), 'addons' => array(), ); $iterator['summary']['num_fields_left'] = $fields_left; $iterator['summary']['items'] = array(); $iterator['detail']['colspan'] = $total_colspan; $iterator['detail']['items'] = array(); // Build array for summary row. $summary_items = array(); foreach ( $headers as $header ) { $colspan = 2; $class = ''; if ( isset( $header['type'] ) ) { if ( 'entry_entry_id' === $header['type'] ) { $summary_items[] = array( 'colspan' => 1, 'value' => $numerator_id, ); continue; } elseif ( 'entry_time_created' === $header['type'] ) { $colspan = 3; $class = 'hui-column-date'; } elseif ( 'entry_integrations' === $header['type'] ) { $class = 'hui-column-apps'; } } $value = $this->get_entry_field_value( $entry, $header, '', false, 100 ); $summary_items[] = array( 'colspan' => $colspan, 'value' => $value, 'class' => $class, ); } // Build array for -content row. $detail_items = array(); foreach ( $fields_mappers as $mapper ) { // Skip entry id and Active integrations. if ( isset( $mapper['type'] ) && ( 'entry_entry_id' === $mapper['type'] || 'entry_integrations' === $mapper['type'] ) ) { continue; } $label = $mapper['label']; $value = $this->get_entry_field_value( $entry, $mapper, '', true ); $sub_entries = array(); $detail_items[] = array( 'label' => $label, 'value' => $value, 'sub_entries' => $sub_entries, ); } // Additional render for addons. $addons_detail_items = $this->attach_addon_on_render_entry( $entry ); $addons = array(); foreach ( $addons_detail_items as $provider_meta ) { foreach ( $provider_meta as $meta ) { $addons[] = array( 'summary' => array( 'name' => $meta['name'], 'icon' => $meta['icon'], 'data_sent' => $meta['data_sent'], ), 'detail' => $meta['sub_entries'], ); } } $iterator['summary']['items'] = $summary_items; $iterator['detail']['items'] = $detail_items; $iterator['addons'] = $addons; $entries_iterator[] = $iterator; $numerator_id --; } return $entries_iterator; } /** * Get Fields Mappers based on current state of form * * @return array */ public function get_fields_mappers() { if ( empty( $this->fields_mappers ) ) { $this->fields_mappers = $this->build_fields_mappers(); } return $this->fields_mappers; } /** * Get fields mappers * * @return type */ private function build_fields_mappers() { $module = $this->module; $fields = $module->get_form_fields(); $ignored_field_types = Hustle_Entry_Model::ignored_fields(); $mappers = array( array( // read model's property. 'property' => 'entry_id', // must be on entries. 'label' => __( 'ID', 'hustle' ), 'type' => 'entry_entry_id', ), array( // read model's property. 'property' => 'time_created', // must be on entries. 'label' => __( 'Date Submitted', 'hustle' ), 'type' => 'entry_time_created', 'class' => 'hui-column-date', ), array( // read entry meta. 'meta_key' => 'active_integrations', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'label' => __( 'Active Integrations', 'hustle' ), 'type' => 'entry_integrations', 'class' => 'hui-column-apps', ), array( // required meta key. must be on entries. 'meta_key' => 'email', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'label' => __( 'Email', 'hustle' ), 'type' => 'email', ), ); foreach ( $fields as $field ) { $field_type = $field['type']; if ( 'email' === $field['name'] || in_array( $field_type, $ignored_field_types, true ) ) { continue; } // base mapper for every field. $mapper = array(); $mapper['meta_key'] = $field['name'];// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key $mapper['label'] = $field['label']; $mapper['type'] = $field_type; if ( ! empty( $mapper ) ) { $mappers[] = $mapper; } } return $mappers; } /** * Get entry field value helper * * @param Hustle_Entry_Model $entry Entry. * @param array $mapper Mapper. * @param string $sub_meta_key Sub meta key. * @param bool $allow_html Allow HTML. * @param int $truncate Truncate. * * @return string */ private function get_entry_field_value( $entry, $mapper, $sub_meta_key = '', $allow_html = false, $truncate = PHP_INT_MAX ) { /** Hustle_Entry_Model $entry */ if ( isset( $mapper['property'] ) ) { if ( property_exists( $entry, $mapper['property'] ) ) { $property = $mapper['property']; // casting property to string. if ( is_array( $entry->$property ) ) { $value = implode( ', ', $entry->$property ); } else { $value = (string) $entry->$property; } } else { $value = ''; } } else { $meta_value = $entry->get_meta( $mapper['meta_key'], '' ); // meta_key based. $value = Hustle_Entry_Model::meta_value_to_string( $mapper['type'], $meta_value, $allow_html, $truncate ); } return $value; } /** * Executor for adding additional items on entry page. * * @see Hustle_Provider_Form_Hooks_Abstract::on_render_entry() * @since 4.0 * * @param Hustle_Entry_Model $entry_model Entry model. * @return array */ private function attach_addon_on_render_entry( Hustle_Entry_Model $entry_model ) { $additonal_items = array(); // Find all registered addons so history can be shown even for deactivated addons. $registered_addons = $this->get_registered_addons(); foreach ( $registered_addons as $registered_addon ) { try { $form_hooks = $registered_addon->get_addon_form_hooks( $this->module_id ); $meta_data = Hustle_Provider_Utils::find_addon_meta_data_from_entry_model( $registered_addon, $entry_model ); $addon_additional_items = $form_hooks->on_render_entry( $entry_model, $meta_data ); $addon_additional_items = self::format_addon_additional_items( $addon_additional_items ); $additonal_items[] = $addon_additional_items; } catch ( Exception $e ) { Opt_In_Utils::maybe_log( $registered_addon->get_slug(), 'failed to on_render_entry', $e->getMessage() ); } } return $additonal_items; } /** * Ensuring additional items for addons meet the entries data requirements. * Format used: * - label * - value * - subentries[] * - label * - value * * @since 4.0 * * @param array $addon_additional_items Addon additional items. * @return mixed */ private static function format_addon_additional_items( $addon_additional_items ) { // to `name` and `value` basis. $formatted_additional_items = array(); if ( ! is_array( $addon_additional_items ) ) { return array(); } foreach ( $addon_additional_items as $additional_item ) { if ( ! isset( $additional_item['name'] ) || ! isset( $additional_item['data_sent'] ) || ! isset( $additional_item['sub_entries'] ) ) { continue; } $sub_entries = array(); // Check if sub_entries are available. if ( isset( $additional_item['sub_entries'] ) && is_array( $additional_item['sub_entries'] ) ) { foreach ( $additional_item['sub_entries'] as $sub_entry ) { // Make sure label and value exist, without it, it will display empty row. if ( ! isset( $sub_entry['label'] ) || ! isset( $sub_entry['value'] ) ) { continue; } $sub_entries[] = array( 'label' => $sub_entry['label'], 'value' => $sub_entry['value'], ); } } $formatted_additional_items[] = array( 'name' => $additional_item['name'], 'icon' => $additional_item['icon'], 'data_sent' => $additional_item['data_sent'], 'sub_entries' => $sub_entries, ); } return $formatted_additional_items; } /** * Get Globally registered Addons, avoid overhead for checking registered addons many times * * @since 4.0 * * @return array|Hustle_Provider_Abstract[] */ public function get_registered_addons() { if ( empty( self::$registered_addons ) ) { self::$registered_addons = array(); $registered_addons = Hustle_Provider_Utils::get_registered_addons(); foreach ( $registered_addons as $registered_addon ) { try { $form_hooks = $registered_addon->get_addon_form_hooks( $this->module_id ); if ( $form_hooks instanceof Hustle_Provider_Form_Hooks_Abstract ) { self::$registered_addons[] = $registered_addon; } } catch ( Exception $e ) { Opt_In_Utils::maybe_log( $registered_addon->get_slug(), 'failed to get_addon_form_hooks', $e->getMessage() ); } } } return self::$registered_addons; } /** * Parsing filters from $_REQUEST * * @since 4.0 */ protected function parse_filters() { $request_data = $_REQUEST;// phpcs:ignore WordPress.Security.NonceVerification.Recommended $data_range = isset( $request_data['date_range'] ) ? sanitize_text_field( $request_data['date_range'] ) : ''; $search = isset( $request_data['search_email'] ) ? sanitize_text_field( $request_data['search_email'] ) : ''; $filters = array(); if ( ! empty( $data_range ) ) { $date_ranges = explode( ' - ', $data_range ); if ( is_array( $date_ranges ) && isset( $date_ranges[0] ) && isset( $date_ranges[1] ) ) { $date_ranges[0] = date( 'Y-m-d', strtotime( $date_ranges[0] ) );// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $date_ranges[1] = date( 'Y-m-d', strtotime( $date_ranges[1] ) );// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $filters['date_created'] = array( $date_ranges[0], $date_ranges[1] ); } } if ( ! empty( $search ) ) { $filters['search_email'] = $search; } $this->filters = $filters; } /** * Parsing order from $_REQUEST * * @since 4.0 */ protected function parse_order() { $valid_order_bys = array( 'entries.date_created', 'entries.entry_id', ); $valid_orders = array( 'DESC', 'ASC', ); $request_data = $_REQUEST;// phpcs:ignore WordPress.Security.NonceVerification.Recommended $order_by = isset( $request_data['order_by'] ) ? sanitize_text_field( $request_data['order_by'] ) : 'entries.date_created'; $order = isset( $request_data['order'] ) ? sanitize_text_field( $request_data['order'] ) : 'DESC'; if ( ! empty( $order_by ) ) { if ( ! in_array( $order_by, $valid_order_bys, true ) ) { $order_by = 'entries.date_created'; } $this->order['order_by'] = $order_by; } if ( ! empty( $order ) ) { $order = strtoupper( $order ); if ( ! in_array( $order, $valid_orders, true ) ) { $order = 'DESC'; } $this->order['order'] = $order; } } /** * Flag whether box filter is open or nope * * @since 4.0 * @return bool */ public function is_filter_box_enabled() { return ( ! empty( $this->filters ) && ! empty( $this->order ) ); } /** * Get module type param * * @since 4.0 * @return string */ public function get_module_type() { return $this->screen_params['module_type']; } /** * Get module id param * * @since 4.0 * @return string */ public function get_module_id() { return $this->screen_params['module_id']; } /** * Export the entries of the current module. * * @since 4.0 */ private function export() { $action = filter_input( INPUT_POST, 'hustle_action', FILTER_SANITIZE_SPECIAL_CHARS ); if ( 'export_listing' !== $action ) { return; } $nonce = filter_input( INPUT_POST, '_wpnonce', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! wp_verify_nonce( $nonce, 'hustle_module_export_listing' ) ) { return; } if ( ! current_user_can( 'hustle_access_emails' ) ) { return; } $id = filter_input( INPUT_POST, 'id', FILTER_VALIDATE_INT ); if ( ! $id ) { return; } $module = new Hustle_Module_Model( $id ); $filename = sprintf( 'hustle-%s-%s-%s-%s-emails.csv', $module->module_type, gmdate( 'Ymd-his' ), get_bloginfo( 'name' ), $module->module_name ); $filename = strtolower( sanitize_file_name( $filename ) ); $entries = $this->get_entries_for_export(); $fp = fopen( 'php://memory', 'w' ); // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen -- disable phpcs because it writes memory foreach ( $entries as $entry ) { $fields = self::get_formatted_csv_fields( $entry ); fputcsv( $fp, $fields ); } fseek( $fp, 0 ); header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); header( 'Content-Description: File Transfer' ); header( 'Content-Type: text/csv; charset=' . get_option( 'blog_charset' ), true ); header( 'Content-Disposition: attachment; filename="' . $filename . '";' ); header( 'Expires: 0' ); header( 'Pragma: public' ); // print BOM Char for Excel Compatible. echo chr( 239 ) . chr( 187 ) . chr( 191 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped // Send the generated csv lines to the browser. if ( function_exists( 'fpassthru' ) ) { fpassthru( $fp ); } elseif ( function_exists( 'stream_get_contents' ) ) { echo stream_get_contents( $fp ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } exit(); } /** * Get the entries as a ready to export csv array. * * @since 4.0 * @return array */ private function get_entries_for_export() { $headers = $this->get_fields_mappers(); $headers[] = array( 'meta_key' => 'hustle_ip', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'label' => 'IP', 'type' => 'ip', ); $header_labels = wp_list_pluck( $headers, 'label' ); $entries = array( $header_labels ); $all_entries = Hustle_Entry_Model::get_entries( $this->module_id ); // Get all entries. foreach ( $all_entries as $entry ) { $row = array(); // Get the entry's value for each header. foreach ( $headers as $header ) { $value = $this->get_entry_field_value( $entry, $header, '', false ); $row[] = $value; } $entries[] = $row; } return $entries; } // ==================== /** * Format csv fields. * * @since 4.0 * * @param array $fields Fields. * @return array|string */ public static function get_formatted_csv_fields( $fields ) { if ( empty( $fields ) || ! is_array( $fields ) ) { return $fields; } $formatted_fields = array(); foreach ( $fields as $field ) { if ( ! is_scalar( $field ) ) { $formatted_fields[] = ''; continue; } if ( is_scalar( $field ) ) { $formatted_fields[] = self::escape_csv_data( $field ); } } return $formatted_fields; } /** * Escape a string to be used in a CSV context * * Taken from Forminator, where was taken from WooCommerce CSV Exporter * * @see https://github.com/woocommerce/woocommerce/blob/master/includes/export/abstract-wc-csv-exporter.php * * @since 1.6 * * 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 http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/ * @see https://hackerone.com/reports/72785 * * @since 4.0 * * @param string $data CSV field to escape. * * @return string */ public static function escape_csv_data( $data ) { $active_content_triggers = array( '=', '+', '-', '@' ); if ( in_array( mb_substr( $data, 0, 1 ), $active_content_triggers, true ) ) { $data = "'" . $data . "'"; } return $data; } }