nonce = $this->nonce( basename( __FILE__ ), 'reports' ); } /** * Create hooks. * * @return void */ public function hooks() { add_action( 'admin_init', array( $this, 'admin_init' ) ); foreach ( $this->get_action_keys() as $key ) { $key_underscore = self::_key( $key ); add_action( 'wp_ajax_' . $this->action_string( $key ), array( $this, 'action_' . $key_underscore ) ); } } /** * Fires on admin_init. Parse input if available. * * @return void */ public function admin_init() { $this->parse_input(); } /** * Enqueue scripts for the reports page. * * @return void */ public function enqueue() { $this->build_report_data(); wp_enqueue_script( 'jquery' ); wp_enqueue_script( 'jquery-ui-core' ); wp_enqueue_script( 'jquery-ui-datepicker' ); wp_enqueue_script( 'jquery-ui-autocomplete' ); wp_enqueue_script( 'jquery-ui-tabs' ); $this->enqueue_jquery_ui_styles(); $handle = Util::ns( 'reports' ); wp_register_script( $handle, AdCommander::assets_url() . 'js/reports.js', array( 'jquery', 'jquery-ui-core', 'jquery-ui-datepicker', 'jquery-ui-autocomplete', 'jquery-ui-tabs', ), AdCommander::version(), array( 'in_footer' => true ) ); wp_enqueue_script( $handle ); /** * Filter the data localized to the reports script. * * extraTables renders additional report tables. Each entry is keyed by * report type and contains: data (array of rows), cols (key => label), * elementId (container id), and optionally linkKey (row key rendered as * a link using the row's edit_link). * * @param array $script_data The localized data. * @param AdminReports $reports This reports instance. */ $term_filter_ajax = array( 'action' => $this->action_string( 'get-terms-for-filter' ), 'security' => wp_create_nonce( $this->nonce_string( 'get-terms-for-filter' ) ), ); $filter_fields = array( array_merge( $term_filter_ajax, array( 'key' => 'filter_by_group_ids', 'request' => array( 'tax' => 'group' ), ) ), array_merge( $term_filter_ajax, array( 'key' => 'filter_by_campaign_ids', 'request' => array( 'tax' => 'campaign' ), ) ), ); if ( $this->use_advertisers_tax() ) { $filter_fields[] = array_merge( $term_filter_ajax, array( 'key' => 'filter_by_advertiser_ids', 'request' => array( 'tax' => 'advertiser' ), ) ); } $script_data = apply_filters( 'adcmdr_report_script_data', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'actions' => $this->get_ajax_actions(), 'reportData' => $this->current_data_bydate, 'adData' => $this->current_data_byad, 'groupData' => $this->current_data_bygroup, 'campaignData' => $this->current_data_bycampaign, 'advertiserData' => $this->current_data_byadvertiser, 'tableByDateCols' => $this->table_by_date_columns(), 'tableByAdCols' => $this->table_by_ad_columns(), 'tableByGroupCols' => $this->table_by_group_columns(), 'tableByCampaignCols' => $this->table_by_campaign_columns(), 'tableByAdvertiserCols' => $this->table_by_advertiser_columns(), 'chartType' => $this->get_chart_type(), 'extraTables' => array(), 'filterFields' => $filter_fields, ), $this ); Util::enqueue_script_data( $handle, $script_data ); } /** * Get necessary action keys, which will be used to create wp_ajax hooks. * * @return array */ private function get_action_keys() { return array( 'get-ads-for-filter', 'get-terms-for-filter', 'export-csv', ); } /** * Creates an array of all of the necessary actions. * * @return array */ private function get_ajax_actions() { $actions = array(); foreach ( $this->get_action_keys() as $key ) { $actions[ self::_key( $key ) ] = array( 'action' => $this->action_string( $key ), 'security' => wp_create_nonce( $this->nonce_string( $key ) ), ); } return $actions; } /** * Create HTML for the title of the page. * * @param string $title The title of the page. * * @return void */ private function title( $title ) { ?>
sf()->title( $title ); ?>
sf()->wo_forms->label( 'chart_type', __( 'Chart:', 'ad-commander' ) ); $this->sf()->wo_forms->inputgroup( 'chart_type', $this->allowed_chart_types(), $this->get_chart_type(), 'radio', array( 'id' => 'adcmdr_chart_type' ) ); ?>
form_start(); $this->form(); $this->form_end(); $this->start_wrap(); $this->title( __( 'Reports', 'ad-commander' ) ); $this->chart(); $this->end_wrap(); $this->start_wrap(); $this->tabs_start(); $this->tab_nav(); $this->table_by_date(); $this->table_by_ad(); $this->table_by_group(); $this->table_by_campaign(); $this->table_by_advertiser(); /** * Render additional report table containers. * * @param AdminReports $reports This reports instance. */ do_action( 'adcmdr_report_tables', $this ); $this->tabs_end(); $this->end_wrap(); } private function tabs_start() { ?>
__( 'By Date', 'ad-commander' ), 'by_ad' => __( 'By Ad', 'ad-commander' ), 'by_group' => __( 'By Group', 'ad-commander' ), 'by_campaign' => __( 'By Campaign', 'ad-commander' ), ); if ( $this->use_advertisers_tax() ) { $tabs['by_advertiser'] = __( 'By Brand', 'ad-commander' ); } /** * Filter the report tabs. Keys map to container ids: adcmdr_tab_{key}. * * @param array $tabs Tab labels keyed by report type. */ $tabs = apply_filters( 'adcmdr_report_tabs', $tabs ); ?>

