Passed
Push — v1 ( 95405e...9db74c )
by Andrew
14:40 queued 05:54
created

IA::logExclusion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 1
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 nystudio107\seomatic\Seomatic;
0 ignored issues
show
Bug introduced by
The type nystudio107\seomatic\Seomatic was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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