PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.1.6
Independent Analytics – WordPress Analytics Plugin v2.1.6
2.15.5 2.15.4 2.15.3 2.15.2 2.15.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
448 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($this->group->plural());
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', ['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-picker', ['buttons' => $this->groups()->buttons(), 'group' => $this->group()]);
73 }
74 public function get_table_markup(string $sort_column, string $sort_direction)
75 {
76 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(), 'row_count' => 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()]);
77 }
78 public function set_statistics(Statistics $statistics)
79 {
80 $this->statistics = $statistics;
81 }
82 public function get_row_markup($rows, $just_rows = \true, string $sort_column = 'visitors', string $sort_direction = 'desc')
83 {
84 return $this->get_rendered_template($rows, $just_rows, $sort_column, $sort_direction);
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 class="toolbar">
169 <div class="buttons primary-buttons">
170 <div class="modal-parent"
171 data-controller="dates"
172 data-dates-relative-range-id-value="<?php
173 \esc_html_e($options->relative_range_id());
174 ?>"
175 data-dates-exact-start-value="<?php
176 \esc_html_e($options->get_date_range()->start()->setTimezone(Timezone::site_timezone())->format('Y-m-d'));
177 ?>"
178 data-dates-exact-end-value="<?php
179 \esc_html_e($options->get_date_range()->end()->setTimezone(Timezone::site_timezone())->format('Y-m-d'));
180 ?>"
181 data-dates-first-day-of-week-value="<?php
182 echo \absint(\IAWP_SCOPED\iawp()->get_option('iawp_dow', 0));
183 ?>"
184 data-dates-css-url-value="<?php
185 echo \esc_url(\IAWP_SCOPED\iawp_url_to('dist/styles/easepick/datepicker.css'));
186 ?>"
187 data-dates-format-value="<?php
188 echo \esc_attr(WordPress_Site_Date_Format_Pattern::for_javascript());
189 ?>"
190 >
191 <button id="dates-button"
192 data-testid="open-calendar"
193 class="iawp-button ghost-white toolbar-button"
194 data-action="dates#toggleModal"
195 data-dates-target="modalButton"
196 >
197 <span class="dashicons dashicons-calendar-alt"></span>
198 <span class="iawp-label"><?php
199 \esc_html_e($options->get_date_range()->label());
200 ?></span>
201 </button>
202 <div id="modal-dates"
203 class="modal large dates"
204 data-dates-target="modal"
205 >
206 <div class="modal-inner">
207 <div id="easepick-picker"
208 data-dates-target="easepick"
209 style="display: none;"
210 >
211 </div>
212 <div class="relative-dates">
213 <?php
214 foreach (Relative_Date_Range::ranges() as $date_range) {
215 ?>
216 <button class="iawp-button ghost-purple"
217 data-dates-target="relativeRange"
218 data-action="dates#relativeRangeSelected"
219 data-relative-range-id="<?php
220 \esc_html_e($date_range->relative_range_id());
221 ?>"
222 data-relative-range-label="<?php
223 \esc_html_e($date_range->label());
224 ?>"
225 data-relative-range-start="<?php
226 \esc_html_e($date_range->start()->setTimezone(Timezone::site_timezone())->format('Y-m-d'));
227 ?>"
228 data-relative-range-end="<?php
229 \esc_html_e($date_range->end()->setTimezone(Timezone::site_timezone())->format('Y-m-d'));
230 ?>"
231 >
232 <?php
233 \esc_html_e($date_range->label());
234 ?>
235 </button>
236 <?php
237 }
238 ?>
239 </div>
240 <div>
241 <hr/>
242 </div>
243 <div>
244 <button class="iawp-button purple"
245 data-dates-target="apply"
246 data-action="dates#apply"
247 data-testid="apply-dates"
248 >
249 <?php
250 \esc_html_e('Apply', 'independent-analytics');
251 ?>
252 </button>
253 <button class="iawp-button ghost-purple"
254 data-action="dates#closeModal"
255 data-testid="close-calendar"
256 >
257 <?php
258 \esc_html_e('Cancel', 'independent-analytics');
259 ?>
260 </button>
261 </div>
262 </div>
263 </div>
264 </div>
265 <?php
266 echo $this->group_picker_html();
267 echo $this->filters()->get_filters_html($this->get_columns());
268 echo $this->column_picker_html();
269 ?>
270 </div>
271 <div class="buttons secondary-buttons">
272 <button class="iawp-button ghost-white toolbar-button" data-report-target="export" data-action="report#export">
273 <span class="dashicons dashicons-download"></span>
274 <span class="iawp-label">
275 <?php
276 \esc_html_e('Download CSV', 'independent-analytics');
277 ?>
278 </span>
279 </button>
280 </div>
281 </div><?php
282 }
283 public function filters_template_html() : string
284 {
285 return $this->filters()->get_condition_html($this->get_columns());
286 }
287 public final function csv(array $rows, bool $is_dashboard_export = \false) : CSV
288 {
289 $columns = $this->get_columns();
290 $csv_header = [];
291 $csv_rows = [];
292 foreach ($columns as $column) {
293 if (!$this->include_column_in_csv($column, $is_dashboard_export)) {
294 continue;
295 }
296 $csv_header[] = $column->label();
297 }
298 foreach ($rows as $row) {
299 $csv_row = [];
300 foreach ($columns as $column) {
301 if (!$this->include_column_in_csv($column, $is_dashboard_export)) {
302 continue;
303 }
304 $column_id = $column->id();
305 $value = $row->{$column_id}();
306 // Todo - This logic is similar to the rendering logic for table cells. This should
307 // all be handled via the column class itself.
308 if (\is_null($value)) {
309 $csv_row[] = '-';
310 } elseif ($column_id === 'date') {
311 $csv_row[] = \date(WordPress_Site_Date_Format_Pattern::for_php(), \strtotime($value));
312 } elseif ($column_id === 'average_session_duration' || $column_id === 'average_view_duration') {
313 $csv_row[] = Number_Formatter::second_to_minute_timestamp($value);
314 } elseif ($column_id === 'views_per_session') {
315 $csv_row[] = Number_Formatter::decimal($value, 2);
316 } else {
317 $csv_row[] = $row->{$column_id}();
318 }
319 }
320 $csv_rows[] = $csv_row;
321 }
322 $csv = new CSV($csv_header, $csv_rows);
323 return $csv;
324 }
325 public function sanitize_filters(array $filters) : array
326 {
327 return \array_values(\array_filter(\array_map(function ($filter) {
328 return $this->sanitize_filter($filter);
329 }, $filters)));
330 }
331 public function sanitize_filter(array $filter) : ?array
332 {
333 $column = $this->get_column($filter['column']);
334 if (\is_null($column)) {
335 return null;
336 }
337 $valid_inclusions = ['include', 'exclude'];
338 if (!\in_array($filter['inclusion'], $valid_inclusions)) {
339 return null;
340 }
341 if (!$column->is_valid_filter_operator($filter['operator'])) {
342 return null;
343 }
344 if (!$column->is_enabled_for_group($this->group())) {
345 return null;
346 }
347 $operand = \trim(Security::string($filter['operand']));
348 if (\strlen($operand) === 0) {
349 return null;
350 }
351 if ($column->database_column() === 'cached_url' && $filter['operator'] === 'exact') {
352 $url = new URL($filter['operand']);
353 if (!$url->is_valid_url()) {
354 $filter['operand'] = \site_url($filter['operand']);
355 }
356 }
357 if ($column->database_column() === 'cached_date') {
358 try {
359 $date = \DateTime::createFromFormat('U', $filter['operand']);
360 $filter['operand'] = $date->format('Y-m-d');
361 } catch (\Throwable $e) {
362 return null;
363 }
364 }
365 return ['inclusion' => Security::string($filter['inclusion']), 'column' => $column->id(), 'operator' => Security::string($filter['operator']), 'operand' => Security::string($filter['operand']), 'database_column' => $column->database_column()];
366 }
367 public function get_column(string $id) : ?Column
368 {
369 $matches = \array_filter($this->local_columns(), function (Column $column) use($id) {
370 return $column->id() === $id;
371 });
372 return \count($matches) === 1 ? \reset($matches) : null;
373 }
374 public function sanitize_sort_parameters(?string $sort_column, ?string $sort_direction) : Sort_Configuration
375 {
376 $column = $this->get_column($sort_column);
377 if (\is_null($column)) {
378 return new Sort_Configuration();
379 }
380 return new Sort_Configuration($sort_column, $sort_direction, $column->is_nullable());
381 }
382 private function include_column_in_csv(Column $column, bool $is_dashboard_export) : bool
383 {
384 if (!$column->visible() && $is_dashboard_export) {
385 return \false;
386 }
387 if (!$column->exportable() && !$is_dashboard_export) {
388 return \false;
389 }
390 if ($column->requires_woocommerce() && \IAWP_SCOPED\iawp_is_free()) {
391 return \false;
392 }
393 return \true;
394 }
395 /**
396 * @return Column[]
397 */
398 private function get_columns() : array
399 {
400 $columns_for_group = \array_filter($this->local_columns(), function (Column $column) {
401 return $column->is_enabled_for_group($this->group);
402 });
403 if (\is_null($this->visible_columns) || \count($this->visible_columns) === 0) {
404 return $columns_for_group;
405 }
406 if (!$this->is_new_group) {
407 return \array_map(function ($column) {
408 $column->set_visibility(\in_array($column->id(), $this->visible_columns));
409 return $column;
410 }, $columns_for_group);
411 }
412 return \array_map(function ($column) {
413 if ($column->is_group_dependent()) {
414 $column->set_visibility(\true);
415 } elseif (!\in_array($column->id(), $this->visible_columns)) {
416 $column->set_visibility(\false);
417 }
418 return $column;
419 }, $columns_for_group);
420 }
421 /**
422 * Get the number of visible columns
423 *
424 * @return int
425 */
426 private function visible_column_count() : int
427 {
428 $visible_columns = 0;
429 foreach ($this->get_columns() as $column) {
430 if ($column->visible()) {
431 $visible_columns++;
432 }
433 }
434 return $visible_columns;
435 }
436 private function get_rendered_template($rows, $just_rows = \false, string $sort_column = 'visitors', string $sort_direction = 'desc')
437 {
438 if ($just_rows) {
439 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(), 'row_count' => \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()]);
440 }
441 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(), 'row_count' => \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()]);
442 }
443 private function filters()
444 {
445 return $this->filters;
446 }
447 }
448