is_pro_loaded() ? ProBridge::instance()->pro_label( true ) : ''; ?>
get_group_by() === 'week' ) : ?>
', esc_html( Util::first_day_of_the_week() ), '' ); ?>

is_pro_loaded() ? ProBridge::instance()->pro_label( true ) : ''; ?>

is_pro_loaded() ? ProBridge::instance()->pro_label( true ) : ''; ?>

is_pro_loaded() ? ProBridge::instance()->pro_label( true ) : ''; ?>

is_pro_loaded() ? ProBridge::instance()->pro_label( true ) : ''; ?>

( $this->get_group_by() === 'hour' ) ? __( 'Hour', 'ad-commander' ) : __( 'Date', 'ad-commander' ), 'impressions' => __( 'Impressions', 'ad-commander' ), 'clicks' => __( 'Clicks', 'ad-commander' ), 'ctr' => __( 'CTR', 'ad-commander' ), ); } /** * Create the columns for the by-ad table. */ private function table_by_ad_columns() { return array( 'ad' => __( 'Ad', 'ad-commander' ), 'impressions' => __( 'Impressions', 'ad-commander' ), 'clicks' => __( 'Clicks', 'ad-commander' ), 'ctr' => __( 'CTR', 'ad-commander' ), ); } /** * Create the columns for the by-group table. */ private function table_by_group_columns() { return array( 'group' => __( 'Group', 'ad-commander' ), 'impressions' => __( 'Impressions', 'ad-commander' ), 'clicks' => __( 'Clicks', 'ad-commander' ), 'ctr' => __( 'CTR', 'ad-commander' ), ); } /** * Create the columns for the by-campaign table. */ private function table_by_campaign_columns() { return array( 'campaign' => __( 'Campaign', 'ad-commander' ), 'impressions' => __( 'Impressions', 'ad-commander' ), 'clicks' => __( 'Clicks', 'ad-commander' ), 'ctr' => __( 'CTR', 'ad-commander' ), ); } /** * Create the columns for the by-advertiser table. */ private function table_by_advertiser_columns() { return array( 'advertiser' => __( 'Brand', 'ad-commander' ), 'impressions' => __( 'Impressions', 'ad-commander' ), 'clicks' => __( 'Clicks', 'ad-commander' ), 'ctr' => __( 'CTR', 'ad-commander' ), ); } protected function sanitize_all_input( $source = array() ) { $input = array(); /** * Filter the input keys accepted by reports. * Keys prefixed with filter_by_ are sanitized as arrays of integers. * * @param array $keys The allowed input keys. */ $allowed_keys = apply_filters( 'adcmdr_report_allowed_input_keys', array( 'period', 'group_by', 'chart_type', 'start_date', 'end_date', 'filter_by_ad_ids', 'filter_by_group_ids', 'filter_by_campaign_ids', 'filter_by_advertiser_ids', ) ); foreach ( $allowed_keys as $key ) { if ( ! isset( $source[ $key ] ) ) { continue; } $value = ( is_array( $source[ $key ] ) ) ? array_map( 'sanitize_text_field', wp_unslash( $source[ $key ] ) ) : sanitize_text_field( wp_unslash( $source[ $key ] ) ); switch ( $key ) { case 'start_date': case 'end_date': $type = 'date'; break; default: $type = strpos( $key, 'filter_by_' ) === 0 ? 'ints' : 'str'; break; } $value = $this->sanitize_by_type( $value, $type ); $input[ $key ] = $value; } return $input; } /** * Parse the input for later use. */ protected function parse_input() { if ( ! isset( $_REQUEST[ $this->nonce['name'] ] ) ) { return; } if ( ! wp_verify_nonce( sanitize_key( $_REQUEST[ $this->nonce['name'] ] ), $this->nonce['action'] ) ) { die( esc_html( __( 'Security check', 'ad-commander' ) ) ); } $this->input = $this->sanitize_all_input( $_REQUEST ); } /** * Start of the form on the Reports page. * * @return void */ private function form_start() { ?>
sf()->wo_forms->label( 'start_date', __( 'Start Date', 'ad-commander' ), array( 'classes' => 'screen-reader-text', 'for' => 'adcmdr_start_date', ) ); $this->sf()->wo_forms->input( 'start_date', $this->get_start_date(), 'text', array( 'id' => 'adcmdr_start_date', 'classes' => 'adcmdr-datepicker', 'placeholder' => __( 'Start Date' ), ) ); ?>
sf()->wo_forms->label( 'end_date', __( 'End Date', 'ad-commander' ), array( 'classes' => 'screen-reader-text', 'for' => 'adcmdr_end_date', ) ); $this->sf()->wo_forms->input( 'end_date', $this->get_end_date(), 'text', array( 'id' => 'adcmdr_end_date', 'classes' => 'adcmdr-datepicker', 'placeholder' => __( 'End Date' ), ) ); ?>
sf()->wo_forms->select( 'period', self::allowed_periods( false ), $this->get_period(), array( 'id' => 'adcmdr_period' ) ); ?>
sf()->wo_forms->select( 'group_by', self::allowed_group_by( false ), $this->get_group_by(), array( 'id' => 'adcmdr_group_by' ) ); ?>
    get_filtered_input_ids( $key ); $items = array(); if ( empty( $ids ) ) { return $items; } $terms = get_terms( array( 'taxonomy' => $taxonomy, 'include' => $ids, 'hide_empty' => false, ) ); if ( ! $terms || is_wp_error( $terms ) ) { return $items; } foreach ( $terms as $term ) { $items[] = array( 'id' => $term->term_id, 'title' => $term->name, ); } return $items; } /** * Render an autocomplete report filter field, following the Filter By Ads pattern: * a search input, removable chips, and a hidden input holding selected IDs. * The autocomplete is wired in reports.js from the localized filterFields config. * * @param string $key The input key. * @param string $label The field label. * @param array $selected_items Selected items: arrays with id and title. * @param string|null $chip_label Optional type label shown on chips mirrored into the filter bar. * @param string|null $placeholder Optional placeholder text. * * @return void */ public function filter_autocomplete_field( $key, $label, $selected_items = array(), $chip_label = null, $placeholder = null ) { ?>
    sf()->wo_forms->label( $key . '_input', $label ); $this->sf()->wo_forms->input( $key . '_input', '', 'text', array( 'id' => 'adcmdr_' . $key . '_input', 'placeholder' => $placeholder ? $placeholder : __( 'Search by name', 'ad-commander' ), ) ); ?>
    nonce_field( $this->nonce ); ?>
    date_display_format ) { if ( $this->get_group_by() !== 'hour' ) { $this->date_display_format = get_option( 'date_format' ); } else { $this->date_display_format = get_option( 'time_format' ); } } return $this->date_display_format; } /** * Alternate display format for dates. * * @param string $format The date format to modify. */ private function date_display_format_alt( $format ) { return str_replace( array( 'F ', ', Y' ), array( 'M ', ', \'y' ), $format ); } /** * The allowed time periods and their related info. * * @param bool $detailed Whether to return a detailed array or not. * * @return array */ public static function allowed_periods( $detailed = true ) { $periods = array( 'today' => array( 'text' => __( 'Today', 'ad-commander' ), 'modifier' => '', ), 'yesterday' => array( 'text' => __( 'Yesterday', 'ad-commander' ), 'modifier' => '', ), 'this_week' => array( 'text' => __( 'This Week', 'ad-commander' ), 'modifier' => 'this week', ), 'this_month' => array( 'text' => __( 'This Month', 'ad-commander' ), 'modifier' => 'first day of this month', ), 'this_year' => array( 'text' => __( 'This Year', 'ad-commander' ), 'modifier' => 'first day of January', ), 'last_week' => array( 'text' => __( 'Last Week', 'ad-commander' ), 'modifier' => 'last week', ), 'last_month' => array( 'text' => __( 'Last Month', 'ad-commander' ), 'modifier' => 'last month', ), 'last_year' => array( 'text' => __( 'Last Year', 'ad-commander' ), 'modifier' => 'last year', ), 'past_7_days' => array( 'text' => __( 'Past 7 days', 'ad-commander' ), 'modifier' => '-6 days', ), 'past_14_days' => array( 'text' => __( 'Past 14 days', 'ad-commander' ), 'modifier' => '-13 days', ), 'past_30_days' => array( 'text' => __( 'Past 30 days', 'ad-commander' ), 'modifier' => '-29 days', ), 'past_365_days' => array( 'text' => __( 'Past 365 days', 'ad-commander' ), 'modifier' => '-364 days', ), 'custom' => array( 'text' => __( 'Custom', 'ad-commander' ), 'modifier' => '', ), ); if ( ! $detailed ) { $periods_simple = array(); foreach ( $periods as $key => $value ) { $periods_simple[ $key ] = $value['text']; } return $periods_simple; } return $periods; } /** * The allowed group_by keys and their related info. * * @param bool $detailed Whether to return a detailed array or not. * * @return array */ public static function allowed_group_by( $detailed = true ) { $periods = array( 'hour' => array( 'text' => __( 'Hour', 'ad-commander' ), 'modifier' => '+1 hour', ), 'day' => array( 'text' => __( 'Day', 'ad-commander' ), 'modifier' => '+1 day', ), 'week' => array( 'text' => __( 'Week', 'ad-commander' ), 'modifier' => '+1 week', ), 'month' => array( 'text' => __( 'Month', 'ad-commander' ), 'modifier' => '+1 month', ), ); if ( ! $detailed ) { $periods_simple = array(); foreach ( $periods as $key => $value ) { $periods_simple[ $key ] = $value['text']; } return $periods_simple; } return $periods; } /** * The allowed chart types. * * @return array */ private function allowed_chart_types() { return array( 'line' => 'Line', 'bar' => 'Bar', ); } /** * Returns a modifier string for a time period if one exists. * * @param string $period The period to find the modifier for. * * @return null|string */ private function datetime_modifier_string( $period ) { $periods = self::allowed_periods(); return isset( $periods[ $period ]['modifier'] ) ? $periods[ $period ]['modifier'] : null; } /** * Returns the ad IDs that we are filtering by, if any. * * @return array */ private function get_filtered_ad_ids() { if ( isset( $this->input['filter_by_ad_ids'] ) ) { return Util::arrayify( $this->input['filter_by_ad_ids'] ); } return array(); } /** * Returns the sanitized IDs for a filter input key, if any. * * @param string $key The input key. * * @return array */ public function get_filtered_input_ids( $key ) { if ( isset( $this->input[ $key ] ) ) { return array_values( array_filter( array_map( 'absint', Util::arrayify( $this->input[ $key ] ) ) ) ); } return array(); } /** * Resolve all active report filters to a single set of ad IDs. * Selections within one filter are OR; separate filters are AND (intersected). * * @return array Ad IDs to report on. Empty array = no filtering. */ protected function report_filtered_ad_ids() { $sets = array(); $ad_ids = $this->get_filtered_ad_ids(); if ( ! empty( $ad_ids ) ) { $sets[] = array_map( 'absint', $ad_ids ); } foreach ( array( 'filter_by_group_ids' => AdCommander::tax_group(), 'filter_by_campaign_ids' => AdCommander::tax_campaign(), 'filter_by_advertiser_ids' => AdCommander::tax_advertiser(), ) as $key => $taxonomy ) { $term_ids = $this->get_filtered_input_ids( $key ); if ( ! empty( $term_ids ) ) { $object_ids = get_objects_in_term( $term_ids, $taxonomy ); $sets[] = is_wp_error( $object_ids ) ? array() : array_map( 'absint', $object_ids ); } } /** * Filter the sets of ad IDs that each active report filter resolved to. * Each set is intersected with the others. * * @param array $sets Arrays of ad IDs, one per active filter. * @param array $input The sanitized report input. * @param AdminReports $reports This reports instance. */ $sets = apply_filters( 'adcmdr_report_filtered_ad_id_sets', $sets, $this->input, $this ); if ( empty( $sets ) ) { return array(); } $ids = array_shift( $sets ); foreach ( $sets as $set ) { $ids = array_intersect( $ids, $set ); } $ids = array_values( array_unique( array_map( 'absint', $ids ) ) ); /** * Active filters that match nothing must produce an empty report, * not an unfiltered one. */ return empty( $ids ) ? array( 0 ) : $ids; } /** * Gets an array of ads based on selected filters. * * @return array */ private function get_filtered_ads() { $ads = $this->get_ads_for_filter(); $filtered_ad_ids = $this->get_filtered_ad_ids(); $filtered_ads = array(); foreach ( $ads as $ad ) { if ( in_array( $ad['id'], $filtered_ad_ids ) ) { $filtered_ads[] = $ad; } } return $filtered_ads; } /** * Gets the current filtered period. * * @return string */ private function get_period() { if ( isset( $this->input['period'] ) && in_array( $this->input['period'], array_keys( self::allowed_periods() ) ) ) { return $this->input['period']; } return 'past_7_days'; } /** * Gets the current filtered group_by. * * @return string */ private function get_group_by() { if ( isset( $this->input['group_by'] ) && in_array( $this->input['group_by'], array_keys( self::allowed_group_by() ) ) ) { return $this->input['group_by']; } return 'day'; } /** * Gets the current chart type. * * @return string */ private function get_chart_type() { if ( isset( $this->input['chart_type'] ) && in_array( $this->input['chart_type'], array_keys( $this->allowed_chart_types() ) ) ) { return $this->input['chart_type']; } return 'line'; } /** * Gets the current filtered start date. * * @return string|null */ private function get_start_date() { if ( isset( $this->input['start_date'] ) && $this->get_period() === 'custom' ) { return $this->input['start_date']; } return null; } /** * Gets the current filtered end date. * * @return string|null */ private function get_end_date() { if ( isset( $this->input['end_date'] ) && $this->get_period() === 'custom' ) { return $this->input['end_date']; } return null; } /** * Gets the modifier string for a group_by string. * * @param string $group_by The period we're grouping by. * * @return string */ private function get_group_by_modifier( $group_by ) { switch ( $group_by ) { case 'month': $modifier = 'first day of next month'; break; case 'week': $modifier = 'next ' . Util::first_day_of_the_week(); break; case 'hour': $modifier = 'next hour'; break; case 'day': default: $modifier = 'next day'; break; } return $modifier; } /** * Creates an array of dates from a start and an end date. * * @param array $start_end_dates Array containing two DateTime objects. * @param string $group_by Determines the frequency of the range of dates. * @param bool $include_end_date Whether to include the end date or not. * * @return array */ private function range_of_start_dates( $start_end_dates, $group_by = 'day', $include_end_date = true ) { $start_date = clone $start_end_dates['start_date']; $end_date = clone $start_end_dates['end_date']; if ( $include_end_date ) { $end_date = $end_date->modify( '+1 day' ); // So that $end_date is included in our DatePeriod } if ( $group_by === 'hour' ) { $now = Util::datetime_wp_timezone( current_time( 'Y-m-d H:00:00' ) ); $now->modify( 'next hour' )->modify( '-1 second' ); if ( $end_date->getTimestamp() > $now->getTimestamp() ) { $end_date = clone $now; } } $modifier = $this->get_group_by_modifier( $group_by ); $periods = array(); $date = clone $start_date; while ( $date->getTimestamp() < $end_date->getTimestamp() ) { $periods[] = clone $date; $date = $date->modify( $modifier ); } $dates = array(); $format = $this->date_display_format(); $format_alt = $this->date_display_format_alt( $format ); foreach ( $periods as $key => $value ) { $timestamp = $value->getTimestamp(); $value_formatted = $value->format( $format ); $value_formatted_alt = $value->format( $format_alt ); $dates[] = array( 'display_format' => $value_formatted, 'display_format_alt' => $value_formatted_alt, 'timestamp' => $timestamp, 'datetime' => $value, ); } return $dates; } /** * Creates start and end dates for a given period. * * @param string $period The period for which we are getting start and end dates. * * @return array */ private function start_end_dates( $period ) { if ( $period === 'custom' ) { $start_date_custom = $this->get_start_date(); $end_date_custom = $this->get_end_date(); if ( ! $start_date_custom || ! $end_date_custom ) { return false; } $start_date = Util::datetime_wp_timezone( $start_date_custom )->modify( 'today' ); $end_date = Util::datetime_wp_timezone( $end_date_custom )->modify( 'today' ); } else { $now = current_time( 'Y-m-d H:i:s' ); $now = Util::datetime_wp_timezone( $now )->modify( 'today' ); $end_date = clone $now; if ( $period === 'yesterday' ) { $end_date = $end_date->modify( '-1 day' ); } $start_date = clone $end_date; } $modifier = $this->datetime_modifier_string( $period ); if ( $modifier ) { if ( $modifier === 'this week' ) { $start_week = Util::first_day_of_the_week(); $today = clone $now; /** * If the week starts today, end date and start date are the same. */ if ( $today->format( 'l' ) !== $start_week ) { $start_date->modify( 'last ' . $start_week ); } } elseif ( $modifier === 'last week' ) { $start_week = Util::first_day_of_the_week(); if ( $end_date->format( 'l' ) === $start_week ) { $end_date->modify( '-1 day' ); } else { $end_date->modify( 'last ' . $start_week )->modify( '-1 day' ); } $end_date->modify( 'today' ); $start_date = clone $end_date; $start_date->modify( '-6 days' ); } elseif ( $modifier === 'last month' ) { $end_date->modify( 'last day of last month' )->modify( 'today' ); $start_date = clone $end_date; $start_date->modify( 'first day of this month' ); } elseif ( $modifier === 'last year' ) { $end_date->modify( 'last year December 31' )->modify( 'today' ); $start_date = clone $end_date; $start_date->modify( 'this year January 1' ); } else { $start_date = $start_date->modify( $modifier ); } } $start_date->modify( 'today' ); /** * Return set of dates */ return array( 'start_date' => $start_date, 'end_date' => $end_date, ); } /** * The name of the transient that stores our filtered ads. * * @return string */ public static function filter_ads_transient() { return Util::ns( 'report_filter_ads', '_' ); } /** * Store the available filter ads in a transient so we don't have to query them every time. * * @return array */ private function get_ads_for_filter() { $ads = get_transient( self::filter_ads_transient() ); if ( $ads === false ) { $available_ads = Query::ads( 'post_title', 'asc', Util::any_post_status(), array(), array() ); $ads = array(); if ( ! empty( $available_ads ) ) { foreach ( $available_ads as $ad ) { $ads[] = array( 'id' => $ad->ID, 'title' => $ad->post_title, ); } } set_transient( self::filter_ads_transient(), $ads, MINUTE_IN_SECONDS ); } return $ads; } /** * Wp_ajax hook. Gets all of the ads for use in the filter. * * @return void */ public function action_get_ads_for_filter() { $action = 'get-ads-for-filter'; check_ajax_referer( $this->nonce_string( $action ), 'security' ); if ( ! current_user_can( AdCommander::capability() ) ) { wp_die(); } wp_send_json_success( array( 'action' => $action, 'ads' => $this->get_ads_for_filter(), ) ); wp_die(); } /** * Wp_ajax hook. Gets the terms of a taxonomy for use in a report filter. * * @return void */ public function action_get_terms_for_filter() { $action = 'get-terms-for-filter'; check_ajax_referer( $this->nonce_string( $action ), 'security' ); if ( ! current_user_can( AdCommander::capability() ) ) { wp_die(); } $tax_key = isset( $_REQUEST['tax'] ) ? sanitize_key( wp_unslash( $_REQUEST['tax'] ) ) : ''; $taxonomies = array( 'group' => AdCommander::tax_group(), 'campaign' => AdCommander::tax_campaign(), 'advertiser' => AdCommander::tax_advertiser(), ); if ( ! isset( $taxonomies[ $tax_key ] ) ) { wp_send_json_error(); } $items = array(); $terms = get_terms( array( 'taxonomy' => $taxonomies[ $tax_key ], 'hide_empty' => false, ) ); if ( $terms && ! is_wp_error( $terms ) ) { foreach ( $terms as $term ) { $items[] = array( 'id' => $term->term_id, 'title' => $term->name, ); } } wp_send_json_success( array( 'action' => $action, 'items' => $items, ) ); wp_die(); } /** * Wp_ajax hook. Gets all of the ads for use in the filter. * * @return void */ public function action_export_csv() { $action = 'export-csv'; check_ajax_referer( $this->nonce_string( $action ), 'security' ); if ( ! current_user_can( AdCommander::capability() ) ) { wp_die(); } if ( ProBridge::instance()->pro_feature_check( 'exportreports' ) ) { AdminExport::instance()->do_export(); } wp_die(); } /** * Get the total stats for a given day or days. * * @param array $ad_ids The ad IDs to total stats for. * @param string|int $days Either 'all' or the number of days to look back when totaling. * @param string $type The type of stat to count. * * @return int */ public function ad_stats( $ad_ids, $days = 'all', $type = 'impressions' ) { if ( $days === 'all' ) { return TrackingLocal::total_stats( $ad_ids, $type, null, null ); } $end_date = Util::datetime_wp_timezone( current_time( 'Y-m-d H:00:00' ) ); $end_date->modify( 'next hour' )->modify( '-1 second' ); $days = ( intval( $days ) - 1 ); if ( $days < 0 ) { $days = 0; } $start_date = clone $end_date; $start_date->modify( '-' . $days . ' days' )->modify( 'today' ); return TrackingLocal::total_stats( $ad_ids, $type, $start_date->getTimestamp(), $end_date->getTimestamp() ); } /** * Builds the data for display on the report page. * * @return void */ protected function build_report_data( $ignore_notices = false ) { $this->current_data_bydate = array(); $this->current_data_byad = array(); $this->current_data_bygroup = array(); $this->current_data_bycampaign = array(); $this->current_data_byadvertiser = array(); $notice = null; $period = $this->get_period(); $start_end_dates = $this->start_end_dates( $period ); if ( ! isset( $start_end_dates['start_date'] ) || ! isset( $start_end_dates['end_date'] ) || ! $start_end_dates['start_date'] || ! $start_end_dates['end_date'] ) { $notice = __( 'There was an error processing your start and end dates.', 'ad-commander' ); } if ( $start_end_dates['start_date'] > $start_end_dates['end_date'] && ! $ignore_notices ) { $notice = __( 'The selected start date is before the end date.', 'ad-commander' ); } if ( $notice && ! $ignore_notices ) { add_action( 'admin_notices', function () use ( $notice ) { ?>

    get_group_by(); $start_date_range = $this->range_of_start_dates( $start_end_dates, $group_by ); $i = 0; $bucket_ranges = array(); $filtered_ad_ids = $this->report_filtered_ad_ids(); foreach ( $start_date_range as $date ) { if ( isset( $start_date_range[ $i + 1 ]['datetime'] ) ) { $end_datetime = clone $start_date_range[ $i + 1 ]['datetime']; $end_datetime->modify( '-1 second' ); } else { $end_datetime = clone $start_end_dates['end_date']; $end_datetime->modify( 'tomorrow' )->modify( '-1 second' ); } $bucket_ranges[] = array( 'start' => $date['timestamp'], 'end' => $end_datetime->getTimestamp(), 'end_datetime' => $end_datetime, ); ++$i; } $range_start_ts = $start_date_range[0]['timestamp']; $range_end_ts = $end_datetime->getTimestamp(); /** * Stats are stored per hourly timestamp, so two grouped queries cover * every chart bucket. Sums are assigned to buckets in a single pass. */ $impressions_by_ts = TrackingLocal::totals_by_timestamp( 'impressions', $range_start_ts, $range_end_ts, $filtered_ad_ids ); $clicks_by_ts = TrackingLocal::totals_by_timestamp( 'clicks', $range_start_ts, $range_end_ts, $filtered_ad_ids ); $bucket_impressions = self::bucket_sums( $impressions_by_ts, $bucket_ranges ); $bucket_clicks = self::bucket_sums( $clicks_by_ts, $bucket_ranges ); $parsed_dates = array(); foreach ( $start_date_range as $i => $date ) { $impressions = $bucket_impressions[ $i ]; $clicks = $bucket_clicks[ $i ]; $end_datetime = $bucket_ranges[ $i ]['end_datetime']; $display_formatted = $date['display_format']; if ( $group_by !== 'hour' && $date['datetime']->format( $this->date_display_format() ) !== $end_datetime->format( $this->date_display_format() ) ) { $display_formatted = $date['datetime']->format( $this->date_display_format() ) . ' – ' . $end_datetime->format( $this->date_display_format() ); } $parsed_dates[] = array( 'date' => $display_formatted, 'date_alt' => $date['display_format_alt'], 'timestamp' => $date['timestamp'], 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => Util::ctr( $clicks, $impressions ), ); } $this->current_data_bydate = $parsed_dates; $parsed_ads = array(); $ad_groups = array(); $ad_campaigns = array(); $ad_advertisers = array(); /** * One grouped query per table replaces the former per-ad stat queries, * and also determines which ads have stats in the range. */ $impressions_by_ad = TrackingLocal::totals_by_ad( 'impressions', $range_start_ts, $range_end_ts, $filtered_ad_ids ); $clicks_by_ad = TrackingLocal::totals_by_ad( 'clicks', $range_start_ts, $range_end_ts, $filtered_ad_ids ); $ad_ids = array_values( array_unique( array_merge( array_keys( $impressions_by_ad ), array_keys( $clicks_by_ad ) ) ) ); if ( ! empty( $ad_ids ) ) { $ads = Query::ads( 'post_title', 'asc', Util::any_post_status(), array(), array(), $ad_ids ); $ads_by_id = array(); foreach ( $ads as $ad ) { $ads_by_id[ $ad->ID ] = $ad; } /** * One batched term query for all ads and all three taxonomies * replaces the former three wp_get_post_terms() calls per ad. */ $all_terms = wp_get_object_terms( $ad_ids, array( AdCommander::tax_group(), AdCommander::tax_campaign(), AdCommander::tax_advertiser() ), array( 'fields' => 'all_with_object_id' ) ); $terms_by_taxonomy = array( AdCommander::tax_group() => array(), AdCommander::tax_campaign() => array(), AdCommander::tax_advertiser() => array(), ); if ( ! is_wp_error( $all_terms ) && ! empty( $all_terms ) ) { foreach ( $all_terms as $term ) { $terms_by_taxonomy[ $term->taxonomy ][ $term->object_id ][] = $term; } } $ad_groups = $terms_by_taxonomy[ AdCommander::tax_group() ]; $ad_campaigns = $terms_by_taxonomy[ AdCommander::tax_campaign() ]; $ad_advertisers = $terms_by_taxonomy[ AdCommander::tax_advertiser() ]; foreach ( $ad_ids as $ad_id ) { $impressions = isset( $impressions_by_ad[ $ad_id ] ) ? $impressions_by_ad[ $ad_id ] : 0; $clicks = isset( $clicks_by_ad[ $ad_id ] ) ? $clicks_by_ad[ $ad_id ] : 0; $this_ad = isset( $ads_by_id[ $ad_id ] ) ? $ads_by_id[ $ad_id ] : new \stdClass(); $parsed_ads[] = array( 'ad_id' => $ad_id, /* translators: %d: Ad id */ 'ad' => isset( $this_ad->post_title ) ? $this_ad->post_title : sprintf( __( 'Ad not found (ID: %d)', 'ad-commander' ), $ad_id ), 'edit_link' => isset( $this_ad->post_title ) ? Admin::edit_ad_post_url( $this_ad->ID, $this_ad->post_status ) : null, 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => Util::ctr( $clicks, $impressions ), ); } } if ( ! empty( $parsed_ads ) ) { usort( $parsed_ads, function ( $a, $b ) { if ( $a['edit_link'] === null ) { return PHP_INT_MAX; } return $a['ad'] <=> $b['ad']; } ); } $this->current_data_byad = $parsed_ads; $parsed_groups = array(); /** * When a dimension is actively filtered, its own tab only lists the * selected rows. Other memberships of the matching ads are excluded. */ $selected_group_ids = $this->get_filtered_input_ids( 'filter_by_group_ids' ); if ( ! empty( $ad_groups ) ) { foreach ( $ad_groups as $ad_id => $groups ) { foreach ( $groups as $group ) { if ( ! empty( $selected_group_ids ) && ! in_array( (int) $group->term_id, $selected_group_ids, true ) ) { continue; } if ( ! isset( $parsed_groups[ $group->term_id ] ) ) { $parsed_groups[ $group->term_id ] = array( 'group_id' => $group->term_id, 'group' => $group->name, 'impressions' => 0, 'clicks' => 0, 'ctr' => 0, 'edit_link' => Admin::admin_group_term_url( $group->term_id ), ); } $parsed_groups[ $group->term_id ]['impressions'] += isset( $impressions_by_ad[ $ad_id ] ) ? $impressions_by_ad[ $ad_id ] : 0; $parsed_groups[ $group->term_id ]['clicks'] += isset( $clicks_by_ad[ $ad_id ] ) ? $clicks_by_ad[ $ad_id ] : 0; } } foreach ( $parsed_groups as $group_id => $group_data ) { $parsed_groups[ $group_id ]['ctr'] = Util::ctr( $group_data['clicks'], $group_data['impressions'] ); } } $this->current_data_bygroup = array_values( $parsed_groups ); $parsed_campaigns = array(); $selected_campaign_ids = $this->get_filtered_input_ids( 'filter_by_campaign_ids' ); if ( ! empty( $ad_campaigns ) ) { foreach ( $ad_campaigns as $ad_id => $campaigns ) { foreach ( $campaigns as $campaign ) { if ( ! empty( $selected_campaign_ids ) && ! in_array( (int) $campaign->term_id, $selected_campaign_ids, true ) ) { continue; } if ( ! isset( $parsed_campaigns[ $campaign->term_id ] ) ) { $parsed_campaigns[ $campaign->term_id ] = array( 'campaign_id' => $campaign->term_id, 'campaign' => $campaign->name, 'impressions' => 0, 'clicks' => 0, 'ctr' => 0, 'edit_link' => Admin::admin_campaign_term_url( $campaign->term_id ), ); } $parsed_campaigns[ $campaign->term_id ]['impressions'] += isset( $impressions_by_ad[ $ad_id ] ) ? $impressions_by_ad[ $ad_id ] : 0; $parsed_campaigns[ $campaign->term_id ]['clicks'] += isset( $clicks_by_ad[ $ad_id ] ) ? $clicks_by_ad[ $ad_id ] : 0; } } foreach ( $parsed_campaigns as $campaign_id => $campaign_data ) { $parsed_campaigns[ $campaign_id ]['ctr'] = Util::ctr( $campaign_data['clicks'], $campaign_data['impressions'] ); } } $this->current_data_bycampaign = array_values( $parsed_campaigns ); $parsed_advertisers = array(); $selected_advertiser_ids = $this->get_filtered_input_ids( 'filter_by_advertiser_ids' ); if ( ! empty( $ad_advertisers ) ) { foreach ( $ad_advertisers as $ad_id => $advertisers ) { foreach ( $advertisers as $advertiser ) { if ( ! empty( $selected_advertiser_ids ) && ! in_array( (int) $advertiser->term_id, $selected_advertiser_ids, true ) ) { continue; } if ( ! isset( $parsed_advertisers[ $advertiser->term_id ] ) ) { $parsed_advertisers[ $advertiser->term_id ] = array( 'advertiser_id' => $advertiser->term_id, 'advertiser' => $advertiser->name, 'impressions' => 0, 'clicks' => 0, 'ctr' => 0, 'edit_link' => Admin::admin_advertiser_term_url( $advertiser->term_id ), ); } $parsed_advertisers[ $advertiser->term_id ]['impressions'] += isset( $impressions_by_ad[ $ad_id ] ) ? $impressions_by_ad[ $ad_id ] : 0; $parsed_advertisers[ $advertiser->term_id ]['clicks'] += isset( $clicks_by_ad[ $ad_id ] ) ? $clicks_by_ad[ $ad_id ] : 0; } } foreach ( $parsed_advertisers as $advertiser_id => $advertiser_data ) { $parsed_advertisers[ $advertiser_id ]['ctr'] = Util::ctr( $advertiser_data['clicks'], $advertiser_data['impressions'] ); } } $this->current_data_byadvertiser = array_values( $parsed_advertisers ); /** * Fires after report data has been built. * * @param AdminReports $reports This reports instance. * @param array $parsed_ads The per-ad report rows. * @param array $range The report range: start and end timestamps. */ do_action( 'adcmdr_report_data_built', $this, $parsed_ads, array( 'start' => $range_start_ts, 'end' => $range_end_ts, ) ); } /** * Assign per-timestamp sums to ordered bucket ranges in a single pass. * * @param array $sums_by_ts Sums keyed by timestamp. * @param array $bucket_ranges Ordered arrays with start and end timestamps. * * @return array Sums indexed like $bucket_ranges. */ private static function bucket_sums( $sums_by_ts, $bucket_ranges ) { $totals = array_fill( 0, count( $bucket_ranges ), 0 ); $count = count( $bucket_ranges ); $b = 0; ksort( $sums_by_ts ); foreach ( $sums_by_ts as $ts => $total ) { while ( $b < $count && $ts > $bucket_ranges[ $b ]['end'] ) { ++$b; } if ( $b >= $count ) { break; } if ( $ts >= $bucket_ranges[ $b ]['start'] ) { $totals[ $b ] += $total; } } return $totals; } /** * Build report data from provided input and return the results. * Intended for programmatic use (e.g., abilities API). * * @param array $input Associative array with keys: period, group_by, start_date, end_date, filter_by_ad_ids. * * @return array|WP_Error */ public function build_report( $input = array() ) { // Convert Y-m-d dates to the F j, Y format expected by sanitize_all_input. foreach ( array( 'start_date', 'end_date' ) as $date_key ) { if ( isset( $input[ $date_key ] ) ) { $date = \DateTime::createFromFormat( 'Y-m-d', $input[ $date_key ] ); if ( $date && $date->format( 'Y-m-d' ) === $input[ $date_key ] ) { $input[ $date_key ] = $date->format( 'F j, Y' ); } } } $this->input = $this->sanitize_all_input( $input ); $this->build_report_data( true ); $cast_ctr = function ( $item ) { $item['ctr'] = (float) $item['ctr']; return $item; }; $report = array( 'by_date' => array_map( $cast_ctr, $this->current_data_bydate ), 'by_ad' => array_map( $cast_ctr, $this->current_data_byad ), 'by_group' => array_map( $cast_ctr, $this->current_data_bygroup ), 'by_campaign' => array_map( $cast_ctr, $this->current_data_bycampaign ), 'by_advertiser' => array_map( $cast_ctr, $this->current_data_byadvertiser ), ); /** * Filter the assembled report data. * * @param array $report Report rows keyed by report type. * @param AdminReports $reports This reports instance. */ return apply_filters( 'adcmdr_report_data', $report, $this ); } }