Passed
Push — develop ( fa3678...7584a1 )
by Andrew
04:05
created

IA::eventAnalytics()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 28
rs 9.6
c 0
b 0
f 0
cc 2
nc 2
nop 4
1
<?php
2
/**
3
 * Instant Analytics plugin for Craft CMS 3.x
4
 *
5
 * Instant Analytics brings full Google Analytics support to your Twig templates
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\instantanalytics\services;
12
13
use nystudio107\instantanalytics\InstantAnalytics;
14
use nystudio107\instantanalytics\helpers\IAnalytics;
15
use nystudio107\instantanalytics\models\Settings;
16
17
use Jaybizzle\CrawlerDetect\CrawlerDetect;
18
19
use Craft;
20
use craft\base\Component;
21
use craft\elements\User as UserElement;
22
use craft\helpers\UrlHelper;
23
24
use yii\base\Exception;
25
26
/** @noinspection MissingPropertyAnnotationsInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
27
28
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
29
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 4
Loading history...
30
 * @package   InstantAnalytics
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
31
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 3 spaces but found 5
Loading history...
32
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
33
class IA extends Component
34
{
35
    // Public Methods
36
    // =========================================================================
37
38
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
39
     * @var null|IAnalytics
40
     */
41
    protected $cachedAnalytics;
42
43
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $title should have a doc-comment as per coding-style.
Loading history...
44
     * Get the global variables for our Twig context
45
     *
46
     * @param $title
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
47
     *
48
     * @return IAnalytics
49
     */
50
    public function getGlobals($title): IAnalytics
51
    {
52
        if ($this->cachedAnalytics) {
53
            $analytics = $this->cachedAnalytics;
54
        } else {
55
            $analytics = $this->pageViewAnalytics('', $title);
56
            $this->cachedAnalytics = $analytics;
57
        }
58
59
        return $analytics;
60
    }
61
62
    /**
63
     * Get a PageView analytics object
64
     *
65
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
66
     * @param string $title
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
67
     *
68
     * @return null|IAnalytics
69
     */
70
    public function pageViewAnalytics($url = '', $title = '')
71
    {
72
        $result = null;
73
        $analytics = $this->analytics();
74
        if ($analytics) {
0 ignored issues
show
introduced by
$analytics is of type nystudio107\instantanalytics\helpers\IAnalytics, thus it always evaluated to true.
Loading history...
75
            $url = $this->documentPathFromUrl($url);
76
            // Prepare the Analytics object, and send the pageview
77
            $analytics->setDocumentPath($url)
78
                ->setDocumentTitle($title);
79
            $result = $analytics;
80
            Craft::info(
81
                Craft::t(
82
                    'instant-analytics',
83
                    'Created sendPageView for: {url} - {title}',
84
                    [
85
                        'url' => $url,
86
                        'title' => $title
87
                    ]
88
                ),
89
                __METHOD__
90
            );
91
        }
92
93
        return $result;
94
    }
95
96
    /**
97
     * Get an Event analytics object
98
     *
99
     * @param string $eventCategory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
100
     * @param string $eventAction
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
101
     * @param string $eventLabel
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
102
     * @param int    $eventValue
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
103
     *
104
     * @return null|IAnalytics
105
     */
106
    public function eventAnalytics($eventCategory = '', $eventAction = '', $eventLabel = '', $eventValue = 0)
107
    {
108
        $result = null;
109
        $analytics = $this->analytics();
110
        if ($analytics) {
0 ignored issues
show
introduced by
$analytics is of type nystudio107\instantanalytics\helpers\IAnalytics, thus it always evaluated to true.
Loading history...
111
            $url = $this->documentPathFromUrl();
112
            $analytics->setDocumentPath($url)
113
                ->setEventCategory($eventCategory)
114
                ->setEventAction($eventAction)
115
                ->setEventLabel($eventLabel)
116
                ->setEventValue((int)$eventValue);
117
            $result = $analytics;
118
            Craft::info(
119
                Craft::t(
120
                    'instant-analytics',
121
                    'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}',
122
                    [
123
                        'eventCategory' => $eventCategory,
124
                        'eventAction' => $eventAction,
125
                        'eventLabel' => $eventLabel,
126
                        'eventValue' => $eventValue
127
                    ]
128
                ),
129
                __METHOD__
130
            );
131
        }
132
133
        return $result;
134
    }
135
136
    /**
137
     * getAnalyticsObject() return an analytics object
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
138
     *
139
     * @return IAnalytics object
140
     */
141
    public function analytics(): IAnalytics
142
    {
143
        $analytics = $this->getAnalyticsObj();
144
        Craft::info(
145
            Craft::t(
146
                'instant-analytics',
147
                'Created generic analytics object'
148
            ),
149
            __METHOD__
150
        );
151
152
        return $analytics;
153
    }
