Issues (653)

src/helpers/Analytics.php (44 issues)

1
<?php
2
/**
3
 * Instant Analytics plugin for Craft CMS
4
 *
5
 * @author    nystudio107
0 ignored issues
show
The tag in position 1 should be the @package tag
Loading history...
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
6
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
The tag in position 2 should be the @author tag
Loading history...
@copyright tag must contain a year and the name of the copyright holder
Loading history...
7
 * @link      http://nystudio107.com
0 ignored issues
show
The tag in position 3 should be the @copyright tag
Loading history...
8
 * @package   InstantAnalytics
0 ignored issues
show
The tag in position 4 should be the @link tag
Loading history...
9
 * @since     1.0.0
10
 */
0 ignored issues
show
PHP version not specified
Loading history...
Missing @category tag in file comment
Loading history...
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\instantanalyticsGa4\helpers;
13
14
use Craft;
15
use craft\elements\User as UserElement;
16
use craft\helpers\App;
17
use craft\helpers\StringHelper;
18
use craft\helpers\UrlHelper;
19
use Jaybizzle\CrawlerDetect\CrawlerDetect;
20
use nystudio107\instantanalyticsGa4\InstantAnalytics;
21
use nystudio107\seomatic\Seomatic;
22
use yii\base\Exception;
23
24
/**
0 ignored issues
show
Missing short description in doc comment
Loading history...
25
 * @author    nystudio107
0 ignored issues
show
The tag in position 1 should be the @package tag
Loading history...
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
26
 * @package   InstantAnalytics
0 ignored issues
show
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
27
 * @since     5.0.0
0 ignored issues
show
The tag in position 3 should be the @author tag
Loading history...
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
28
 */
0 ignored issues
show
Missing @category tag in class comment
Loading history...
Missing @license tag in class comment
Loading history...
Missing @link tag in class comment
Loading history...
29
class Analytics
30
{
31
    /**
32
     * If SEOmatic is installed, set the page title from it
33
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
34
    public static function getTitleFromSeomatic(): ?string
35
    {
36
        if (!InstantAnalytics::$seomaticPlugin) {
37
            return null;
38
        }
39
        if (!Seomatic::$settings->renderEnabled) {
40
            return null;
41
        }
42
        $titleTag = Seomatic::$plugin->title->get('title');
43
44
        if ($titleTag === null) {
45
            return null;
46
        }
47
48
        $titleArray = $titleTag->renderAttributes();
49
50
        if (empty($titleArray['title'])) {
51
            return null;
52
        }
53
54
        return $titleArray['title'];
55
    }
56
57
    /**
58
     * Return a sanitized documentPath from a URL
59
     *
60
     * @param string $url
0 ignored issues
show
Missing parameter comment
Loading history...
61
     *
62
     * @return string
63
     */
64
    public static function getDocumentPathFromUrl(string $url = ''): string
65
    {
66
        if ($url === '') {
67
            $url = Craft::$app->getRequest()->getFullPath();
68
        }
69
70
        // We want to send just a path to GA for page views
71
        if (UrlHelper::isAbsoluteUrl($url)) {
72
            $urlParts = parse_url($url);
73
            $url = $urlParts['path'] ?? '/';
74
            if (isset($urlParts['query'])) {
75
                $url .= '?' . $urlParts['query'];
76
            }
77
        }
78
79
        // We don't want to send protocol-relative URLs either
80
        if (UrlHelper::isProtocolRelativeUrl($url)) {
81
            $url = substr($url, 1);
82
        }
83
84
        // Strip the query string if that's the global config setting
85
        if (InstantAnalytics::$settings) {
86
            if (InstantAnalytics::$settings->stripQueryString !== null
87
                && InstantAnalytics::$settings->stripQueryString) {
0 ignored issues
show
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
88
                $url = UrlHelper::stripQueryString($url);
89
            }
90
        }
91
92
        // We always want the path to be / rather than empty
93
        if ($url === '') {
94
            $url = '/';
95
        }
96
97
        return $url;
98
    }
99
100
    /**
101
     * Get a PageView tracking URL
102
     *
103
     * @param $url
0 ignored issues
show
Missing parameter comment
Loading history...
104
     * @param $title
0 ignored issues
show
Missing parameter comment
Loading history...
105
     *
106
     * @return string
107
     * @throws Exception
108
     */
109
    public static function getPageViewTrackingUrl($url, $title): string
110
    {
111
        $urlParams = compact('url', 'title');
112
113
        $path = parse_url($url, PHP_URL_PATH);
114
        $pathFragments = explode('/', rtrim($path, '/'));
115
        $fileName = end($pathFragments);
116
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/' . $fileName, $urlParams);
117
118
        InstantAnalytics::$plugin->logAnalyticsEvent(
0 ignored issues
show
The method logAnalyticsEvent() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
        InstantAnalytics::$plugin->/** @scrutinizer ignore-call */ 
119
                                   logAnalyticsEvent(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
            'Created pageViewTrackingUrl for: {trackingUrl}',
120
            [
121
                'trackingUrl' => $trackingUrl,
122
            ],
123
            __METHOD__
124
        );
125
126
        return $trackingUrl;
127
    }
128
129
    /**
130
     * Get an Event tracking URL
131
     *
132
     * @param string $url
0 ignored issues
show
Missing parameter comment
Loading history...
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
133
     * @param string $eventName
0 ignored issues
show
Missing parameter comment
Loading history...
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
134
     * @param array $params
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 2 spaces after parameter type; 1 found
Loading history...
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
135
     * @return string
0 ignored issues
show
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
136
     * @throws Exception
0 ignored issues
show
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
137
     */
138
    public static function getEventTrackingUrl(
139
        string $url,
140
        string $eventName,
141
        array  $params = [],
142
    ): string {
143
        $urlParams = compact('url', 'eventName', 'params');
144
145
        $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
146
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . $fileName, $urlParams);
0 ignored issues
show
Are you sure $fileName of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

146
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . /** @scrutinizer ignore-type */ $fileName, $urlParams);
Loading history...
147
148
        InstantAnalytics::$plugin->logAnalyticsEvent(
149
            'Created eventTrackingUrl for: {trackingUrl}',
150
            [
151
                'trackingUrl' => $trackingUrl,
152
            ],
153
            __METHOD__
154
        );
155
156
        return $trackingUrl;
157
    }
