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