Ticket #4054: 4054.diff
File 4054.diff, 19.8 KB (added by , 6 years ago) |
---|
-
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
6 6 */ 7 7 8 8 namespace WordCamp\Reports\Report; 9 defined( 'WPINC' ) || die();10 use Exception;9 use DateTime, Exception; 10 use WP_Error; 11 11 use WordCamp\Reports\Utility\Date_Range; 12 12 use function WordCamp\Reports\{get_assets_url, get_assets_dir_path, get_views_dir_path}; 13 13 use function WordCamp\Reports\Validation\{validate_date_range}; 14 14 use WordCamp\Utilities\Export_CSV; 15 15 16 defined( 'WPINC' ) || die(); 17 16 18 /** 17 19 * Class Base_Details 18 20 * Base class of details report type … … abstract class Base_Details extends Base { 360 362 361 363 $exporter->emit_file(); 362 364 } 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 } 363 404 } 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; 14 14 * A base report class with methods for filtering and caching data, and handling errors, plus some other helper methods. 15 15 * 16 16 * Things to keep in mind when writing a new report 17 * - Currently all reports extend `Date_Range` ra ngethan extending this directly. If your new report isn't based17 * - Currently all reports extend `Date_Range` rather than extending this directly. If your new report isn't based 18 18 * on pulling data between a date range, then you might want to write a new abstract class that's similar to 19 19 * `Date_Range`, or just extend this directly, depending on the likelihood of that new abstract class being 20 20 * 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
3 3 * @package WordCamp\Reports 4 4 */ 5 5 6 // todo run phpcs and eslint in next commit 7 6 8 namespace WordCamp\Reports\Report; 7 9 defined( 'WPINC' ) || die(); 8 10 … … use WordCamp_Admin, WordCamp_Loader; 21 23 * 22 24 * Note that this report does not use caching because it is only used in WP Admin and has a large number of 23 25 * 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 24 29 * 25 30 * @package WordCamp\Reports\Report 26 31 */ … … class WordCamp_Details extends Base_Details { 66 71 */ 67 72 public static $group = 'wordcamp'; 68 73 74 /** 75 * Shortcode tag for outputting the public report form. 76 * 77 * @var string 78 */ 79 public static $shortcode_tag = 'wordcamp_details_report'; 80 69 81 /** 70 82 * Whether to include counts of various post types for each WordCamp. 71 83 * … … class WordCamp_Details extends Base_Details { 109 121 return array_merge( 110 122 [ 111 123 'Name', 124 //todo? or included below? 112 125 ], 113 126 WordCamp_Loader::get_public_meta_keys() 114 127 ); … … class WordCamp_Details extends Base_Details { 286 299 require_once( WP_PLUGIN_DIR . '/wcpt/wcpt-wordcamp/wordcamp-admin.php' ); 287 300 } 288 301 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 289 304 $meta_keys = array_merge( 290 305 array_keys( WordCamp_Admin::meta_keys( 'all' ) ), 291 306 WordCamp_Admin::get_venue_address_meta_keys() … … class WordCamp_Details extends Base_Details { 381 396 return $counts; 382 397 } 383 398 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 384 446 /** 385 447 * Render the page for this report in the WP Admin. 386 448 * … … class WordCamp_Details extends Base_Details { 412 474 $nonce = filter_input( INPUT_POST, self::$slug . '-nonce' ); 413 475 414 476 $report = null; 477 $options = array(); 415 478 479 return; 480 wp_die('x'); 416 481 if ( 'Export CSV' !== $action ) { 417 482 return; 418 483 } 419 484 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 } 423 489 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 426 500 } 427 501 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 428 505 $error = null; 429 506 $range = null; 430 507 … … class WordCamp_Details extends Base_Details { 450 527 // The "Name" field should always be included, but does not get submitted because the input is disabled, 451 528 // so add it in here. 452 529 $fields[] = 'Name'; 530 // need to add on front end? 453 531 454 $options = array( 455 'fields' => $fields, 456 'public' => false, 457 ); 458 532 $options['fields'] = $fields; // hardcode these to the public function? 459 533 460 534 $report = new self( $range, null, $include_counts, $options ); 461 535 536 var_dump($report);wp_die(); 537 462 538 self::export_to_file_common( $report ); 463 539 } 464 540 541 // ask corey for feedback on this changeset 465 542 } -
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 8 namespace WordCamp\Reports\Report; 9 defined( 'WPINC' ) || die(); 10 11 use Exception; 12 use WordCamp\Reports; 13 use WordCamp\Utilities; 14 use function WordCamp\Reports\Validation\{validate_wordcamp_id}; 15 16 /** 17 * Class WordCamp_Payment_Methods 18 * 19 * @package WordCamp\Reports\Report 20 */ 21 class 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 { 424 424 $months = month_array(); 425 425 $statuses = WordCamp_Loader::get_post_statuses(); 426 426 427 $error = $params['error']; 427 $error = $params['error']; // todo never gets used? 428 428 $report = null; 429 429 $period = $params['period']; 430 430 $year = $params['year']; -
wordcamp-reports/index.php
diff --git wordcamp-reports/index.php wordcamp-reports/index.php index 86ca132..3fec081 100644
13 13 namespace WordCamp\Reports; 14 14 defined( 'WPINC' ) || die(); 15 15 16 use WP_Post; 16 17 use WordCamp\Reports\Report; 17 18 18 19 define( __NAMESPACE__ . '\PLUGIN_DIR', \plugin_dir_path( __FILE__ ) ); … … function register_file_exports() { 341 342 foreach ( $report_classes as $class ) { 342 343 if ( method_exists( $class, 'export_to_file' ) ) { 343 344 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 } 344 358 } 345 359 } 346 360 } -
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 6 namespace WordCamp\Reports\Views\Report\WordCamp_Details; 7 defined( 'WPINC' ) || die(); 8 9 use 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>