PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.12.2
Independent Analytics – WordPress Analytics Plugin v2.12.2
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 / Overview / Modules / Module.php
independent-analytics / IAWP / Overview / Modules Last commit date
Busiest_Day_Of_Week_Module.php 1 year ago Busiest_Time_Of_Day_Module.php 1 year ago Line_Chart_Module.php 1 year ago Map_Module.php 1 year ago Module.php 11 months ago New_Sessions_Module.php 1 year ago Pie_Chart_Module.php 1 year ago Quick_Stats_Module.php 1 year ago Recent_Conversions_Module.php 1 year ago Recent_Views_Module.php 1 year ago Top_Ten_Module.php 1 year ago
Module.php
427 lines
1 <?php
2
3 namespace IAWP\Overview\Modules;
4
5 use DateTime;
6 use IAWP\Date_Range\Relative_Date_Range;
7 use IAWP\Overview\Form_Field;
8 use IAWP\Overview\Form_Field_Option;
9 use IAWP\Overview\Sync_Module_Background_Job;
10 use IAWP\Overview\WP_Options_Storage;
11 use IAWP\Report;
12 use IAWP\Report_Finder;
13 use IAWP\Utils\Security;
14 use IAWP\Utils\Timezone;
15 use IAWPSCOPED\Illuminate\Support\Arr;
16 use IAWPSCOPED\Illuminate\Support\Collection;
17 /** @internal */
18 abstract class Module
19 {
20 protected $attributes;
21 private $report;
22 private $form_fields;
23 private $validation_errors = null;
24 private $options_storage;
25 public function __construct(?array $attributes = [])
26 {
27 $this->report = $this->get_report($attributes);
28 $this->form_fields = $this->create_form_fields();
29 $this->attributes = $this->prune_attributes($attributes);
30 $this->options_storage = new WP_Options_Storage('iawp_overview_modules');
31 }
32 public abstract function module_type() : string;
33 public abstract function module_name() : string;
34 protected abstract function module_fields() : array;
35 public abstract function calculate_dataset();
36 public function id() : ?string
37 {
38 return $this->attributes['id'] ?? null;
39 }
40 public function name() : ?string
41 {
42 return $this->attributes['name'] ?? null;
43 }
44 public function is_full_width() : bool
45 {
46 return $this->attributes['is_full_width'] ?? \false;
47 }
48 public function report() : ?Report
49 {
50 return $this->report;
51 }
52 public function save($skip_sync = \false) : bool
53 {
54 if (!$this->is_valid()) {
55 return \false;
56 }
57 $is_update = $this->options_storage->exists($this->id());
58 // Generate an id
59 if (!$is_update) {
60 $this->attributes['id'] = $this->options_storage->generate_id();
61 }
62 $this->options_storage->insert($this->attributes);
63 $this->report = $this->get_report($this->attributes);
64 if (!$skip_sync) {
65 static::queue_dataset_refresh([$this]);
66 }
67 return \true;
68 }
69 public function update(array $attributes)
70 {
71 $pruned_attributes = $this->prune_attributes($attributes, \true);
72 $this->attributes = \array_merge($this->attributes, $pruned_attributes);
73 $this->report = $this->get_report($attributes);
74 $this->form_fields = $this->create_form_fields();
75 }
76 public function delete() : bool
77 {
78 // Delete any cached datasets
79 \delete_option($this->option_name());
80 // Delete the module configuration
81 return $this->options_storage->delete($this->id());
82 }
83 public function is_valid() : bool
84 {
85 $validation_errors = [];
86 if (\is_string($this->attributes['name'] ?? null) && \strlen($this->attributes['name']) > 0) {
87 } else {
88 $validation_errors[] = ['name', 'Must be provided'];
89 }
90 // Check every form field
91 foreach ($this->form_fields as $form_field) {
92 $value = $this->attributes[$form_field->id()] ?? null;
93 if (!$form_field->is_a_supported_value($value)) {
94 $validation_errors[] = [$form_field->id(), "Invalid option provided"];
95 }
96 }
97 if (\count($validation_errors) > 0) {
98 $this->validation_errors = $validation_errors;
99 return \false;
100 } else {
101 return \true;
102 }
103 }
104 public function validation_errors() : ?array
105 {
106 return $this->validation_errors;
107 }
108 public function form_fields() : array
109 {
110 return $this->form_fields;
111 }
112 public function get_module_html() : string
113 {
114 $dataset = $this->get_dataset();
115 return \IAWPSCOPED\iawp_render("overview.modules.layout", ['module' => $this, 'dataset' => $dataset, 'is_loaded' => $dataset !== null, 'is_empty' => \is_array($dataset) && empty($dataset)]);
116 }
117 public function get_editor_html() : string
118 {
119 return \IAWPSCOPED\iawp_render('overview.module-editor', ['module' => $this]);
120 }
121 public function get_form_fields_html() : string
122 {
123 $html = '';
124 foreach ($this->form_fields as $form_field) {
125 $type = $form_field->type();
126 $html .= \IAWPSCOPED\iawp_render("overview.form-fields.{$type}", ['form_field' => $form_field, 'selected_value' => $this->attributes[$form_field->id()] ?? null]);
127 }
128 return $html;
129 }
130 public function get_report_details() : array
131 {
132 return Form_Field::get_report_details();
133 }
134 /**
135 * Has this module already been saved?
136 *
137 * @return bool
138 */
139 public function is_saved() : bool
140 {
141 return $this->id() !== null;
142 }
143 public function get_dataset() : ?array
144 {
145 $value = \get_option($this->option_name(), null);
146 return \is_array($value) ? $value : null;
147 }
148 public function has_dataset() : bool
149 {
150 return \is_array($this->get_dataset());
151 }
152 /**
153 * Subtitle to show under the module name
154 *
155 * @return string
156 */
157 public function subtitle() : string
158 {
159 // If the module has a range option, use the selected value
160 if ($this->has_dataset()) {
161 $relative_range = Relative_Date_Range::range_by_id($this->attributes['date_range'] ?? $this->attributes['busiest_date_range']);
162 if ($relative_range) {
163 return $relative_range->label();
164 }
165 }
166 $number_of_days = $this->get_dataset_number_of_days();
167 if (\is_int($number_of_days)) {
168 return \sprintf(\__("Last %s Days", 'independent-analytics'), $this->get_dataset_number_of_days());
169 }
170 return '';
171 }
172 public function refresh_dataset()
173 {
174 \delete_option($this->option_name());
175 \update_option($this->option_name(), $this->calculate_dataset(), \true);
176 }
177 protected function get_dataset_number_of_days() : ?int
178 {
179 return null;
180 }
181 protected function option_name() : string
182 {
183 return 'iawp_module_' . $this->id();
184 }
185 /**
186 * Get the name for the field option that was selcted for the given $field_id.
187 *
188 * @param string $field_id
189 *
190 * @return string|null
191 */
192 protected function get_field_option_name(string $field_id) : ?string
193 {
194 $selected_field_option_id = $this->attributes[$field_id] ?? null;
195 if ($selected_field_option_id === null) {
196 return null;
197 }
198 $options = $this->get_form_field($field_id)->supported_values();
199 $option = Collection::make($options)->first(function (Form_Field_Option $option) use($selected_field_option_id) {
200 return $option->id() === $selected_field_option_id;
201 });
202 if ($option === null) {
203 return null;
204 }
205 return $option->name();
206 }
207 /**
208 * @return ?Form_Field
209 */
210 protected function get_form_field(string $id) : ?Form_Field
211 {
212 return Collection::make($this->form_fields)->first(function (Form_Field $field) use($id) {
213 return $field->id() === $id;
214 });
215 }
216 /**
217 * Determine which report to associate with a module instance.
218 *
219 * @param array $attributes
220 *
221 * @return Report
222 */
223 private function get_report(array $attributes) : Report
224 {
225 $default_report = Report_Finder::new()->get_base_report_for_type('views');
226 $report_id = $attributes['report'] ?? $attributes['geo_report'] ?? null;
227 if ($report_id === null) {
228 return $default_report;
229 }
230 $report = Report_Finder::new()->fetch_report($report_id);
231 if ($report instanceof Report) {
232 return $report;
233 } else {
234 return $default_report;
235 }
236 }
237 /**
238 * Remove attributes that are not available for a given module
239 *
240 * @param array $attributes
241 *
242 * @return array
243 */
244 private function prune_attributes(array $attributes, $for_update = \false) : array
245 {
246 if ($for_update) {
247 $valid_attributes = Collection::make(['name', 'is_full_width']);
248 } else {
249 $valid_attributes = Collection::make(['id', 'module_type', 'name', 'is_full_width']);
250 }
251 foreach ($this->form_fields as $form_field) {
252 $valid_attributes->push($form_field->id());
253 }
254 $new_attributes = Arr::only($attributes, $valid_attributes->all());
255 // Convert is_full_width into a boolean if present
256 if (\array_key_exists('is_full_width', $new_attributes) && !\is_bool($new_attributes['is_full_width'])) {
257 $new_attributes['is_full_width'] = $new_attributes['is_full_width'] === 'true';
258 }
259 foreach ($new_attributes as $key => $value) {
260 if (\is_string($value)) {
261 $new_attributes[$key] = Security::string($value);
262 }
263 if (\is_array($value) && Arr::isList($value)) {
264 $new_attributes[$key] = Collection::make($value)->map(function ($value) {
265 if (\is_string($value)) {
266 return Security::string($value);
267 }
268 return $value;
269 })->all();
270 } elseif (\is_array($value) && Arr::isAssoc($value)) {
271 unset($new_attributes[$key]);
272 }
273 }
274 return $new_attributes;
275 }
276 /**
277 * @return Form_Field[]
278 */
279 private function create_form_fields() : array
280 {
281 return Collection::make($this->module_fields())->map(function (string $id) {
282 return new Form_Field($id, $this->report);
283 })->all();
284 }
285 /**
286 * Triggers a background job to refresh the module's dataset.
287 *
288 * @param Module[] $modules
289 *
290 * @return void
291 */
292 public static function queue_dataset_refresh(array $modules) : void
293 {
294 $ids = [];
295 foreach ($modules as $module) {
296 // Delete existing data
297 \delete_option($module->option_name());
298 // Add it to the list of modules to update
299 $ids[] = $module->id();
300 }
301 // Trigger a background job
302 $sync = new Sync_Module_Background_Job();
303 $sync->data(['ids' => $ids]);
304 $sync->dispatch();
305 }
306 public static function last_refreshed_at() : string
307 {
308 $timestamp = \get_option('iawp_modules_refreshed_at', null);
309 // The option could be a string or an int. Normalize it.
310 if (\is_int($timestamp)) {
311 $timestamp = \strval($timestamp);
312 }
313 if (!\ctype_digit($timestamp)) {
314 return '';
315 }
316 try {
317 $date = DateTime::createFromFormat('U', $timestamp);
318 $date->setTimezone(Timezone::site_timezone());
319 $format = \IAWPSCOPED\iawp()->get_option('time_format', 'g:i a');
320 return \sprintf(\_x('Last updated at %s.', 'The placeholder is a time of day', 'independent-analytics'), $date->format($format));
321 } catch (\Exception $e) {
322 return '';
323 }
324 }
325 public static function new(string $module_type, array $attributes) : ?self
326 {
327 switch ($module_type) {
328 case 'top-ten':
329 return new \IAWP\Overview\Modules\Top_Ten_Module($attributes);
330 case 'quick-stats':
331 return new \IAWP\Overview\Modules\Quick_Stats_Module($attributes);
332 case 'line-chart':
333 return new \IAWP\Overview\Modules\Line_Chart_Module($attributes);
334 case 'pie-chart':
335 return new \IAWP\Overview\Modules\Pie_Chart_Module($attributes);
336 case 'map':
337 return new \IAWP\Overview\Modules\Map_Module($attributes);
338 case 'recent-views':
339 return new \IAWP\Overview\Modules\Recent_Views_Module($attributes);
340 case 'recent-conversions':
341 return new \IAWP\Overview\Modules\Recent_Conversions_Module($attributes);
342 case 'busiest-time-of-day':
343 return new \IAWP\Overview\Modules\Busiest_Time_Of_Day_Module($attributes);
344 case 'busiest-day-of-week':
345 return new \IAWP\Overview\Modules\Busiest_Day_Of_Week_Module($attributes);
346 case 'new-sessions':
347 return new \IAWP\Overview\Modules\New_Sessions_Module($attributes);
348 default:
349 return null;
350 }
351 }
352 /**
353 * Get a template version of each module for use when creating new modules
354 *
355 * @return self[]
356 */
357 public static function get_template_modules() : array
358 {
359 return [new \IAWP\Overview\Modules\Top_Ten_Module(), new \IAWP\Overview\Modules\Quick_Stats_Module(), new \IAWP\Overview\Modules\Line_Chart_Module(), new \IAWP\Overview\Modules\Pie_Chart_Module(), new \IAWP\Overview\Modules\Map_Module(), new \IAWP\Overview\Modules\Recent_Views_Module(), new \IAWP\Overview\Modules\Recent_Conversions_Module(), new \IAWP\Overview\Modules\Busiest_Time_Of_Day_Module(), new \IAWP\Overview\Modules\Busiest_Day_Of_Week_Module(), new \IAWP\Overview\Modules\New_Sessions_Module()];
360 }
361 /**
362 * Get the modules that are saved to wp_options
363 *
364 * @return self[]
365 */
366 public static function get_saved_modules() : array
367 {
368 $options_storage = new WP_Options_Storage('iawp_overview_modules');
369 $records = $options_storage->all();
370 // No records? Add the default modules.
371 if (\count($records) === 0 && \IAWPSCOPED\iawp()->get_option('iawp_default_modules_added', \false) === \false) {
372 \update_option('iawp_modules_refreshed_at', \time(), \true);
373 \update_option('iawp_default_modules_added', \true, \true);
374 foreach (self::default_modules() as $attributes) {
375 $module = self::new($attributes['module_type'], $attributes);
376 $module->save(\true);
377 }
378 $records = $options_storage->all();
379 static::queue_full_module_refresh();
380 }
381 return \array_map(function ($record) {
382 return self::new($record['module_type'], $record);
383 }, $records);
384 }
385 public static function queue_full_module_refresh() : void
386 {
387 $modules = self::get_saved_modules();
388 static::queue_dataset_refresh($modules);
389 \update_option('iawp_modules_refreshed_at', \time(), \true);
390 }
391 public static function get_saved_module(string $id) : ?self
392 {
393 $modules = self::get_saved_modules();
394 return Collection::make($modules)->first(function (self $module) use($id) {
395 return $module->id() === $id;
396 });
397 }
398 /**
399 * @param string[] $ids
400 *
401 * @return void
402 */
403 public static function set_module_order(array $ids) : void
404 {
405 $options_storage = new WP_Options_Storage('iawp_overview_modules');
406 $options_storage->set_order($ids);
407 }
408 /**
409 * Delete all exiting modules and restore the defaults.
410 *
411 * @return void
412 */
413 public static function reset()
414 {
415 $modules = self::get_saved_modules();
416 foreach ($modules as $module) {
417 $module->delete();
418 }
419 \delete_option('iawp_modules_refreshed_at');
420 \delete_option('iawp_default_modules_added');
421 }
422 public static function default_modules() : array
423 {
424 return [["module_type" => "line-chart", "name" => \esc_html__("Site Traffic", "independent-analytics"), "report" => "views", "primary_metric" => "visitors", "secondary_metric" => "views", "date_range" => "LAST_THIRTY"], ["module_type" => "top-ten", "name" => \esc_html__("Top 10 Pages", "independent-analytics"), "report" => "views", "sort_by" => "visitors", "sort_direction" => "desc", "date_range" => "LAST_SEVEN"], ["module_type" => "map", "name" => \esc_html__("Geographic Traffic", "independent-analytics"), "geo_report" => "geo", "date_range" => "LAST_SEVEN"], ["module_type" => "new-sessions", "name" => \esc_html__("New vs. Returning Sessions", "independent-analytics"), "date_range" => "LAST_THIRTY"], ["module_type" => "quick-stats", "name" => \esc_html__("Site Metrics", "independent-analytics"), "report" => "views", "statistics" => ["visitors", "views", "sessions", "average_session_duration", "bounce_rate", "views_per_session", "clicks"], "date_range" => "LAST_SEVEN", "is_full_width" => \true], ["module_type" => "recent-views", "name" => \esc_html__("Recent Views", "independent-analytics")], ["module_type" => "recent-conversions", "name" => \esc_html__("Recent Conversions", "independent-analytics"), "recent_conversion_types" => ["order", "form_submission", "click"]], ["module_type" => "pie-chart", "name" => \esc_html__("Devices", "independent-analytics"), "report" => "devices", "aggregatable_sort_by" => "visitors", "date_range" => "LAST_THIRTY"], ["module_type" => "pie-chart", "name" => \esc_html__("Top Traffic Sources", "independent-analytics"), "report" => "referrers", "aggregatable_sort_by" => "visitors", "date_range" => "LAST_SEVEN"], ["module_type" => "busiest-time-of-day", "name" => \esc_html__("Busiest Time of Day", "independent-analytics"), "busiest_date_range" => "LAST_NINETY"], ["module_type" => "busiest-day-of-week", "name" => \esc_html__("Busiest Day of Week", "independent-analytics"), "busiest_date_range" => "LAST_NINETY"]];
425 }
426 }
427