158
159
    /**
160
     * _shouldSendAnalytics determines whether we should be sending Google
161
     * Analytics data
162
     *
163
     * @return bool
164
     */
165
    public static function shouldSendAnalytics(): bool
166
    {
167
        $result = true;
168
        $request = Craft::$app->getRequest();
169
170
        $logExclusion = static function(string $setting) {
0 ignored issues
show
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
171
            if (InstantAnalytics::$settings->logExcludedAnalytics) {
172
                $request = Craft::$app->getRequest();
173
                $requestIp = $request->getUserIP();
174
                InstantAnalytics::$plugin->logAnalyticsEvent(
175
                    'Analytics excluded for:: {requestIp} due to: `{setting}`',
176
                    compact('requestIp', 'setting'),
177
                    __METHOD__
178
                );
179
            }
180
        };
181
182
        if (!InstantAnalytics::$settings->sendAnalyticsData) {
183
            $logExclusion('sendAnalyticsData');
184
            return false;
185
        }
186
187
        if (!InstantAnalytics::$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) {
188
            $logExclusion('sendAnalyticsInDevMode');
189
            return false;
190
        }
191
192
        if ($request->getIsConsoleRequest()) {
193
            $logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()');
194
            return false;
195
        }
196
197
        if ($request->getIsCpRequest()) {
198
            $logExclusion('Craft::$app->getRequest()->getIsCpRequest()');
199
            return false;
200
        }
201
202
        if ($request->getIsLivePreview()) {
203
            $logExclusion('Craft::$app->getRequest()->getIsLivePreview()');
204
            return false;
205
        }
206
207
        // Check the $_SERVER[] super-global exclusions
208
        if (InstantAnalytics::$settings->serverExcludes !== null
209
            && !empty(InstantAnalytics::$settings->serverExcludes)) {
0 ignored issues
show
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
210
            foreach (InstantAnalytics::$settings->serverExcludes as $match => $matchArray) {
211
                if (isset($_SERVER[$match])) {
212
                    foreach ($matchArray as $matchItem) {
213
                        if (preg_match($matchItem, $_SERVER[$match])) {
214
                            $logExclusion('serverExcludes');
215
216
                            return false;
217
                        }
218
                    }
219
                }
220
            }
221
        }
222
223
        // Filter out bot/spam requests via UserAgent
224
        if (InstantAnalytics::$settings->filterBotUserAgents) {
225
            $crawlerDetect = new CrawlerDetect();
226
            // Check the user agent of the current 'visitor'
227
            if ($crawlerDetect->isCrawler()) {
228
                $logExclusion('filterBotUserAgents');
229
230
                return false;
231
            }
232
        }
233
234
        // Filter by user group
235
        $userService = Craft::$app->getUser();
236
        /** @var ?UserElement $user */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
237
        $user = $userService->getIdentity();
238
        if ($user) {
239
            if (InstantAnalytics::$settings->adminExclude && $user->admin) {
240
                $logExclusion('adminExclude');
241
242
                return false;
243
            }
244
245
            if (InstantAnalytics::$settings->groupExcludes !== null
246
                && !empty(InstantAnalytics::$settings->groupExcludes)) {
0 ignored issues
show
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
247
                foreach (InstantAnalytics::$settings->groupExcludes as $matchItem) {
248
                    if ($user->isInGroup($matchItem)) {
249
                        $logExclusion('groupExcludes');
250
251
                        return false;
252
                    }
253
                }
254
            }
255
        }
256
257
        return $result;
258
    }
