PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.14.8
Independent Analytics – WordPress Analytics Plugin v2.14.8
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 / Views / View.php
independent-analytics / IAWP / Views Last commit date
Campaign.php 9 months ago CampaignParameters.php 9 months ago View.php 5 months ago
View.php
371 lines
1 <?php
2
3 namespace IAWP\Views;
4
5 use IAWP\Custom_WordPress_Columns\Views_Column;
6 use IAWP\Illuminate_Builder;
7 use IAWP\Known_Referrers;
8 use IAWP\Models\Page;
9 use IAWP\Models\Page_Home;
10 use IAWP\Models\Page_Post_Type_Archive;
11 use IAWP\Models\Page_Singular;
12 use IAWP\Models\Visitor;
13 use IAWP\Query;
14 use IAWP\Tables;
15 use IAWP\Utils\Device;
16 use IAWP\Utils\String_Util;
17 use IAWP\Utils\Timezone;
18 use IAWP\Utils\URL;
19 use IAWPSCOPED\Illuminate\Database\Query\JoinClause;
20 /** @internal */
21 class View
22 {
23 private $payload;
24 private $referrer_url;
25 private $visitor;
26 private $campaign_parameters;
27 private $viewed_at;
28 private $resource;
29 private $session;
30 /**
31 * @param array $payload
32 * @param string|null $referrer_url
33 * @param Visitor $visitor
34 * @param ?CampaignParameters $campaign_parameters
35 * @param \DateTime|null $viewed_at
36 */
37 public function __construct(array $payload, ?string $referrer_url, Visitor $visitor, ?\IAWP\Views\CampaignParameters $campaign_parameters, ?\DateTime $viewed_at = null)
38 {
39 $this->payload = $payload;
40 $this->referrer_url = \is_null($referrer_url) ? '' : \trim($referrer_url);
41 $this->visitor = $visitor;
42 $this->campaign_parameters = $campaign_parameters;
43 $this->viewed_at = $viewed_at instanceof \DateTime ? $viewed_at : new \DateTime('now', Timezone::utc_timezone());
44 $this->resource = $this->fetch_or_create_resource();
45 // If a resource can't be found or created, a view cannot be recorded
46 if (\is_null($this->resource)) {
47 return;
48 }
49 $this->session = $this->fetch_or_create_session();
50 $view_id = $this->create_view();
51 $this->link_with_previous_view($view_id);
52 $this->set_session_total_views();
53 $this->set_sessions_initial_view($view_id);
54 $this->set_sessions_final_view($view_id);
55 $this->set_views_postmeta($this->resource);
56 }
57 /**
58 * @return int ID of newly created session
59 */
60 public function create_session() : int
61 {
62 $sessions_table = Query::get_table_name(Query::SESSIONS);
63 return Illuminate_Builder::new()->from($sessions_table)->insertGetId(['visitor_id' => $this->visitor->id(), 'referrer_id' => $this->referrer_id(), 'country_id' => $this->fetch_or_create_country(), 'city_id' => $this->fetch_or_create_city(), 'campaign_id' => $this->fetch_or_create_campaign(), 'device_type_id' => Device::getInstance()->type_id(), 'device_os_id' => Device::getInstance()->os_id(), 'device_browser_id' => Device::getInstance()->browser_id(), 'is_first_session' => $this->visitor->is_first_session(), 'created_at' => $this->viewed_at()]);
64 }
65 public function fetch_or_create_country() : ?int
66 {
67 if (!$this->visitor->geoposition()->valid_location()) {
68 return null;
69 }
70 $countries_table = Query::get_table_name(Query::COUNTRIES);
71 $country_id = Illuminate_Builder::new()->from($countries_table)->where('country_code', '=', $this->visitor->geoposition()->country_code())->where('country', '=', $this->visitor->geoposition()->country())->where('continent', '=', $this->visitor->geoposition()->continent())->value('country_id');
72 if (!\is_null($country_id)) {
73 return $country_id;
74 }
75 Illuminate_Builder::new()->from($countries_table)->insertOrIgnore(['country_code' => $this->visitor->geoposition()->country_code(), 'country' => $this->visitor->geoposition()->country(), 'continent' => $this->visitor->geoposition()->continent()]);
76 return Illuminate_Builder::new()->from($countries_table)->where('country_code', '=', $this->visitor->geoposition()->country_code())->where('country', '=', $this->visitor->geoposition()->country())->where('continent', '=', $this->visitor->geoposition()->continent())->value('country_id');
77 }
78 public function fetch_or_create_city() : ?int
79 {
80 if (!$this->visitor->geoposition()->valid_location()) {
81 return null;
82 }
83 $country_id = $this->fetch_or_create_country();
84 $cities_table = Query::get_table_name(Query::CITIES);
85 $city_id = Illuminate_Builder::new()->from($cities_table)->where('country_id', $country_id)->where('subdivision', '=', $this->visitor->geoposition()->subdivision())->where('city', '=', $this->visitor->geoposition()->city())->value('city_id');
86 if (!\is_null($city_id)) {
87 return $city_id;
88 }
89 Illuminate_Builder::new()->from($cities_table)->insertOrIgnore(['country_id' => $country_id, 'subdivision' => $this->visitor->geoposition()->subdivision(), 'city' => $this->visitor->geoposition()->city()]);
90 return Illuminate_Builder::new()->from($cities_table)->where('country_id', $country_id)->where('subdivision', '=', $this->visitor->geoposition()->subdivision())->where('city', '=', $this->visitor->geoposition()->city())->value('city_id');
91 }
92 /**
93 * Fetch the last view, if any.
94 *
95 * @return int|null
96 */
97 private function fetch_last_viewed_resource() : ?int
98 {
99 global $wpdb;
100 $views_table = Query::get_table_name(Query::VIEWS);
101 $session = $this->fetch_current_session();
102 if (\is_null($session)) {
103 return null;
104 }
105 $view = $wpdb->get_row($wpdb->prepare("\n SELECT * FROM {$views_table} WHERE session_id = %d ORDER BY viewed_at DESC LIMIT 1\n ", $session->session_id));
106 if (\is_null($view)) {
107 return null;
108 }
109 return $view->resource_id;
110 }
111 private function viewed_at() : string
112 {
113 return $this->viewed_at->format('Y-m-d\\TH:i:s');
114 }
115 private function link_with_previous_view($view_id) : void
116 {
117 global $wpdb;
118 $views_tables = Query::get_table_name(Query::VIEWS);
119 $sessions_tables = Query::get_table_name(Query::SESSIONS);
120 $session = Illuminate_Builder::new()->from($sessions_tables)->where('session_id', '=', $this->session)->first();
121 if (\is_null($session)) {
122 return;
123 }
124 $final_view_id = $session->final_view_id;
125 $initial_view_id = $session->initial_view_id;
126 if (!\is_null($final_view_id)) {
127 $wpdb->update($views_tables, ['next_view_id' => $view_id, 'next_viewed_at' => $this->viewed_at()], ['id' => $final_view_id]);
128 } elseif (!\is_null($initial_view_id)) {
129 $wpdb->update($views_tables, ['next_view_id' => $view_id, 'next_viewed_at' => $this->viewed_at()], ['id' => $initial_view_id]);
130 }
131 }
132 private function set_session_total_views()
133 {
134 global $wpdb;
135 $sessions_table = Query::get_table_name(Query::SESSIONS);
136 $views_table = Query::get_table_name(Query::VIEWS);
137 $wpdb->query($wpdb->prepare("\n UPDATE {$sessions_table} AS sessions\n LEFT JOIN (\n SELECT\n session_id,\n COUNT(*) AS view_count\n FROM\n {$views_table} AS views\n WHERE\n views.session_id = %d\n GROUP BY\n session_id) AS view_counts ON sessions.session_id = view_counts.session_id\n SET\n sessions.total_views = COALESCE(view_counts.view_count, 0)\n WHERE sessions.session_id = %d\n ", $this->session, $this->session));
138 }
139 private function set_sessions_initial_view(int $view_id)
140 {
141 global $wpdb;
142 $sessions_table = Query::get_table_name(Query::SESSIONS);
143 $wpdb->query($wpdb->prepare("UPDATE {$sessions_table} SET initial_view_id = %d WHERE session_id = %d AND initial_view_id IS NULL", $view_id, $this->session));
144 }
145 private function set_sessions_final_view(int $view_id)
146 {
147 global $wpdb;
148 $sessions_table = Query::get_table_name(Query::SESSIONS);
149 $wpdb->query($wpdb->prepare("\n UPDATE {$sessions_table} AS sessions\n SET\n sessions.final_view_id = %d,\n sessions.ended_at = %s\n WHERE sessions.session_id = %d AND sessions.initial_view_id IS NOT NULL AND sessions.initial_view_id != %d\n ", $view_id, $this->viewed_at(), $this->session, $view_id));
150 }
151 private function create_view() : int
152 {
153 $views_table = Query::get_table_name(Query::VIEWS);
154 return Illuminate_Builder::new()->from($views_table)->insertGetId(['resource_id' => $this->resource->id(), 'viewed_at' => $this->viewed_at(), 'page' => $this->payload['page'], 'session_id' => $this->session]);
155 }
156 private function fetch_resource()
157 {
158 global $wpdb;
159 $resources_table = Query::get_table_name(Query::RESOURCES);
160 $query = '';
161 $payload_copy = \array_merge($this->payload);
162 unset($payload_copy['page']);
163 switch ($payload_copy['resource']) {
164 case 'singular':
165 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND singular_id = %d", $payload_copy['resource'], $payload_copy['singular_id']);
166 break;
167 case 'author_archive':
168 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND author_id = %d", $payload_copy['resource'], $payload_copy['author_id']);
169 break;
170 case 'date_archive':
171 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND date_archive = %s", $payload_copy['resource'], $payload_copy['date_archive']);
172 break;
173 case 'post_type_archive':
174 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND post_type = %s", $payload_copy['resource'], $payload_copy['post_type']);
175 break;
176 case 'term_archive':
177 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND term_id = %s", $payload_copy['resource'], $payload_copy['term_id']);
178 break;
179 case 'search':
180 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND search_query = %s", $payload_copy['resource'], $payload_copy['search_query']);
181 break;
182 case 'home':
183 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s ", $payload_copy['resource']);
184 break;
185 case '404':
186 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND not_found_url = %s", $payload_copy['resource'], $payload_copy['not_found_url']);
187 break;
188 case 'virtual_page':
189 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND virtual_page_id = %s", $payload_copy['resource'], $payload_copy['virtual_page_id']);
190 break;
191 }
192 $resource = $wpdb->get_row($query);
193 if (\is_null($resource)) {
194 return null;
195 }
196 return $resource;
197 }
198 private function fetch_or_create_resource() : ?Page
199 {
200 global $wpdb;
201 $resources_table = Query::get_table_name(Query::RESOURCES);
202 // Allow site owners to make any view for a virtual page
203 $virtual_id = \apply_filters('iawp_convert_to_virtual_page', $this->payload);
204 if (\is_string($virtual_id)) {
205 $this->payload = ['resource' => 'virtual_page', 'virtual_page_id' => $virtual_id, 'page' => $this->payload['page'] ?? 1];
206 }
207 $resource = $this->fetch_resource();
208 if (\is_null($resource)) {
209 $payload_copy = \array_merge($this->payload);
210 unset($payload_copy['page']);
211 $wpdb->insert($resources_table, $payload_copy);
212 $resource = $this->fetch_resource();
213 }
214 if (\is_null($resource)) {
215 return null;
216 }
217 $page = Page::from_row($resource);
218 $page->update_cache();
219 return $page;
220 }
221 /**
222 * @return int|null ID of the session that should be used for this view
223 */
224 private function fetch_or_create_session() : ?int
225 {
226 $session = $this->fetch_current_session();
227 if (\is_null($session)) {
228 return $this->create_session();
229 }
230 $is_same_referrer = $this->referrer_id() === \intval($session->referrer_id);
231 $is_same_resource = \intval($this->fetch_resource()->id) === $this->fetch_last_viewed_resource();
232 $same_as_previous_view = $is_same_referrer && $is_same_resource;
233 // The goal here is to prevent opening multiple tabs to the site from creating multiple sessions
234 if ($is_same_referrer) {
235 return $session->session_id;
236 }
237 // The goal here is to prevent a page refresh from creating another session
238 if ($this->is_internal_referrer($this->referrer_url) || $same_as_previous_view) {
239 return $session->session_id;
240 }
241 return $this->create_session();
242 }
243 /**
244 * @param string|null $referrer_url
245 *
246 * @return bool
247 */
248 private function is_internal_referrer(?string $referrer_url) : bool
249 {
250 return !empty($referrer_url) && String_Util::str_starts_with(\strtolower($referrer_url), \strtolower(\site_url()));
251 }
252 private function referrer_id() : int
253 {
254 $url = new URL($this->referrer_url);
255 $domain = $url->get_domain() ?? $this->referrer_url;
256 $known_referrer = Known_Referrers::get_group_for($domain);
257 // Is the referrer one of a special set of predefined known referrers?
258 if ($known_referrer) {
259 return $this->fetch_referrer(['domain' => $known_referrer['domain'], 'type' => $known_referrer['type'], 'referrer' => $known_referrer['name']]);
260 }
261 // Is the referrer invalid or for the current site?
262 if (!$url->is_valid_url() || $this->is_internal_referrer($this->referrer_url)) {
263 return $this->fetch_referrer(['domain' => '', 'type' => 'Direct', 'referrer' => 'Direct']);
264 }
265 return $this->fetch_referrer(['domain' => $url->get_domain(), 'type' => 'Referrer', 'referrer' => $this->strip_www($url->get_domain())]);
266 }
267 private function fetch_referrer(array $attributes) : int
268 {
269 $referrer = Illuminate_Builder::new()->select(['referrers.id', 'referrers.domain', 'referrers.referrer', 'referrer_types.referrer_type'])->from(Tables::referrers(), 'referrers')->leftJoin(Tables::referrer_types() . ' AS referrer_types', 'referrers.referrer_type_id', '=', 'referrer_types.id')->where('domain', '=', $attributes['domain'])->first();
270 if ($referrer === null) {
271 return Illuminate_Builder::new()->from(Tables::referrers())->insertGetId(['domain' => $attributes['domain'], 'referrer_type_id' => $this->referrer_type_id($attributes['type']), 'referrer' => $attributes['referrer']]);
272 }
273 $updates = [];
274 if ($referrer->referrer !== $attributes['referrer']) {
275 $updates['referrer'] = $attributes['referrer'];
276 }
277 if ($referrer->referrer_type !== $attributes['type']) {
278 $updates['referrer_type_id'] = $this->referrer_type_id($attributes['type']);
279 }
280 if (!empty($updates)) {
281 Illuminate_Builder::new()->from(Tables::referrers())->where('id', '=', $referrer->id)->update($updates);
282 }
283 return $referrer->id;
284 }
285 private function referrer_type_id(string $type) : int
286 {
287 $referrer_type_id = Illuminate_Builder::new()->select('id')->from(Tables::referrer_types())->where('referrer_type', '=', $type)->value('id');
288 if ($referrer_type_id === null) {
289 $referrer_type_id = Illuminate_Builder::new()->from(Tables::referrer_types())->insertGetId(['referrer_type' => $type]);
290 }
291 return $referrer_type_id;
292 }
293 private function strip_www(string $string) : string
294 {
295 if (\strpos($string, "www.") !== 0) {
296 return $string;
297 }
298 return \substr($string, 4);
299 }
300 private function fetch_or_create_campaign() : ?int
301 {
302 if (\is_null($this->campaign_parameters) || \is_null($this->resource->title())) {
303 return null;
304 }
305 $campaign = new \IAWP\Views\Campaign($this->campaign_parameters, $this->resource->title());
306 return $campaign->sync();
307 }
308 private function fetch_current_session() : ?object
309 {
310 $sessions_table = Query::get_table_name(Query::SESSIONS);
311 $session = Illuminate_Builder::new()->from($sessions_table, 'sessions')->selectRaw('IFNULL(ended_at, created_at) AS latest_view_at')->selectRaw('sessions.*')->where('visitor_id', '=', $this->visitor->id())->havingRaw('latest_view_at > DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 MINUTE)')->orderBy('latest_view_at', 'DESC')->first();
312 return $session;
313 }
314 private function set_views_postmeta(Page $resource) : void
315 {
316 if ($resource instanceof Page_Singular) {
317 $this->set_views_postmeta_for_singular($resource);
318 } elseif ($resource instanceof Page_Home) {
319 $this->set_views_postmeta_for_home($resource);
320 } elseif ($resource instanceof Page_Post_Type_Archive && $resource->post_type() === 'product') {
321 $this->set_views_postmeta_for_shop_page($resource);
322 }
323 }
324 private function set_views_postmeta_for_singular(Page_Singular $resource) : void
325 {
326 $singular_id = $resource->get_singular_id();
327 if ($singular_id === null) {
328 return;
329 }
330 $views_table = Query::get_table_name(Query::VIEWS);
331 $resources_table = Query::get_table_name(Query::RESOURCES);
332 $total_views = Illuminate_Builder::new()->selectRaw('COUNT(*) AS views')->from("{$resources_table} as resources")->join("{$views_table} AS views", function (JoinClause $join) {
333 $join->on('resources.id', '=', 'views.resource_id');
334 })->where('singular_id', '=', $singular_id)->value('views');
335 \update_post_meta($singular_id, Views_Column::$meta_key, $total_views);
336 }
337 private function set_views_postmeta_for_home(Page_Home $resource) : void
338 {
339 $blog_page_id = \get_option('page_for_posts');
340 if (\is_string($blog_page_id) && \ctype_digit($blog_page_id)) {
341 $blog_page_id = \intval($blog_page_id);
342 }
343 $blog_page = \get_post($blog_page_id);
344 if ($blog_page === null) {
345 return;
346 }
347 $views_table = Query::get_table_name(Query::VIEWS);
348 $resources_table = Query::get_table_name(Query::RESOURCES);
349 $total_views = Illuminate_Builder::new()->selectRaw('COUNT(*) AS views')->from("{$resources_table} as resources")->join("{$views_table} AS views", function (JoinClause $join) {
350 $join->on('resources.id', '=', 'views.resource_id');
351 })->where('resource', '=', 'home')->value('views');
352 \update_post_meta($blog_page_id, Views_Column::$meta_key, $total_views);
353 }
354 private function set_views_postmeta_for_shop_page(Page_Post_Type_Archive $resource) : void
355 {
356 try {
357 $shop_id = wc_get_page_id('shop');
358 if ($shop_id === -1) {
359 return;
360 }
361 $views_table = Query::get_table_name(Query::VIEWS);
362 $resources_table = Query::get_table_name(Query::RESOURCES);
363 $total_views = Illuminate_Builder::new()->selectRaw('COUNT(*) AS views')->from("{$resources_table} as resources")->join("{$views_table} AS views", function (JoinClause $join) {
364 $join->on('resources.id', '=', 'views.resource_id');
365 })->where('resource', '=', 'post_type_archive')->where('post_type', '=', 'product')->value('views');
366 \update_post_meta($shop_id, Views_Column::$meta_key, $total_views);
367 } catch (\Throwable $e) {
368 }
369 }
370 }
371