154
155
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $url should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $title should have a doc-comment as per coding-style.
Loading history...
156
     * Get a PageView tracking URL
157
     *
158
     * @param $url
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
159
     * @param $title
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
160
     *
161
     * @return string
162
     * @throws \yii\base\Exception
163
     */
164
    public function pageViewTrackingUrl($url, $title): string
165
    {
166
        $urlParams = [
167
            'url'   => $url,
168
            'title' => $title,
169
        ];
170
        $path = parse_url($url, PHP_URL_PATH);
171
        $pathFragments = explode('/', rtrim($path, '/'));
172
        $fileName = end($pathFragments);
173
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/'.$fileName, $urlParams);
174
        Craft::info(
175
            Craft::t(
176
                'instant-analytics',
177
                'Created pageViewTrackingUrl for: {trackingUrl}',
178
                [
179
                    'trackingUrl' => $trackingUrl,
180
                ]
181
            ),
182
            __METHOD__
183
        );
184
185
        return $trackingUrl;
186
    }
187
188
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $url should have a doc-comment as per coding-style.
Loading history...
189
     * Get an Event tracking URL
190
     *
191
     * @param        $url
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 8
Loading history...
192
     * @param string $eventCategory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
193
     * @param string $eventAction
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
194
     * @param string $eventLabel
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
195
     * @param int    $eventValue
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
196
     *
197
     * @return string
198
     * @throws \yii\base\Exception
199
     */
200
    public function eventTrackingUrl(
201
        $url,
202
        $eventCategory = '',
203
        $eventAction = '',
204
        $eventLabel = '',
205
        $eventValue = 0
206
    ): string {
207
        $urlParams = [
208
            'url'           => $url,
209
            'eventCategory' => $eventCategory,
210
            'eventAction'   => $eventAction,
211
            'eventLabel'    => $eventLabel,
212
            'eventValue'    => $eventValue,
213
        ];
214
        $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
215
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/'.$fileName, $urlParams);
216
        Craft::info(
217
            Craft::t(
218
                'instant-analytics',
219
                'Created eventTrackingUrl for: {trackingUrl}',
220
                [
221
                    'trackingUrl' => $trackingUrl,
222
                ]
223
            ),
224
            __METHOD__
225
        );
226
227
        return $trackingUrl;
228
    }
229
230
    /**
231
     * _shouldSendAnalytics determines whether we should be sending Google
232
     * Analytics data
233
     *
234
     * @return bool
235
     */
236
    public function shouldSendAnalytics(): bool
237
    {
238
        $result = true;
239
240
        /** @var Settings $settings */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
241
        $settings = InstantAnalytics::$plugin->getSettings();
242
        $request = Craft::$app->getRequest();
243
244
        if (!$settings->sendAnalyticsData) {
245
            $this->logExclusion('sendAnalyticsData');
246
247
            return false;
248
        }
249
250
        if (!$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) {
251
            $this->logExclusion('sendAnalyticsInDevMode');
252
253
            return false;
254
        }
255
256
        if ($request->getIsConsoleRequest()) {
257
            $this->logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()');
258
259
            return false;
260
        }
261
262
        if ($request->getIsCpRequest()) {
263
            $this->logExclusion('Craft::$app->getRequest()->getIsCpRequest()');
264
265
            return false;
266
        }
267
268
        if ($request->getIsLivePreview()) {
269
            $this->logExclusion('Craft::$app->getRequest()->getIsLivePreview()');
270
271
            return false;
272
        }
273
274
        // Check the $_SERVER[] super-global exclusions
275
        if ($settings->serverExcludes !== null && \is_array($settings->serverExcludes)) {
276
            foreach ($settings->serverExcludes as $match => $matchArray) {
277
                if (isset($_SERVER[$match])) {
278
                    foreach ($matchArray as $matchItem) {
279
                        if (preg_match($matchItem, $_SERVER[$match])) {
280
                            $this->logExclusion('serverExcludes');
281
282
                            return false;
283
                        }
284
                    }
285
                }
286
            }
287
        }
288
289
        // Filter out bot/spam requests via UserAgent
290
        if ($settings->filterBotUserAgents) {
291
            $crawlerDetect = new CrawlerDetect;
292
            // Check the user agent of the current 'visitor'
293
            if ($crawlerDetect->isCrawler()) {
294
                $this->logExclusion('filterBotUserAgents');
295
296
                return false;
297
            }
298
        }
299
300
        // Filter by user group
301
        $userService = Craft::$app->getUser();
302
        /** @var UserElement $user */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
303
        $user = $userService->getIdentity();
304
        if ($user) {
0 ignored issues
show
introduced by
$user is of type craft\elements\User, thus it always evaluated to true. If $user can have other possible types, add them to src/services/IA.php:302
Loading history...
305
            if ($settings->adminExclude && $user->admin) {
306
                $this->logExclusion('adminExclude');
307
308
                return false;
309
            }
310
311
            if ($settings->groupExcludes !== null && \is_array($settings->groupExcludes)) {
312
                foreach ($settings->groupExcludes as $matchItem) {
313
                    if ($user->isInGroup($matchItem)) {
314
                        $this->logExclusion('groupExcludes');
315
316
                        return false;
317
                    }
318
                }
319
            }
320
        }
321
322
        return $result;
323
    }
