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