WordPress.org

Making WordPress.org

Ticket #4054: 4054.diff

File 4054.diff, 19.8 KB (added by iandunn, 14 months ago)

Rough draft

  • wordcamp-reports/classes/report/class-base-details.php

    diff --git wordcamp-reports/classes/report/class-base-details.php wordcamp-reports/classes/report/class-base-details.php
    index 1bc1e1c..643de4d 100644
     
    66 */
    77
    88namespace WordCamp\Reports\Report;
    9 defined( 'WPINC' ) || die();
    10 use Exception;
     9use DateTime, Exception;
     10use WP_Error;
    1111use WordCamp\Reports\Utility\Date_Range;
    1212use function WordCamp\Reports\{get_assets_url, get_assets_dir_path, get_views_dir_path};
    1313use function WordCamp\Reports\Validation\{validate_date_range};
    1414use WordCamp\Utilities\Export_CSV;
    1515
     16defined( 'WPINC' ) || die();
     17
    1618/**
    1719 * Class Base_Details
    1820 * Base class of details report type
    abstract class Base_Details extends Base { 
    360362
    361363                $exporter->emit_file();
    362364        }
     365
     366        /**
     367         * Parse input params for public report.
     368         *
     369         * @return array
     370         */
     371        public static function parse_public_report_input() {
     372                $action     = filter_input( INPUT_GET, 'action' );
     373                $start_date = filter_input( INPUT_GET, 'start-date' );
     374                $end_date   = filter_input( INPUT_GET, 'end-date' );
     375
     376                $valid_input = array(
     377                        'range'   => null,
     378                        'error'   => null,
     379                        'options' => array(),
     380                );
     381
     382                if ( $start_date && $end_date ) {
     383                        try {
     384                                $valid_input['range'] = validate_date_range( $start_date, $end_date );
     385                        } catch ( Exception $exception ) {
     386                                $valid_input['error'] = array(
     387                                        'error' => new WP_Error( 'date-range-error', $exception->getMessage() ),
     388                                );
     389                        }
     390
     391                        $valid_input['options'] = array(
     392                                'earliest_start' => new DateTime( '2006-01-01' ), // No WordCamp posts before 2006.
     393                                // this isn't working on front-end, but i don't think it's meant to
     394
     395                                // should pass public explicitly here?
     396
     397                                // should this be set at all? if so, should it be set from the child class? b/c this is used for meetups too.
     398                                // maybe make note that no meetups before 2006 either?
     399                        );
     400                }
     401
     402                return $valid_input;
     403        }
    363404}
     405 No newline at end of file
  • wordcamp-reports/classes/report/class-base.php

    diff --git wordcamp-reports/classes/report/class-base.php wordcamp-reports/classes/report/class-base.php
    index 4386a76..82d8afd 100644
    use WP_Error, WP_Post, WP_REST_Response; 
    1414 * A base report class with methods for filtering and caching data, and handling errors, plus some other helper methods.
    1515 *
    1616 * Things to keep in mind when writing a new report
    17  *  - Currently all reports extend `Date_Range` range than extending this directly. If your new report isn't based
     17 *  - Currently all reports extend `Date_Range` rather than extending this directly. If your new report isn't based
    1818 *    on pulling data between a date range, then you might want to write a new abstract class that's similar to
    1919 *    `Date_Range`, or just extend this directly, depending on the likelihood of that new abstract class being
    2020 *    reused in the future.
  • wordcamp-reports/classes/report/class-wordcamp-details.php

    diff --git wordcamp-reports/classes/report/class-wordcamp-details.php wordcamp-reports/classes/report/class-wordcamp-details.php
    index 4c4648f..64f6feb 100644
     
    33 * @package WordCamp\Reports
    44 */
    55
     6// todo run phpcs and eslint in next commit
     7
    68namespace WordCamp\Reports\Report;
    79defined( 'WPINC' ) || die();
    810
    use WordCamp_Admin, WordCamp_Loader; 
    2123 *
    2224 * Note that this report does not use caching because it is only used in WP Admin and has a large number of
    2325 * optional parameters.
     26 * todo add caching, update ^
     27 *   make sure don't expose private fields via caching, but should be find if use get_cache_key(), since it includes $options['public'] in key
     28 *  maybe don't add caching b/c unlikely to get cache-hit on the date ranges, or maybe just leave it as a todo
    2429 *
    2530 * @package WordCamp\Reports\Report
    2631 */
    class WordCamp_Details extends Base_Details { 
    6671         */
    6772        public static $group = 'wordcamp';
    6873
     74        /**
     75         * Shortcode tag for outputting the public report form.
     76         *
     77         * @var string
     78         */
     79        public static $shortcode_tag = 'wordcamp_details_report';
     80
    6981        /**
    7082         * Whether to include counts of various post types for each WordCamp.
    7183         *
    class WordCamp_Details extends Base_Details { 
    109121                return array_merge(
    110122                        [
    111123                                'Name',
     124                                //todo? or included below?
    112125                        ],
    113126                        WordCamp_Loader::get_public_meta_keys()
    114127                );
    class WordCamp_Details extends Base_Details { 
    286299                        require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-admin.php' );
    287300                }
    288301
     302                // todo if phpcs doesn't already catch defining arrays inside params, add a note to catch that. it should though b/c that's what core does now, unless it was just talked about but not impelemented
     303                        // now that manually changes this line, need to find another example in this plugin to look for
    289304                $meta_keys = array_merge(
    290305                        array_keys( WordCamp_Admin::meta_keys( 'all' ) ),
    291306                        WordCamp_Admin::get_venue_address_meta_keys()
    class WordCamp_Details extends Base_Details { 
    381396                return $counts;
    382397        }
    383398
     399        /**
     400         * Determine whether to render the public report form.
     401         *
     402         * This shortcode is limited to use on pages.
     403         *
     404         * @return string HTML content to display shortcode.
     405         */
     406        public static function handle_shortcode() {
     407                if ( 'page' !== get_post_type() ) {
     408                        return '';
     409                }
     410
     411                ob_start();
     412                self::render_public_page();
     413                return ob_get_clean();
     414
     415                // add note that showing all public fields for simplicity, rather than letting pick individual ones like wp-admin can
     416                // make sure that only showing PUBLIC fields, though
     417        }
     418
     419        /**
     420         * Render the page for this report on the front end.
     421         *
     422         * @return void
     423         */
     424        public static function render_public_page() {
     425                $report     = null;
     426                $start_date = '';
     427                $end_date   = '';
     428                $params     = self::parse_public_report_input();
     429
     430                if ( ! empty( $params['range'] ) ) {
     431                        $start_date = $params['range']->start->format( 'Y-m-d' );
     432                        $end_date   = $params['range']->end->format( 'Y-m-d' );
     433                        $report     = new self( $params['range'], null, false, $params['options'] );
     434                        // need to do this part in other function like admin page does?
     435                                // what other function?
     436
     437                        // add params for public (just to be explicit), or any other params?
     438                        // maybe earliest_date should be in here instead of parse_inptu()?
     439                }
     440
     441                include get_views_dir_path() . 'public/wordcamp-details.php';
     442
     443                // css to size/position export button
     444        }
     445
    384446        /**
    385447         * Render the page for this report in the WP Admin.
    386448         *
    class WordCamp_Details extends Base_Details { 
    412474                $nonce            = filter_input( INPUT_POST, self::$slug . '-nonce' );
    413475
    414476                $report = null;
     477                $options = array();
    415478
     479return;
     480wp_die('x');
    416481                if ( 'Export CSV' !== $action ) {
    417482                        return;
    418483                }
    419484
    420                 if ( ! wp_verify_nonce( $nonce, 'run-report' ) ) {
    421                         return;
    422                 }
     485                if ( is_admin() ) {
     486                        if ( ! wp_verify_nonce( $nonce, 'run-report' ) ) {
     487                                return;
     488                        }
    423489
    424                 if ( ! current_user_can( 'manage_network' ) ) {
    425                         return;
     490                        if ( ! current_user_can( 'manage_network' ) ) {
     491                                return;
     492                        }
     493
     494                        $options['public'] = false;
     495                } else {
     496                        $options['public'] = true;
     497
     498                        // where does $fields get set? i guess i need to make a hidden input field for that
     499                        // but make sure it's validated server side b/c attacker could just override
    426500                }
    427501
     502                // need to make sure any changes above don't allow attacker to get a private report via the front end
     503                        // maybe pass in a param to
     504
    428505                $error = null;
    429506                $range = null;
    430507
    class WordCamp_Details extends Base_Details { 
    450527                // The "Name" field should always be included, but does not get submitted because the input is disabled,
    451528                // so add it in here.
    452529                $fields[] = 'Name';
     530                // need to add on front end?
    453531
    454                 $options = array(
    455                         'fields' => $fields,
    456                         'public' => false,
    457                 );
    458 
     532                $options['fields'] = $fields;    // hardcode these to the public function?
    459533
    460534                $report = new self( $range, null, $include_counts, $options );
    461535
     536                var_dump($report);wp_die();
     537
    462538                self::export_to_file_common( $report );
    463539        }
    464540
     541        // ask corey for feedback on this changeset
    465542}
  • new file wordcamp-reports/classes/report/class-wordcamp-sessions.php

    diff --git wordcamp-reports/classes/report/class-wordcamp-sessions.php wordcamp-reports/classes/report/class-wordcamp-sessions.php
    new file mode 100644
    index 0000000..9b088e2
    - +  
     1<?php
     2/**
     3 * WordCamp Payment Methods.
     4 *
     5 * @package WordCamp\Reports
     6 */
     7
     8namespace WordCamp\Reports\Report;
     9defined( 'WPINC' ) || die();
     10
     11use Exception;
     12use WordCamp\Reports;
     13use WordCamp\Utilities;
     14use function WordCamp\Reports\Validation\{validate_wordcamp_id};
     15
     16/**
     17 * Class WordCamp_Payment_Methods
     18 *
     19 * @package WordCamp\Reports\Report
     20 */
     21class WordCamp_Payment_Methods extends Date_Range {
     22        /**
     23         * Report name.
     24         *
     25         * @var string
     26         */
     27        public static $name = 'WordCamp Payment Methods';
     28
     29        /**
     30         * Report slug.
     31         *
     32         * @var string
     33         */
     34        public static $slug = 'wordcamp-payment-methods';
     35
     36        /**
     37         * Report description.
     38         *
     39         * @var string
     40         */
     41        public static $description = 'WordCamp ticket sales broken out by payment method.';
     42
     43        /**
     44         * Report methodology.
     45         *
     46         * @var string
     47         */
     48        public static $methodology = "
     49                <ol>
     50                        <li>Generate a transaction data set from the Ticket Revenue report, using the specified date range.</li>
     51                        <li>Strip out refund transactions, since we're only interested in the initial purchase.</li>
     52                        <li>Count the number of transactions for each payment method.</li>
     53                        <li>Count the number of transactions for each payment method, for each WordCamp that had transactions during the specified date range.</li>
     54                </ol>
     55        ";
     56
     57        /**
     58         * Report group.
     59         *
     60         * @var string
     61         */
     62        public static $group = 'misc';
     63
     64        /**
     65         * WordCamp post ID.
     66         *
     67         * @var int The ID of the WordCamp post for this report.
     68         */
     69        public $wordcamp_id = 0;
     70
     71        /**
     72         * WordCamp site ID.
     73         *
     74         * @var int The ID of the WordCamp site where the invoices are located.
     75         */
     76        public $wordcamp_site_id = 0;
     77
     78        /**
     79         * Currency exchange rate client.
     80         *
     81         * @var Utilities\Currency_XRT_Client Utility to handle currency conversion.
     82         */
     83        protected $xrt = null;
     84
     85        /**
     86         * Data fields that can be visible in a public context.
     87         *
     88         * @var array An associative array of key/default value pairs.
     89         */
     90        protected $public_data_fields = array(
     91                'timestamp'        => '',
     92                'blog_id'          => 0,
     93                'object_id'        => 0,
     94                'method'           => '',
     95                'currency'         => '',
     96                'full_price'       => 0,
     97                'discounted_price' => 0,
     98        );
     99
     100        /**
     101         * Ticket_Payment_Methods constructor.
     102         *
     103         * @param string $start_date  The start of the date range for the report.
     104         * @param string $end_date    The end of the date range for the report.
     105         * @param int    $wordcamp_id Optional. The ID of a WordCamp post to limit this report to.
     106         * @param array  $options     {
     107         *     Optional. Additional report parameters.
     108         *     See Base::__construct and Date_Range::__construct for additional parameters.
     109         * }
     110         */
     111        public function __construct( $start_date, $end_date, $wordcamp_id = 0, array $options = array() ) {
     112                parent::__construct( $start_date, $end_date, $options );
     113
     114                $this->xrt = new Utilities\Currency_XRT_Client();
     115
     116                if ( $wordcamp_id ) {
     117                        try {
     118                                $valid = validate_wordcamp_id( $wordcamp_id );
     119
     120                                $this->wordcamp_id      = $valid->post_id;
     121                                $this->wordcamp_site_id = $valid->site_id;
     122                        } catch( Exception $e ) {
     123                                $this->error->add(
     124                                        self::$slug . '-wordcamp-id-error',
     125                                        $e->getMessage()
     126                                );
     127                        }
     128                }
     129        }
     130
     131        /**
     132         * Generate a cache key.
     133         *
     134         * @return string
     135         */
     136        protected function get_cache_key() {
     137                $cache_key = parent::get_cache_key();
     138
     139                if ( $this->wordcamp_id ) {
     140                        $cache_key .= '_' . $this->wordcamp_id;
     141                }
     142
     143                return $cache_key;
     144        }
     145
     146        /**
     147         * Query and parse the data for the report.
     148         *
     149         * @return array
     150         */
     151        public function get_data() {
     152                // Bail if there are errors.
     153                if ( ! empty( $this->error->get_error_messages() ) ) {
     154                        return array();
     155                }
     156
     157                // Maybe use cached data.
     158                $data = $this->maybe_get_cached_data();
     159                if ( is_array( $data ) ) {
     160                        return $data;
     161                }
     162
     163                $transactions = $this->get_ticket_transactions();
     164
     165                // We're not interested in refunds, only the original purchases.
     166                $purchases = array_filter( $transactions, function( $transaction ) {
     167                        if ( 'Purchase' === $transaction['type'] ) {
     168                                return true;
     169                        }
     170
     171                        return false;
     172                } );
     173
     174                // Thus we don't need the `type` property. We need every method field to have a value, though.
     175                $data = array_map( function( $purchase ) {
     176                        unset( $purchase['type'] );
     177
     178                        if ( ! $purchase['method'] ) {
     179                                $purchase['method'] = 'none';
     180                        }
     181
     182                        return $purchase;
     183                }, $purchases );
     184
     185                $data = $this->filter_data_fields( $data );
     186                $this->maybe_cache_data( $data );
     187
     188                return $data;
     189        }
     190
     191        /**
     192         * Compile the report data into results.
     193         *
     194         * @param array $data The data to compile.
     195         *
     196         * @return array
     197         */
     198        public function compile_report_data( array $data ) {
     199                $totals = array_reduce( $data, function( $carry, $item ) {
     200                        $method = $item['method'];
     201
     202                        if ( ! isset( $carry['Total'] ) ) {
     203                                $carry['Total'] = 0;
     204                        }
     205
     206                        if ( ! isset( $carry[ $method ] ) ) {
     207                                $carry[ $method ] = 0;
     208                        }
     209
     210                        $carry[ $method ] ++;
     211                        $carry['Total'] ++;
     212
     213                        return $carry;
     214                } );
     215
     216                uksort( $totals, function( $a, $b ) {
     217                        if ( 'Total' === $a ) {
     218                                return 1;
     219                        }
     220
     221                        if ( 'Total' === $b ) {
     222                                return -1;
     223                        }
     224
     225                        return ( $a < $b ) ? -1 : 1;
     226                } );
     227
     228                $by_site = array_reduce( $data, function( $carry, $item ) use ( $totals ) {
     229                        $blog_id = $item['blog_id'];
     230                        $method  = $item['method'];
     231
     232                        if ( ! isset( $carry[ $blog_id ] ) ) {
     233                                $carry[ $blog_id ] = array_merge( array_fill_keys( array_keys( $totals ), 0 ), array(
     234                                        'name' => get_wordcamp_name( $blog_id ),
     235                                ) );
     236                        }
     237
     238                        $carry[ $blog_id ][ $method ] ++;
     239                        $carry[ $blog_id ]['Total'] ++;
     240
     241                        return $carry;
     242                } );
     243
     244                usort( $by_site, function( $a, $b ) {
     245                        if ( $a['Total'] === $b['Total'] ) {
     246                                return 0;
     247                        }
     248
     249                        return ( $a['Total'] < $b['Total'] ) ? 1 : -1;
     250                } );
     251
     252                return array(
     253                        'method_totals'   => $totals,
     254                        'methods_by_site' => $by_site,
     255                );
     256        }
     257
     258        /**
     259         * Get a list of ticket transaction events from the given date range.
     260         *
     261         * Uses the Ticket Revenue report to get the relevant transaction event data.
     262         *
     263         * @return array
     264         */
     265        protected function get_ticket_transactions() {
     266                $ticket_revenue_report = new Ticket_Revenue(
     267                        $this->start_date->format( 'Y-m-d' ),
     268                        $this->end_date->format( 'Y-m-d' ),
     269                        $this->wordcamp_id,
     270                        $this->options
     271                );
     272
     273                return $ticket_revenue_report->get_data();
     274        }
     275
     276        /**
     277         * Render an HTML version of the report output.
     278         *
     279         * @return void
     280         */
     281        public function render_html() {
     282                $data = $this->compile_report_data( $this->get_data() );
     283
     284                $start_date    = $this->start_date;
     285                $end_date      = $this->end_date;
     286                $wordcamp_name = ( $this->wordcamp_site_id ) ? get_wordcamp_name( $this->wordcamp_site_id ) : '';
     287                $method_totals = $data['method_totals'];
     288                $site_totals   = $data['methods_by_site'];
     289
     290                if ( ! empty( $this->error->get_error_messages() ) ) {
     291                        $this->render_error_html();
     292                } else {
     293                        include Reports\get_views_dir_path() . 'html/wordcamp-payment-methods.php';
     294                }
     295        }
     296
     297        /**
     298         * Render the page for this report in the WP Admin.
     299         *
     300         * @return void
     301         */
     302        public static function render_admin_page() {
     303                $start_date  = filter_input( INPUT_POST, 'start-date' );
     304                $end_date    = filter_input( INPUT_POST, 'end-date' );
     305                $wordcamp_id = filter_input( INPUT_POST, 'wordcamp-id' );
     306                $refresh     = filter_input( INPUT_POST, 'refresh', FILTER_VALIDATE_BOOLEAN );
     307                $action      = filter_input( INPUT_POST, 'action' );
     308                $nonce       = filter_input( INPUT_POST, self::$slug . '-nonce' );
     309
     310                $report = null;
     311
     312                if ( 'Show results' === $action
     313                     && wp_verify_nonce( $nonce, 'run-report' )
     314                     && current_user_can( 'manage_network' )
     315                ) {
     316                        $options = array(
     317                                'earliest_start' => new \DateTime( '2015-01-01' ), // No indexed CampTix events before 2015.
     318                                'max_interval'   => new \DateInterval( 'P1Y' ), // 1 year. See http://php.net/manual/en/dateinterval.construct.php.
     319                        );
     320
     321                        if ( $refresh ) {
     322                                $options['flush_cache'] = true;
     323                        }
     324
     325                        $report = new self( $start_date, $end_date, $wordcamp_id, $options );
     326
     327                        // The report adjusts the end date in some circumstances.
     328                        if ( empty( $report->error->get_error_messages() ) ) {
     329                                $end_date = $report->end_date->format( 'Y-m-d' );
     330                        }
     331                }
     332
     333                include Reports\get_views_dir_path() . 'report/wordcamp-payment-methods.php';
     334        }
     335}
  • wordcamp-reports/classes/report/class-wordcamp-status.php

    diff --git wordcamp-reports/classes/report/class-wordcamp-status.php wordcamp-reports/classes/report/class-wordcamp-status.php
    index 45bce1a..249f06a 100644
    class WordCamp_Status extends Base_Status { 
    424424                $months   = month_array();
    425425                $statuses = WordCamp_Loader::get_post_statuses();
    426426
    427                 $error = $params['error'];
     427                $error = $params['error'];  // todo never gets used?
    428428                $report = null;
    429429                $period = $params['period'];
    430430                $year = $params['year'];
  • wordcamp-reports/index.php

    diff --git wordcamp-reports/index.php wordcamp-reports/index.php
    index 86ca132..3fec081 100644
     
    1313namespace WordCamp\Reports;
    1414defined( 'WPINC' ) || die();
    1515
     16use WP_Post;
    1617use WordCamp\Reports\Report;
    1718
    1819define( __NAMESPACE__ . '\PLUGIN_DIR', \plugin_dir_path( __FILE__ ) );
    function register_file_exports() { 
    341342        foreach ( $report_classes as $class ) {
    342343                if ( method_exists( $class, 'export_to_file' ) ) {
    343344                        add_action( 'admin_init', array( $class, 'export_to_file' ) );
     345
     346//                      if ( false !== stripos( $class, 'details' ) ) {
     347//                              var_dump($class, $post );
     348//                              wp_die();
     349//                      }
     350
     351                        if ( method_exists( $class, 'handle_shortcode' ) ) {
     352                                add_action( 'wp', array( $class, 'export_to_file' ) );
     353
     354                                // also make sure that only public reports
     355                                        // maybe hardcode it to just details, since that's the only one that needs this?
     356                                        // not sure, but make sure attacker can't leverage this to access private reports
     357                        }
    344358                }
    345359        }
    346360}
  • new file wordcamp-reports/views/public/wordcamp-details.php

    diff --git wordcamp-reports/views/public/wordcamp-details.php wordcamp-reports/views/public/wordcamp-details.php
    new file mode 100644
    index 0000000..6d9d0c3
    - +  
     1<?php
     2/**
     3 * @package WordCamp\Reports
     4 */
     5
     6namespace WordCamp\Reports\Views\Report\WordCamp_Details;
     7defined( 'WPINC' ) || die();
     8
     9use WordCamp\Reports\Report;
     10
     11/**
     12 * @var string $start_date
     13 * @var string $end_date
     14 */
     15
     16// need something to restrict years to > 2006 ?
     17
     18// test front end w/ date error, like end date more than 365 days from start date
     19
     20?>
     21
     22<div id="<?php echo esc_attr( Report\WordCamp_Details::$slug ); ?>-report" class="report-container">
     23        <p class="report-description">
     24                <?php echo wp_kses_post( Report\WordCamp_Details::$description ); ?>
     25        </p>
     26
     27        <form method="get" action="" class="report-form">
     28                <table class="form-table">
     29                        <tbody>
     30                                <tr>
     31                                        <th scope="row">
     32                                                <label for="start-date">
     33                                                        Start Date (optional)
     34                                                </label>
     35                                        </th>
     36
     37                                        <td>
     38                                                <input type="date" id="start-date" name="start-date" value="<?php echo esc_attr( $start_date ); ?>" />
     39                                        </td>
     40                                </tr>
     41
     42                                <tr>
     43                                        <th scope="row">
     44                                                <label for="end-date">
     45                                                        End Date (optional)
     46                                                </label>
     47                                        </th>
     48
     49                                        <td>
     50                                                <input type="date" id="end-date" name="end-date" value="<?php echo esc_attr( $end_date ); ?>" />
     51                                        </td>
     52                                </tr>
     53                        </tbody>
     54                </table>
     55
     56                <?php submit_button( 'Export CSV', 'primary', 'action', false ); ?>
     57        </form>
     58</div>