324
325
    /**
326
     * Log the reason for excluding the sending of analytics
327
     *
328
     * @param string $setting
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
329
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
330
    protected function logExclusion(string $setting)
331
    {
332
        /** @var Settings $settings */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
333
        $settings = InstantAnalytics::$plugin->getSettings();
334
        if ($settings->logExcludedAnalytics) {
335
            $request = Craft::$app->getRequest();
336
            $requestIp = $request->getUserIP();
337
            Craft::info(
338
                Craft::t(
339
                    'instant-analytics',
340
                    'Analytics excluded for:: {requestIp} due to: `{setting}`',
341
                    [
342
                        'requestIp' => $requestIp,
343
                        'setting' => $setting,
344
                    ]
345
                ),
346
                __METHOD__
347
            );
348
        }
349
    }
350
351
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $url should have a doc-comment as per coding-style.
Loading history...
352
     * Return a sanitized documentPath from a URL
353
     *
354
     * @param $url
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
355
     *
356
     * @return string
357
     */
358
    protected function documentPathFromUrl($url = ''): string
359
    {
360
        if ($url === '') {
361
            $url = Craft::$app->getRequest()->getFullPath();
362
        }
363
364
        // We want to send just a path to GA for page views
365
        if (UrlHelper::isAbsoluteUrl($url)) {
366
            $urlParts = parse_url($url);
367
            if (isset($urlParts['path'])) {
368
                $url = $urlParts['path'];
369
            } else {
370
                $url = '/';
371
            }
372
            if (isset($urlParts['query'])) {
373
                $url = $url.'?'.$urlParts['query'];
374
            }
375
        }
376
377
        // We don't want to send protocol-relative URLs either
378
        if (UrlHelper::isProtocolRelativeUrl($url)) {
379
            $url = substr($url, 1);
380
        }
381
382
        // Strip the query string if that's the global config setting
383
        $settings = InstantAnalytics::$plugin->getSettings();
384
        if (isset($settings, $settings->stripQueryString) && $settings->stripQueryString) {
385
            $url = UrlHelper::stripQueryString($url);
386
        }
387
388
        // We always want the path to be / rather than empty
389
        if ($url === '') {
390
            $url = '/';
391
        }
392
393
        return $url;
394
    }
395
396
    /**
397
     * Get the Google Analytics object, primed with the default values
398
     *
399
     * @return IAnalytics object
400
     */
401
    private function getAnalyticsObj(): IAnalytics
