PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.3.1
Independent Analytics – WordPress Analytics Plugin v2.3.1
2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / IAWP / Tables / Table.php
independent-analytics / IAWP / Tables Last commit date
Columns 2 years ago Groups 2 years ago Table.php 2 years ago Table_Campaigns.php 2 years ago Table_Devices.php 2 years ago Table_Geo.php 2 years ago Table_Pages.php 2 years ago Table_Referrers.php 2 years ago
Table.php
496 lines
1 <?php
2
3 namespace IAWP\Tables;
4
5 use IAWP\Campaign_Builder;
6 use IAWP\Dashboard_Options;
7 use IAWP\Date_Range\Relative_Date_Range;
8 use IAWP\Filters;
9 use IAWP\Icon_Directory_Factory;
10 use IAWP\Rows\Filter;
11 use IAWP\Sort_Configuration;
12 use IAWP\Statistics\Statistics;
13 use IAWP\Tables\Columns\Column;
14 use IAWP\Tables\Groups\Group;
15 use IAWP\Tables\Groups\Groups;
16 use IAWP\Utils\CSV;
17 use IAWP\Utils\Currency;
18 use IAWP\Utils\Number_Formatter;
19 use IAWP\Utils\Security;
20 use IAWP\Utils\URL;
21 use IAWP\Utils\WordPress_Site_Date_Format_Pattern;
22 use IAWPSCOPED\Proper\Timezone;
23 /** @internal */
24 abstract class Table
25 {
26 private $filters;
27 private $visible_columns;
28 private $group;
29 private $is_new_group;
30 private $statistics;
31 /**
32 * @param string[]|null $visible_columns
33 * @param string|null $group_id
34 * @param bool $is_new_group
35 */
36 public function __construct(array $visible_columns = null, ?string $group_id = null, bool $is_new_group = \false)
37 {
38 $this->visible_columns = $visible_columns;
39 $this->group = $this->groups()->find_by_id($group_id);
40 $this->is_new_group = $is_new_group;
41 $this->filters = new Filters();
42 }
43 protected abstract function groups() : Groups;
44 /**
45 * @return array<Column>
46 */
47 protected abstract function local_columns() : array;
48 protected abstract function table_name() : string;
49 public function visible_column_ids()
50 {
51 $visible_columns = [];
52 foreach ($this->get_columns() as $column) {
53 if ($column->visible()) {
54 $visible_columns[] = $column->id();
55 }
56 }
57 return $visible_columns;
58 }
59 public function group() : Group
60 {
61 return $this->group;
62 }
63 public function column_picker_html() : string
64 {
65 return \IAWPSCOPED\iawp_blade()->run('tables.column-picker-modal', ['columns' => $this->get_columns()]);
66 }
67 public function group_picker_html() : ?string
68 {
69 $has_group_options = \count($this->groups()->buttons()) > 0;
70 if (!$has_group_options) {
71 return null;
72 }
73 return \IAWPSCOPED\iawp_blade()->run('tables.group-select', ['options' => $this->groups()->buttons(), 'group' => $this->group()]);
74 }
75 public function get_table_toolbar_markup()
76 {
77 return \IAWPSCOPED\iawp_blade()->run('tables.table-toolbar', ['all_columns' => $this->get_columns(), 'group_html' => $this->group_picker_html()]);
78 }
79 public function get_table_markup(string $sort_column, string $sort_direction)
80 {
81 return \IAWPSCOPED\iawp_blade()->run('tables.table', ['table' => $this, 'table_name' => $this->table_name(), 'all_columns' => $this->get_columns(), 'visible_column_count' => $this->visible_column_count(), 'number_of_shown_rows' => 0, 'rows' => [], 'render_skeleton' => \true, 'page_size' => \IAWPSCOPED\iawp()->pagination_page_size(), 'sort_column' => $sort_column, 'sort_direction' => $sort_direction, 'has_campaigns' => Campaign_Builder::has_campaigns()]);
82 }
83 public function set_statistics(Statistics $statistics)
84 {
85 $this->statistics = $statistics;
86 }
87 public function get_row_data_attributes($row)
88 {
89 $html = '';
90 foreach ($this->get_columns() as $column) {
91 $id = $column->id();
92 $data_val = $row->{$id}();
93 $html .= ' data-' . \esc_attr($column->id()) . '="' . \esc_attr($data_val) . '"';
94 }
95 return $html;
96 }
97 public function get_cell_content($row, Column $column)
98 {
99 $column_id = $column->id();
100 if (\is_null($row->{$column_id}())) {
101 return '-';
102 }
103 if ($column_id == 'title' && $row->is_deleted()) {
104 return Security::string($row->{$column_id}()) . ' <span class="deleted-label">' . \esc_html__('(deleted)', 'independent-analytics') . '</span>';
105 } elseif ($column_id == 'views') {
106 $views = Number_Formatter::decimal($row->views());
107 // Getting a divide by zero error from the line below?
108 // It's likely an issue with $this->views which is an instance of Views. Make sure the queries there are working.
109 $views_percentage = Number_Formatter::percent($row->views() / $this->statistics->views()->value() * 100, 2);
110 return '<span class="no-wrap">' . Security::string($views) . '</span> <span class="percentage">(' . Security::string($views_percentage) . ')</span>';
111 } elseif ($column_id == 'visitors') {
112 $visitors = Number_Formatter::decimal($row->visitors());
113 $visitors_percentage = Number_Formatter::percent($row->visitors() / $this->statistics->visitors()->value() * 100, 2);
114 return '<span class="no-wrap">' . Security::string($visitors) . '</span> <span class="percentage">(' . Security::string($visitors_percentage) . ')</span>';
115 } elseif ($column_id == 'sessions') {
116 $sessions = Number_Formatter::decimal($row->sessions());
117 $sessions_percentage = Number_Formatter::percent($row->sessions() / $this->statistics->sessions()->value() * 100, 2);
118 return '<span class="no-wrap">' . Security::string($sessions) . '</span> <span class="percentage">(' . Security::string($sessions_percentage) . ')</span>';
119 } elseif ($column_id === 'entrances') {
120 $entrances = Number_Formatter::decimal($row->entrances());
121 $entrances_percentage = Number_Formatter::percent($row->entrances() / $this->statistics->sessions()->value() * 100, 2);
122 return '<span class="no-wrap">' . Security::string($entrances) . '</span> <span class="percentage">(' . Security::string($entrances_percentage) . ')</span>';
123 } elseif ($column_id === 'exits') {
124 $exits = Number_Formatter::decimal($row->exits());
125 $exits_percentage = Number_Formatter::percent($row->exits() / $this->statistics->sessions()->value() * 100, 2);
126 return '<span class="no-wrap">' . Security::string($exits) . '</span> <span class="percentage">(' . Security::string($exits_percentage) . ')</span>';
127 } elseif ($column_id === 'bounce_rate') {
128 return Security::string(Number_Formatter::percent($row->bounce_rate()));
129 } elseif ($column_id === 'average_session_duration' || $column_id === 'average_view_duration') {
130 return Number_Formatter::second_to_minute_timestamp($row->{$column_id}());
131 } elseif ($column_id === 'views_growth' || $column_id === 'visitors_growth' || $column_id === 'wc_conversion_rate' || $column_id === 'exit_percent') {
132 return Number_Formatter::percent($row->{$column_id}(), 2);
133 } elseif ($column_id == 'url') {
134 if ($row->is_deleted()) {
135 return \urldecode(\esc_url($row->url()));
136 } else {
137 return '<a href="' . \esc_url($row->url(\true)) . '" target="_blank" class="external-link">' . \urldecode(\esc_url($row->url())) . '<span class="dashicons dashicons-external"></span></a>';
138 }
139 } elseif ($column_id == 'author') {
140 return Security::html($row->avatar()) . ' ' . Security::string($row->author());
141 } elseif ($column_id == 'date') {
142 return Security::string(\date(WordPress_Site_Date_Format_Pattern::for_php(), \strtotime($row->date())));
143 } elseif ($column_id == 'type' && \method_exists($row, 'icon') && \method_exists($row, 'type')) {
144 return $row->icon(0) . ' ' . Security::string($row->type());
145 } elseif ($column_id == 'referrer' && $row->has_link()) {
146 return '<a href="' . \esc_url($row->referrer_url()) . '" target="_blank" class="external-link">' . Security::string($row->referrer()) . '<span class="dashicons dashicons-external"></span></a>';
147 } elseif ($column_id === 'device_type') {
148 return Icon_Directory_Factory::device_types()->find($row->device_type()) . Security::string($row->device_type());
149 } elseif ($column_id === 'browser') {
150 return Icon_Directory_Factory::browsers()->find($row->browser()) . Security::string($row->browser());
151 } elseif ($column_id === 'os') {
152 return Icon_Directory_Factory::operating_systems()->find($row->os()) . Security::string($row->os());
153 } elseif ($column_id === 'country') {
154 return Icon_Directory_Factory::flags()->find($row->country_code()) . Security::string($row->country());
155 } elseif ($column_id === 'wc_gross_sales' || $column_id === 'wc_refunded_amount' || $column_id === 'wc_net_sales' || $column_id === 'wc_average_order_volume') {
156 return Security::string(Currency::format($row->{$column_id}()));
157 } elseif ($column_id === 'wc_earnings_per_visitor') {
158 return Security::string(Currency::format($row->{$column_id}(), \false));
159 } elseif ($column_id === 'views_per_session') {
160 return Number_Formatter::decimal($row->{$column_id}(), 2);
161 } else {
162 return Security::string($row->{$column_id}());
163 }
164 }
165 public function output_toolbar()
166 {
167 $options = new Dashboard_Options();
168 $exact_start = $options->get_date_range()->start()->setTimezone(Timezone::site_timezone())->format('Y-m-d');
169 $exact_end = $options->get_date_range()->end()->setTimezone(Timezone::site_timezone())->format('Y-m-d');
170 ?>
171 <div id="toolbar" class="toolbar" data-filter-count="<?php
172 echo \count($options->filters());
173 ?>">
174 <div class="date-picker-parent">
175 <div class="modal-parent"
176 data-controller="dates"
177 data-dates-relative-range-id-value="<?php
178 echo \esc_attr($options->relative_range_id());
179 ?>"
180 data-dates-exact-start-value="<?php
181 echo \esc_attr($exact_start);
182 ?>"
183 data-dates-exact-end-value="<?php
184 echo \esc_attr($exact_end);
185 ?>"
186 data-dates-first-day-of-week-value="<?php
187 echo \absint(\IAWPSCOPED\iawp()->get_option('iawp_dow', 0));
188 ?>"
189 data-dates-css-url-value="<?php
190 echo \esc_url(\IAWPSCOPED\iawp_url_to('dist/styles/easepick/datepicker.css'));
191 ?>"
192 data-dates-format-value="<?php
193 echo \esc_attr(WordPress_Site_Date_Format_Pattern::for_javascript());
194 ?>"
195 >
196 <button id="dates-button"
197 data-testid="open-calendar"
198 class="iawp-button"
199 data-action="dates#toggleModal"
200 data-dates-target="modalButton"
201 >
202 <span class="dashicons dashicons-calendar-alt"></span>
203 <span class="iawp-label"><?php
204 echo \esc_html($options->get_date_range()->label());
205 ?></span>
206 </button>
207 <div id="modal-dates"
208 class="modal large dates"
209 data-dates-target="modal"
210 >
211 <div class="modal-inner">
212 <div id="easepick-picker"
213 data-dates-target="easepick"
214 style="display: none;"
215 >
216 </div>
217 <div class="relative-dates">
218 <?php
219 foreach (Relative_Date_Range::ranges() as $date_range) {
220 ?>
221 <?php
222 $exact_start = $date_range->start()->setTimezone(Timezone::site_timezone())->format('Y-m-d');
223 ?>
224 <?php
225 $exact_end = $date_range->end()->setTimezone(Timezone::site_timezone())->format('Y-m-d');
226 ?>
227 <button class="iawp-button"
228 data-dates-target="relativeRange"
229 data-action="dates#relativeRangeSelected"
230 data-relative-range-id="<?php
231 echo \esc_attr($date_range->relative_range_id());
232 ?>"
233 data-relative-range-label="<?php
234 echo \esc_attr($date_range->label());
235 ?>"
236 data-relative-range-start="<?php
237 echo \esc_attr($exact_start);
238 ?>"
239 data-relative-range-end="<?php
240 echo \esc_attr($exact_end);
241 ?>"
242 >
243 <?php
244 echo \esc_html($date_range->label());
245 ?>
246 </button>
247 <?php
248 }
249 ?>
250 </div>
251 <div class="apply-buttons">
252 <button class="iawp-button purple"
253 data-dates-target="apply"
254 data-action="dates#apply"
255 data-testid="apply-dates"
256 >
257 <?php
258 \esc_html_e('Apply', 'independent-analytics');
259 ?>
260 </button>
261 <button class="iawp-button ghost-purple"
262 data-action="dates#closeModal"
263 data-testid="close-calendar"
264 >
265 <?php
266 \esc_html_e('Cancel', 'independent-analytics');
267 ?>
268 </button>
269 </div>
270 </div>
271 </div>
272 </div>
273 </div>
274 <div class="filter-parent">
275 <?php
276 echo $this->filters()->get_filters_html($this->get_columns());
277 ?>
278 </div>
279 <div class="download-options-parent" data-controller="modal">
280 <div class="modal-parent downloads">
281 <button id="download-options" data-modal-target="modalButton" data-action="click->modal#toggleModal" class="download-options">
282 <?php
283 \esc_html_e('Download Report', 'independent-analytics');
284 ?>
285 </button>
286 <div class="modal small downloads" data-modal-target="modal">
287 <div class="modal-inner">
288 <div class="title-small"><?php
289 \esc_html_e('Choose a format', 'independent-analytics');
290 ?></div>
291 <button id="download-csv" class="iawp-button" data-report-target="exportCSV" data-action="report#exportCSV">
292 <span class="dashicons dashicons-media-spreadsheet"></span>
293 <span class="iawp-label">
294 <?php
295 \esc_html_e('Download CSV', 'independent-analytics');
296 ?>
297 </span>
298 </button>
299 <button id="download-pdf" class="iawp-button" data-report-target="exportPDF" data-action="report#exportPDF" disabled="disabled">
300 <span class="dashicons dashicons-pdf"></span>
301 <span class="iawp-label">
302 <?php
303 \esc_html_e('Download PDF', 'independent-analytics');
304 ?>
305 </span>
306 </button>
307 </div>
308 </div>
309 </div>
310 </div>
311 </div><?php
312 }
313 public function filters_template_html() : string
314 {
315 return $this->filters()->get_condition_html($this->get_columns());
316 }
317 public function filters_condition_buttons_html(array $filters) : string
318 {
319 return $this->filters()->condition_buttons_html($filters);
320 }
321 public final function csv(array $rows, bool $is_dashboard_export = \false) : CSV
322 {
323 $columns = $this->get_columns();
324 $csv_header = [];
325 $csv_rows = [];
326 foreach ($columns as $column) {
327 if (!$this->include_column_in_csv($column, $is_dashboard_export)) {
328 continue;
329 }
330 $csv_header[] = $column->label();
331 }
332 foreach ($rows as $row) {
333 $csv_row = [];
334 foreach ($columns as $column) {
335 if (!$this->include_column_in_csv($column, $is_dashboard_export)) {
336 continue;
337 }
338 $column_id = $column->id();
339 $value = $row->{$column_id}();
340 // Todo - This logic is similar to the rendering logic for table cells. This should
341 // all be handled via the column class itself.
342 if (\is_null($value)) {
343 $csv_row[] = '-';
344 } elseif ($column_id === 'date') {
345 $csv_row[] = \date(WordPress_Site_Date_Format_Pattern::for_php(), \strtotime($value));
346 } elseif ($column_id === 'average_session_duration' || $column_id === 'average_view_duration') {
347 $csv_row[] = Number_Formatter::second_to_minute_timestamp($value);
348 } elseif ($column_id === 'views_per_session') {
349 $csv_row[] = Number_Formatter::decimal($value, 2);
350 } else {
351 $csv_row[] = $row->{$column_id}();
352 }
353 }
354 $csv_rows[] = $csv_row;
355 }
356 $csv = new CSV($csv_header, $csv_rows);
357 return $csv;
358 }
359 /**
360 * @param array[] $filters Raw filter associative arrays
361 *
362 * @return Filter[]
363 */
364 public function sanitize_filters(array $filters) : array
365 {
366 return \array_values(\array_filter(\array_map(function ($filter) {
367 return $this->sanitize_filter($filter);
368 }, $filters)));
369 }
370 public function sanitize_filter(array $filter) : ?Filter
371 {
372 $column = $this->get_column($filter['column']);
373 if (\is_null($column)) {
374 return null;
375 }
376 $valid_inclusions = ['include', 'exclude'];
377 if (!\in_array($filter['inclusion'], $valid_inclusions)) {
378 return null;
379 }
380 if (!$column->is_valid_filter_operator($filter['operator'])) {
381 return null;
382 }
383 if (!$column->is_enabled_for_group($this->group())) {
384 return null;
385 }
386 $operand = \trim(Security::string($filter['operand']));
387 if (\strlen($operand) === 0) {
388 return null;
389 }
390 if ($column->database_column() === 'cached_url' && $filter['operator'] === 'exact') {
391 $url = new URL($filter['operand']);
392 if (!$url->is_valid_url()) {
393 $filter['operand'] = \site_url($filter['operand']);
394 }
395 }
396 return new Filter(['inclusion' => Security::string($filter['inclusion']), 'column' => $column->id(), 'operator' => Security::string($filter['operator']), 'operand' => Security::string($filter['operand']), 'database_column' => $column->database_column()]);
397 }
398 public function get_column(string $id) : ?Column
399 {
400 $matches = \array_filter($this->local_columns(), function (Column $column) use($id) {
401 return $column->id() === $id;
402 });
403 return \count($matches) === 1 ? \reset($matches) : null;
404 }
405 public function sanitize_sort_parameters(?string $sort_column, ?string $sort_direction) : Sort_Configuration
406 {
407 $column = $this->get_column($sort_column);
408 if (\is_null($column)) {
409 return new Sort_Configuration();
410 }
411 return new Sort_Configuration($sort_column, $sort_direction, $column->is_nullable());
412 }
413 public function get_rendered_template($rows, $just_rows = \false, string $sort_column = 'visitors', string $sort_direction = 'desc')
414 {
415 if ($just_rows) {
416 return \IAWPSCOPED\iawp_blade()->run('tables.rows', ['table' => $this, 'table_name' => $this->table_name(), 'all_columns' => $this->get_columns(), 'visible_column_count' => $this->visible_column_count(), 'number_of_shown_rows' => \count($rows), 'rows' => $rows, 'render_skeleton' => \false, 'page_size' => \IAWPSCOPED\iawp()->pagination_page_size(), 'sort_column' => $sort_column, 'sort_direction' => $sort_direction, 'has_campaigns' => Campaign_Builder::has_campaigns()]);
417 }
418 return \IAWPSCOPED\iawp_blade()->run('tables.table', ['table' => $this, 'table_name' => $this->table_name(), 'all_columns' => $this->get_columns(), 'visible_column_count' => $this->visible_column_count(), 'number_of_shown_rows' => \count($rows), 'rows' => $rows, 'render_skeleton' => \false, 'page_size' => \IAWPSCOPED\iawp()->pagination_page_size(), 'sort_column' => $sort_column, 'sort_direction' => $sort_direction, 'has_campaigns' => Campaign_Builder::has_campaigns()]);
419 }
420 private function include_column_in_csv(Column $column, bool $is_dashboard_export) : bool
421 {
422 if (!$column->visible() && $is_dashboard_export) {
423 return \false;
424 }
425 if (!$column->exportable() && !$is_dashboard_export) {
426 return \false;
427 }
428 if ($column->requires_woocommerce() && \IAWPSCOPED\iawp_is_free()) {
429 return \false;
430 }
431 return \true;
432 }
433 /**
434 * @return Column[]
435 */
436 private function get_columns() : array
437 {
438 $columns_for_group = \array_filter($this->local_columns(), function (Column $column) {
439 return $column->is_enabled_for_group($this->group);
440 });
441 if (\is_null($this->visible_columns) || \count($this->visible_columns) === 0) {
442 return $columns_for_group;
443 }
444 if (!$this->is_new_group) {
445 return \array_map(function ($column) {
446 $column->set_visibility(\in_array($column->id(), $this->visible_columns));
447 return $column;
448 }, $columns_for_group);
449 }
450 return \array_map(function ($column) {
451 if ($column->is_group_dependent()) {
452 $column->set_visibility(\true);
453 } elseif (!\in_array($column->id(), $this->visible_columns)) {
454 $column->set_visibility(\false);
455 }
456 return $column;
457 }, $columns_for_group);
458 }
459 /**
460 * Get the number of visible columns
461 *
462 * @return int
463 */
464 private function visible_column_count() : int
465 {
466 $visible_columns = 0;
467 foreach ($this->get_columns() as $column) {
468 if ($column->visible()) {
469 $visible_columns++;
470 }
471 }
472 return $visible_columns;
473 }
474 private function filters()
475 {
476 return $this->filters;
477 }
478 public static function get_table_by_type(string $type) : ?string
479 {
480 switch ($type) {
481 case 'views':
482 return \IAWP\Tables\Table_Pages::class;
483 case 'referrers':
484 return \IAWP\Tables\Table_Referrers::class;
485 case 'geo':
486 return \IAWP\Tables\Table_Geo::class;
487 case 'campaigns':
488 return \IAWP\Tables\Table_Campaigns::class;
489 case 'devices':
490 return \IAWP\Tables\Table_Devices::class;
491 default:
492 return null;
493 }
494 }
495 }
496