click_conversions( 'gclid', 'Google Click ID', 'google-ads', Settings::get( 'gads_conversion' ) ); break; case 'microsoft_ads': $this->click_conversions( 'msclkid', 'Microsoft Click ID', 'microsoft-ads', Settings::get( 'msads_conversion' ) ); break; case 'meta': $this->meta(); break; default: $this->leads(); } exit; } /** * All leads matching the list filters. */ private function leads() { $filters = Leads_Table::filters(); $out = $this->open( 'leads-' . gmdate( 'Y-m-d' ) . '.csv' ); fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- UTF-8 BOM so Excel detects the encoding. $this->row( $out, array( __( 'ID', 'dealroot' ), __( 'Date', 'dealroot' ), __( 'Name', 'dealroot' ), __( 'Email', 'dealroot' ), __( 'Phone', 'dealroot' ), __( 'Form', 'dealroot' ), __( 'Status', 'dealroot' ), __( 'Value', 'dealroot' ), __( 'Currency', 'dealroot' ), __( 'Channel', 'dealroot' ), __( 'Source', 'dealroot' ), __( 'Medium', 'dealroot' ), __( 'Campaign', 'dealroot' ), __( 'Last channel', 'dealroot' ), __( 'Last source', 'dealroot' ), __( 'Last campaign', 'dealroot' ), __( 'Landing page', 'dealroot' ), __( 'Device', 'dealroot' ), __( 'Browser', 'dealroot' ), __( 'Country', 'dealroot' ), __( 'City', 'dealroot' ), 'gclid', 'fbclid', 'msclkid', __( 'Note', 'dealroot' ), __( 'Submission', 'dealroot' ), ) ); $statuses = Leads::status_labels(); $page = 1; do { $result = Leads::query( array_merge( $filters, array( 'per_page' => self::BATCH, 'page' => $page, ) ) ); foreach ( $result['items'] as $lead ) { $this->row( $out, array( $lead->id, get_date_from_gmt( $lead->created_at ), $lead->name, $lead->email, $lead->phone, $lead->form_name, $statuses[ $lead->status ] ?? $lead->status, $lead->value, $lead->currency, Attribution::channel_label( $lead->channel ), $lead->source, $lead->medium, $lead->campaign, Attribution::channel_label( $lead->last_channel ), $lead->last_source, $lead->last_campaign, $lead->landing_page, Devices::type_label( (string) $lead->device ), Devices::browser_label( (string) $lead->browser ), Geo::country_name( (string) $lead->country ), $lead->city, $lead->gclid, $lead->fbclid, $lead->msclkid, $lead->note, $this->fields_text( $lead->fields ), ) ); } ++$page; $fetched = count( $result['items'] ); } while ( self::BATCH === $fetched ); fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } /** * Won leads with an ad click ID, in the offline conversion format of Google Ads and Microsoft Advertising. * * @param string $column Click ID column. * @param string $header Header of the click ID column. * @param string $file File name prefix. * @param string $name Name of the conversion in the ad platform. */ private function click_conversions( $column, $header, $file, $name ) { global $wpdb; // Ad platforms accept conversions up to 90 days after the click; older rows would only show up as errors. $since = gmdate( 'Y-m-d H:i:s', time() - 90 * DAY_IN_SECONDS ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $leads = $wpdb->get_results( $wpdb->prepare( "SELECT %i AS click_id, won_at, updated_at, value, currency FROM %i WHERE status = 'won' AND %i <> '' AND created_at >= %s ORDER BY won_at ASC", $column, Installer::table(), $column, $since ) ); $out = $this->open( $file . '-conversions-' . gmdate( 'Y-m-d' ) . '.csv' ); $this->row( $out, array( 'Parameters:TimeZone=+0000' ) ); $this->row( $out, array( $header, 'Conversion Name', 'Conversion Time', 'Conversion Value', 'Conversion Currency' ) ); foreach ( (array) $leads as $lead ) { $this->row( $out, array( $lead->click_id, $name, $lead->won_at ? $lead->won_at : $lead->updated_at, null === $lead->value ? '' : number_format( (float) $lead->value, 2, '.', '' ), null === $lead->value ? '' : ( '' !== $lead->currency ? $lead->currency : Settings::get( 'currency' ) ), ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } /** * Won leads as offline events for Meta Events Manager. Meta matches them by email and phone, * so leads without a Facebook click are included too. */ private function meta() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $leads = $wpdb->get_results( $wpdb->prepare( "SELECT name, email, phone, fbclid, created_at, won_at, updated_at, value, currency FROM %i WHERE status = 'won' AND (email <> '' OR phone <> '') ORDER BY won_at ASC", Installer::table() ) ); $out = $this->open( 'meta-offline-events-' . gmdate( 'Y-m-d' ) . '.csv' ); $this->row( $out, array( 'email', 'phone', 'fn', 'ln', 'event_name', 'event_time', 'value', 'currency', 'fbc' ) ); foreach ( (array) $leads as $lead ) { $parts = preg_split( '/\s+/', trim( (string) $lead->name ), 2 ); $time = strtotime( ( $lead->won_at ? $lead->won_at : $lead->updated_at ) . ' UTC' ); $this->row( $out, array( strtolower( $lead->email ), preg_replace( '/\D/', '', (string) $lead->phone ), strtolower( $parts[0] ?? '' ), strtolower( $parts[1] ?? '' ), 'Purchase', $time, null === $lead->value ? '' : number_format( (float) $lead->value, 2, '.', '' ), $lead->currency, '' !== $lead->fbclid ? 'fb.1.' . ( strtotime( $lead->created_at . ' UTC' ) * 1000 ) . '.' . $lead->fbclid : '', ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } /** * Sends download headers and opens the output stream. * * @param string $filename File name. * @return resource */ private function open( $filename ) { nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' ); return fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen } /** * Writes one row, neutralizing spreadsheet formulas. * * @param resource $out Stream. * @param array $values Values. */ private function row( $out, array $values ) { $values = array_map( static function ( $value ) { $value = (string) $value; $is_formula = preg_match( '/^[=@\t\r]/', $value ) || ( preg_match( '/^[+\-]/', $value ) && ! preg_match( '/^[+\-][\d\s().\/-]*$/', $value ) ); return $is_formula ? "'" . $value : $value; }, $values ); fputcsv( $out, $values, ',', '"', '\\' ); } /** * Submitted fields as "Label: value" lines. * * @param string $json Stored fields. * @return string */ private function fields_text( $json ) { $lines = array(); foreach ( (array) json_decode( (string) $json, true ) as $field ) { $lines[] = ( $field['label'] ?? '' ) . ': ' . ( $field['value'] ?? '' ); } return implode( "\n", $lines ); } }