0 ignored issues
show
Coding Style introduced by
Private method name "IA::getAnalyticsObj" must be prefixed with an underscore
Loading history...
402
    {
403
        $analytics = null;
404
        $settings = InstantAnalytics::$plugin->getSettings();
405
        $request = Craft::$app->getRequest();
406
        if ($settings !== null && !empty($settings->googleAnalyticsTracking)) {
407
            $analytics = new IAnalytics();
408
            if ($analytics) {
0 ignored issues
show
introduced by
$analytics is of type nystudio107\instantanalytics\helpers\IAnalytics, thus it always evaluated to true.
Loading history...
409
                $hostName = $request->getServerName();
410
                if (empty($hostName)) {
411
                    try {
412
                        $hostName = parse_url(UrlHelper::siteUrl(), PHP_URL_HOST);
413
                    } catch (Exception $e) {
414
                        Craft::error(
415
                            $e->getMessage(),
416
                            __METHOD__
417
                        );
418
                    }
419
                }
420
                $userAgent = $request->getUserAgent();
421
                if (empty($userAgent)) {
422
                    $userAgent = "User-Agent:Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13\r\n";
423
                }
424
                $referrer = $request->getReferrer();
425
                if (empty($referrer)) {
426
                    $referrer = '';
427
                }
428
                $analytics->setProtocolVersion('1')
429
                    ->setTrackingId($settings->googleAnalyticsTracking)
430
                    ->setIpOverride($request->getUserIP())
431
                    ->setUserAgentOverride($userAgent)
432
                    ->setDocumentHostName($hostName)
433
                    ->setDocumentReferrer($referrer)
434
                    ->setAsyncRequest(false)
435
                    ->setClientId($this->gaParseCookie());
436
437
                // Set the gclid
438
                $gclid = $this->getGclid();
439
                if ($gclid) {
440
                    $analytics->setGoogleAdwordsId($gclid);
441
                }
442
443
                // Handle UTM parameters
444
                $utm_source = $request->getParam('utm_source');
445
                if (!empty($utm_source)) {
446
                    $analytics->setCampaignSource($utm_source);
447
                }
448
                $utm_medium = $request->getParam('utm_medium');
449
                if (!empty($utm_medium)) {
450
                    $analytics->setCampaignMedium($utm_medium);
451
                }
452
                $utm_campaign = $request->getParam('utm_campaign');
453
                if (!empty($utm_campaign)) {
454
                    $analytics->setCampaignName($utm_campaign);
455
                }
456
                $utm_content = $request->getParam('utm_content');
457
                if (!empty($utm_content)) {
458
                    $analytics->setCampaignContent($utm_content);
459
                }
460
461
                // If SEOmatic is installed, set the affiliation as well
462
                // TODO: handle Seomatic
463
                /*
464
                $seomatic = craft()->plugins->getPlugin('Seomatic');
465
                if ($seomatic && $seomatic->isInstalled && $seomatic->isEnabled) {
466
                    $seomaticSettings = craft()->seomatic->getSettings(craft()->language);
467
                    $analytics->setAffiliation($seomaticSettings['siteSeoName']);
468
                }
469
                */
470
            }
471
        }
472
473
        return $analytics;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $analytics could return the type null which is incompatible with the type-hinted return nystudio107\instantanalytics\helpers\IAnalytics. Consider adding an additional type-check to rule them out.
Loading history...
474
    } /* -- _getAnalyticsObj */
475
476
    /**
477
     * _getGclid get the `gclid` and sets the 'gclid' cookie
478
     */
479
    /**
480
     * _getGclid get the `gclid` and sets the 'gclid' cookie
481
     *
482
     * @return string
483
     */
484
    private function getGclid()
0 ignored issues
show
Coding Style introduced by
Private method name "IA::getGclid" must be prefixed with an underscore
Loading history...
485
    {
486
        $gclid = '';
487
        if (isset($_GET['gclid'])) {
488
            $gclid = $_GET['gclid'];
489
            if (!empty($gclid)) {
490
                setcookie('gclid', $gclid, strtotime('+10 years'), '/');
491
            }
492
        }
493
494
        return $gclid;
495
    }
496
497
    /**
498
     * gaParseCookie handles the parsing of the _ga cookie or setting it to a
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
499
     * unique identifier
500
     *
501
     * @return string the cid
502
     */
503
    private function gaParseCookie()
0 ignored issues
show
Coding Style introduced by
Private method name "IA::gaParseCookie" must be prefixed with an underscore
Loading history...
504
    {
505
        $cid = '';
506
        if (isset($_COOKIE['_ga'])) {
507
            $parts = preg_split('[\.]', $_COOKIE["_ga"], 4);
508
            if ($parts !== false) {
509
                $cid = implode('.', \array_slice($parts, 2));
510
            }
511
        } else {
512
            if (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') {
513
                $cid = $_COOKIE['_ia'];
514
            } else {
515
                $cid = $this->gaGenUUID();
516
            }
517
        }
518
        setcookie('_ia', $cid, strtotime('+2 years'), '/'); // Two years
519
520
        return $cid;
521
    }
522
523
    /**
524
     * gaGenUUID Generate UUID v4 function - needed to generate a CID when one
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
525
     * isn't available
526
     *
527
     * @return string The generated UUID
528
     */
529
    private function gaGenUUID()
0 ignored issues
show
Coding Style introduced by
Private method name "IA::gaGenUUID" must be prefixed with an underscore
Loading history...
530
    {
531
        return sprintf(
532
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
533
            // 32 bits for "time_low"
534
            mt_rand(0, 0xffff),
535
            mt_rand(0, 0xffff),
536
            // 16 bits for "time_mid"
537
            mt_rand(0, 0xffff),
538
            // 16 bits for "time_hi_and_version",
539
            // four most significant bits holds version number 4
540
            mt_rand(0, 0x0fff) | 0x4000,
541
            // 16 bits, 8 bits for "clk_seq_hi_res",
542
            // 8 bits for "clk_seq_low",
543
            // two most significant bits holds zero and one for variant DCE1.1
544
            mt_rand(0, 0x3fff) | 0x8000,
545
            // 48 bits for "node"
546
            mt_rand(0, 0xffff),
547
            mt_rand(0, 0xffff),
548
            mt_rand(0, 0xffff)
549
        );
550
    }
551
}
552