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