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