259
260
    /**
261
     * getClientId handles the parsing of the _ga cookie or setting it to a
0 ignored issues
show
Doc comment short description must start with a capital letter
Loading history...
262
     * unique identifier
263
     *
264
     * @return string the cid
265
     */
266
    public static function getClientId(): string
267
    {
268
        $cid = '';
269
        if (isset($_COOKIE['_ga'])) {
270
            $parts = explode(".", $_COOKIE['_ga'], 4);
271
            if ($parts !== false) {
272
                $cid = implode('.', array_slice($parts, 2));
273
            }
274
        } elseif (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') {
275
            $cid = $_COOKIE['_ia'];
276
        } else {
277
            // Generate our own client id, otherwise.
278
            $cid = static::gaGenUUID() . '.1';
279
        }
280
281
        if (InstantAnalytics::$settings->createGclidCookie && !empty($cid)) {
282
            setcookie('_ia', $cid, strtotime('+2 years'), '/'); // Two years
283
        }
284
285
        return $cid;
286
    }
287
288
    /**
289
     * Get the Google Analytics session string from the cookie.
290
     *
291
     * @return string
292
     */
293
    public static function getSessionString(): string
294
    {
295
        $sessionString = '';
296
        $measurementId = App::parseEnv(InstantAnalytics::$settings->googleAnalyticsMeasurementId);
297
        $cookieName = '_ga_' . StringHelper::removeLeft($measurementId, 'G-');
0 ignored issues
show
It seems like $measurementId can also be of type null; however, parameter $str of craft\helpers\StringHelper::removeLeft() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

297
        $cookieName = '_ga_' . StringHelper::removeLeft(/** @scrutinizer ignore-type */ $measurementId, 'G-');
Loading history...
298
299
        if (isset($_COOKIE[$cookieName])) {
300
            $parts = explode(".", $_COOKIE[$cookieName], 5);
301
            if ($parts !== false) {
302
                $sessionString = implode('.', array_slice($parts, 2, 2));
303
            }
304
        }
305
306
        return $sessionString;
307
    }
308
309
    /**
310
     * Get the user id.
311
     *
312
     * @return string
313
     */
314
    public static function getUserId(): string
315
    {
316
        $userId = Craft::$app->getUser()->getId();
317
318
        if (!$userId) {
319
            return '';
320
        }
321
322
        return $userId;
323
    }
324
325
    /**
326
     * gaGenUUID Generate UUID v4 function - needed to generate a CID when one
0 ignored issues
show
Doc comment short description must start with a capital letter
Loading history...
327
     * isn't available
328
     *
329
     * @return string The generated UUID
330
     */
331
    protected static function gaGenUUID()
332
    {
333
        return sprintf(
334
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
335
            // 32 bits for "time_low"
336
            mt_rand(0, 0xffff),
337
            mt_rand(0, 0xffff),
338
            // 16 bits for "time_mid"
339
            mt_rand(0, 0xffff),
340
            // 16 bits for "time_hi_and_version",
341
            // four most significant bits holds version number 4
342
            mt_rand(0, 0x0fff) | 0x4000,
343
            // 16 bits, 8 bits for "clk_seq_hi_res",
344
            // 8 bits for "clk_seq_low",
345
            // two most significant bits holds zero and one for variant DCE1.1
346
            mt_rand(0, 0x3fff) | 0x8000,
347
            // 48 bits for "node"
348
            mt_rand(0, 0xffff),
349
            mt_rand(0, 0xffff),
350
            mt_rand(0, 0xffff)
351
        );
352
    }
353
}
354