PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.15.0
Independent Analytics – WordPress Analytics Plugin v2.15.0
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 4 days ago
View.php
315 lines
1 <?php
2
3 namespace IAWP\Views;
4
5 use IAWP\Illuminate_Builder;
6 use IAWP\Known_Referrers;
7 use IAWP\Models\Page;
8 use IAWP\Models\Visitor;
9 use IAWP\Query;
10 use IAWP\Tables;
11 use IAWP\Utils\Device;
12 use IAWP\Utils\Timezone;
13 use IAWP\Utils\URL;
14 use IAWP\ViewsColumn\UpdateTotalViewsPostMeta;
15 /** @internal */
16 class View
17 {
18 private $payload;
19 private $referrer_url;
20 private $visitor;
21 private $campaign_parameters;
22 private $viewed_at;
23 private $resource;
24 private $session;
25 /**
26 * @param array $payload
27 * @param string|null $referrer_url
28 * @param Visitor $visitor
29 * @param ?CampaignParameters $campaign_parameters
30 * @param \DateTime|null $viewed_at
31 */
32 public function __construct(array $payload, ?string $referrer_url, Visitor $visitor, ?\IAWP\Views\CampaignParameters $campaign_parameters, ?\DateTime $viewed_at = null)
33 {
34 $this->payload = $payload;
35 $this->referrer_url = \is_null($referrer_url) ? '' : \trim($referrer_url);
36 $this->visitor = $visitor;
37 $this->campaign_parameters = $campaign_parameters;
38 $this->viewed_at = $viewed_at instanceof \DateTime ? $viewed_at : new \DateTime('now', Timezone::utc_timezone());
39 $this->resource = $this->fetch_or_create_resource();
40 // If a resource can't be found or created, a view cannot be recorded
41 if (\is_null($this->resource)) {
42 return;
43 }
44 $this->session = $this->fetch_or_create_session();
45 $view_id = $this->create_view();
46 $this->link_with_previous_view($view_id);
47 $this->set_session_total_views();
48 $this->set_sessions_initial_view($view_id);
49 $this->set_sessions_final_view($view_id);
50 UpdateTotalViewsPostMeta::for_page($this->resource);
51 }
52 /**
53 * @return int ID of newly created session
54 */
55 public function create_session() : int
56 {
57 $sessions_table = Query::get_table_name(Query::SESSIONS);
58 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()]);
59 }
60 public function fetch_or_create_country() : ?int
61 {
62 if (!$this->visitor->geoposition()->valid_location()) {
63 return null;
64 }
65 $countries_table = Query::get_table_name(Query::COUNTRIES);
66 $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');
67 if (!\is_null($country_id)) {
68 return $country_id;
69 }
70 Illuminate_Builder::new()->from($countries_table)->insertOrIgnore(['country_code' => $this->visitor->geoposition()->country_code(), 'country' => $this->visitor->geoposition()->country(), 'continent' => $this->visitor->geoposition()->continent()]);
71 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');
72 }
73 public function fetch_or_create_city() : ?int
74 {
75 if (!$this->visitor->geoposition()->valid_location()) {
76 return null;
77 }
78 $country_id = $this->fetch_or_create_country();
79 $cities_table = Query::get_table_name(Query::CITIES);
80 $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');
81 if (!\is_null($city_id)) {
82 return $city_id;
83 }
84 Illuminate_Builder::new()->from($cities_table)->insertOrIgnore(['country_id' => $country_id, 'subdivision' => $this->visitor->geoposition()->subdivision(), 'city' => $this->visitor->geoposition()->city()]);
85 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');
86 }
87 /**
88 * Fetch the last view, if any.
89 *
90 * @return int|null
91 */
92 private function fetch_last_viewed_resource() : ?int
93 {
94 global $wpdb;
95 $views_table = Query::get_table_name(Query::VIEWS);
96 $session = $this->fetch_current_session();
97 if (\is_null($session)) {
98 return null;
99 }
100 $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));
101 if (\is_null($view)) {
102 return null;
103 }
104 return $view->resource_id;
105 }
106 private function viewed_at() : string
107 {
108 return $this->viewed_at->format('Y-m-d\\TH:i:s');
109 }
110 private function link_with_previous_view($view_id) : void
111 {
112 global $wpdb;
113 $views_tables = Query::get_table_name(Query::VIEWS);
114 $sessions_tables = Query::get_table_name(Query::SESSIONS);
115 $session = Illuminate_Builder::new()->from($sessions_tables)->where('session_id', '=', $this->session)->first();
116 if (\is_null($session)) {
117 return;
118 }
119 $final_view_id = $session->final_view_id;
120 $initial_view_id = $session->initial_view_id;
121 if (!\is_null($final_view_id)) {
122 $wpdb->update($views_tables, ['next_view_id' => $view_id, 'next_viewed_at' => $this->viewed_at()], ['id' => $final_view_id]);
123 } elseif (!\is_null($initial_view_id)) {
124 $wpdb->update($views_tables, ['next_view_id' => $view_id, 'next_viewed_at' => $this->viewed_at()], ['id' => $initial_view_id]);
125 }
126 }
127 private function set_session_total_views()
128 {
129 global $wpdb;
130 $sessions_table = Query::get_table_name(Query::SESSIONS);
131 $views_table = Query::get_table_name(Query::VIEWS);
132 $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));
133 }
134 private function set_sessions_initial_view(int $view_id)
135 {
136 global $wpdb;
137 $sessions_table = Query::get_table_name(Query::SESSIONS);
138 $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));
139 }
140 private function set_sessions_final_view(int $view_id)
141 {
142 global $wpdb;
143 $sessions_table = Query::get_table_name(Query::SESSIONS);
144 $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));
145 }
146 private function create_view() : int
147 {
148 $views_table = Query::get_table_name(Query::VIEWS);
149 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]);
150 }
151 private function fetch_resource()
152 {
153 global $wpdb;
154 $resources_table = Query::get_table_name(Query::RESOURCES);
155 $query = '';
156 $payload_copy = \array_merge($this->payload);
157 unset($payload_copy['page']);
158 switch ($payload_copy['resource']) {
159 case 'singular':
160 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND singular_id = %d", $payload_copy['resource'], $payload_copy['singular_id']);
161 break;
162 case 'author_archive':
163 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND author_id = %d", $payload_copy['resource'], $payload_copy['author_id']);
164 break;
165 case 'date_archive':
166 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND date_archive = %s", $payload_copy['resource'], $payload_copy['date_archive']);
167 break;
168 case 'post_type_archive':
169 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND post_type = %s", $payload_copy['resource'], $payload_copy['post_type']);
170 break;
171 case 'term_archive':
172 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND term_id = %s", $payload_copy['resource'], $payload_copy['term_id']);
173 break;
174 case 'search':
175 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND search_query = %s", $payload_copy['resource'], $payload_copy['search_query']);
176 break;
177 case 'home':
178 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s ", $payload_copy['resource']);
179 break;
180 case '404':
181 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND not_found_url = %s", $payload_copy['resource'], $payload_copy['not_found_url']);
182 break;
183 case 'virtual_page':
184 $query = $wpdb->prepare("SELECT * FROM {$resources_table} WHERE resource = %s AND virtual_page_id = %s", $payload_copy['resource'], $payload_copy['virtual_page_id']);
185 break;
186 }
187 $resource = $wpdb->get_row($query);
188 if (\is_null($resource)) {
189 return null;
190 }
191 return $resource;
192 }
193 private function fetch_or_create_resource() : ?Page
194 {
195 global $wpdb;
196 $resources_table = Query::get_table_name(Query::RESOURCES);
197 // Allow site owners to make any view for a virtual page
198 $virtual_id = \apply_filters('iawp_convert_to_virtual_page', $this->payload);
199 if (\is_string($virtual_id)) {
200 $this->payload = ['resource' => 'virtual_page', 'virtual_page_id' => $virtual_id, 'page' => $this->payload['page'] ?? 1];
201 }
202 $resource = $this->fetch_resource();
203 if (\is_null($resource)) {
204 $payload_copy = \array_merge($this->payload);
205 unset($payload_copy['page']);
206 $wpdb->insert($resources_table, $payload_copy);
207 $resource = $this->fetch_resource();
208 }
209 if (\is_null($resource)) {
210 return null;
211 }
212 $page = Page::from_row($resource);
213 $page->update_cache();
214 return $page;
215 }
216 /**
217 * @return int|null ID of the session that should be used for this view
218 */
219 private function fetch_or_create_session() : ?int
220 {
221 $session = $this->fetch_current_session();
222 if (\is_null($session)) {
223 return $this->create_session();
224 }
225 $is_same_referrer = $this->referrer_id() === \intval($session->referrer_id);
226 $is_same_resource = \intval($this->fetch_resource()->id) === $this->fetch_last_viewed_resource();
227 $same_as_previous_view = $is_same_referrer && $is_same_resource;
228 // The goal here is to prevent opening multiple tabs to the site from creating multiple sessions
229 if ($is_same_referrer) {
230 return $session->session_id;
231 }
232 // The goal here is to prevent a page refresh from creating another session
233 if ($this->is_internal_referrer($this->referrer_url) || $same_as_previous_view) {
234 return $session->session_id;
235 }
236 return $this->create_session();
237 }
238 /**
239 * @param string|null $referrer_url
240 *
241 * @return bool
242 */
243 private function is_internal_referrer(?string $referrer_url) : bool
244 {
245 return !empty($referrer_url) && \str_starts_with(\strtolower($referrer_url), \strtolower(\get_home_url()));
246 }
247 private function referrer_id() : int
248 {
249 $url = new URL($this->referrer_url);
250 $domain = $url->get_domain() ?? $this->referrer_url;
251 $known_referrer = Known_Referrers::get_group_for($domain);
252 // Is the referrer one of a special set of predefined known referrers?
253 if ($known_referrer) {
254 return $this->fetch_referrer(['domain' => $known_referrer['domain'], 'type' => $known_referrer['type'], 'referrer' => $known_referrer['name']]);
255 }
256 // Is the referrer invalid or for the current site?
257 if (!$url->is_valid_url() || $this->is_internal_referrer($this->referrer_url)) {
258 return $this->fetch_referrer(['domain' => '', 'type' => 'Direct', 'referrer' => 'Direct']);
259 }
260 return $this->fetch_referrer(['domain' => $url->get_domain(), 'type' => 'Referrer', 'referrer' => $this->strip_www($url->get_domain())]);
261 }
262 private function fetch_referrer(array $attributes) : int
263 {
264 $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();
265 if ($referrer === null) {
266 return Illuminate_Builder::new()->from(Tables::referrers())->insertGetId(['domain' => $attributes['domain'], 'referrer_type_id' => $this->referrer_type_id($attributes['type']), 'referrer' => $attributes['referrer']]);
267 }
268 $updates = [];
269 if ($referrer->referrer !== $attributes['referrer']) {
270 $updates['referrer'] = $attributes['referrer'];
271 }
272 if ($referrer->referrer_type !== $attributes['type']) {
273 $updates['referrer_type_id'] = $this->referrer_type_id($attributes['type']);
274 }
275 if (!empty($updates)) {
276 Illuminate_Builder::new()->from(Tables::referrers())->where('id', '=', $referrer->id)->update($updates);
277 }
278 return $referrer->id;
279 }
280 private function referrer_type_id(string $type) : int
281 {
282 $table = Tables::referrer_types();
283 $referrer_type_id = Illuminate_Builder::new()->from($table)->where('referrer_type', '=', $type)->value('id');
284 if ($referrer_type_id !== null) {
285 return $referrer_type_id;
286 }
287 try {
288 return Illuminate_Builder::new()->from($table)->insertGetId(['referrer_type' => $type]);
289 } catch (\Throwable $error) {
290 return Illuminate_Builder::new()->from($table)->where('referrer_type', '=', $type)->value('id');
291 }
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 }
315