[TASK] Release of 3.1.1
[TYPO3CMS/Extensions/rsgoogleanalytics.git] / class.tx_rsgoogleanalytics.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2005-2012 Steffen Ritter (info@rs-websystems.de)
6 *
7 * All rights reserved
8 *
9 * This script is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; version 2 of the License.
12 *
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 *
16 * This script is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * This copyright notice MUST APPEAR in all copies of the script!
22 ***************************************************************/
23
24 /*
25 * Inserts Google Analytics code into any page. Also uses hooks to add tracking code to external
26 * or download links. Provides an API for adding custom tracking variables
27 *
28 * Originally inspired by m1_google_analytics.
29 *
30 * @author Steffen Ritter <info@rs-websystems.de>
31 * @package TYPO3
32 * @subpackage tx_rsgoogleanalytics
33 */
34 class tx_rsgoogleanalytics implements t3lib_singleton {
35 /**
36 * @var string
37 */
38 var $trackerVar = 'pageTracker';
39
40 /**
41 * Saves TypoScript config
42 */
43 var $modConfig = array();
44
45 /**
46 * @var array
47 */
48 protected $commands = array();
49
50 /**
51 * @var array
52 */
53 protected $domainConfig = array();
54
55 /**
56 * @var string
57 */
58 protected $selectedDomain;
59
60 /**
61 * @var array
62 */
63 protected $eCommerce = array('items' => array(), 'transaction' => array());
64
65 /**
66 * Constructs the system.
67 */
68 public function __construct() {
69 $this->modConfig = $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_rsgoogleanalytics.'];
70
71 if (t3lib_extmgm::isLoaded('naw_securedl')) {
72 $this->specialFiles = 'naw';
73 }
74 if (t3lib_extmgm::isLoaded('dam_frontend')) {
75 $this->specialFiles = 'dam_frontend';
76 }
77 }
78
79 /**
80 * Returns the extension's TypoScript configuration
81 *
82 * @return array
83 */
84 public function getModConfig() {
85 return $this->modConfig;
86 }
87
88 /**
89 * Adds a GA command to the command list
90 *
91 * @param string $command The GA command
92 * @param int $key The key at which to add the command in the commands array (at the end if empty)
93 */
94 public function addCommand($command, $key = 0) {
95 $key = intval($key);
96 if (empty($key)) {
97 array_push($this->commands, $command);
98 } else {
99 $this->commands[$key] = $command;
100 }
101 }
102
103 /**
104 * Adds the tracking code at the end of the body tag (pi Method called from TS USER_INT). further the method
105 * Adds some js code for downloads and external links if configured.
106 *
107 * @param string $content page content
108 * @param array $params Additional call parameters (unused for now)
109 * @return string Page content with google tracking code
110 */
111 public function processTrackingCode($content, $params) {
112 // return if the extension is not activated or no account is configured
113 if (!$this->isActive()) {
114 return $content;
115 }
116 // detect how the pageTitle should be rendered
117 if ($this->modConfig['registerTitle'] == 'title') {
118 $pageName = str_replace(array(CR, LF), '', trim($GLOBALS['TSFE']->page['title']));
119 } elseif ($this->modConfig['registerTitle'] == 'rootline') {
120 $rootline = $GLOBALS['TSFE']->sys_page->getRootLine($GLOBALS['TSFE']->page['uid']);
121 $pageName = '';
122 $rootlineLength = count($rootline);
123 for ($i = 0; $i < $rootlineLength; $i++) {
124 if ($rootline[$i]['is_siteroot'] == 0) {
125 $title = str_replace(array(CR, LF), '', $rootline[$i]['title']);
126 $pageName .= '/' . addslashes(trim($title));
127 }
128 }
129 } else {
130 $pageName = NULL;
131 }
132
133 // Hook for special treatment of the page name
134 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rsgoogleanalytics']['processPageName'])) {
135 foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rsgoogleanalytics']['processPageName'] as $className) {
136 $processor = &t3lib_div::getUserObj($className);
137 $pageName = $processor->processPageName($pageName, $this);
138 }
139 }
140 return $this->buildTrackingCode($pageName);
141 }
142
143 /**
144 * Generates the google tracking code (JS script at the end of the body tag).
145 *
146 * @param string $pageName Name of the page to register for tracking
147 * @return string JS tracking code
148 */
149 protected function buildTrackingCode($pageName = NULL) {
150 $codeTemplate = file_get_contents(t3lib_div::getFileAbsFileName($this->modConfig['templateFile']));
151 $marker = array(
152 'ACCOUNT' => $this->modConfig['account'],
153 'TRACKER_VAR' => $this->trackerVar,
154 'COMMANDS' => ''
155 );
156
157 if ($pageName === NULL) {
158 $this->commands[999] = $this->buildCommand('trackPageview', array());
159 } else {
160 $this->commands[999] = $this->buildCommand('trackPageview', array($pageName));
161 }
162
163 $this->makeDomainConfiguration();
164 $this->makeSearchEngineConfiguration(); // 100
165 $this->makeSpecialVars(); // 300
166 $this->makeDataTracking(); // 500
167 $this->makeECommerceTracking(); // 2000
168
169 ksort($this->commands);
170 $marker['COMMANDS'] = implode("\n", $this->commands);
171 $code = t3lib_parsehtml::substituteMarkerArray($codeTemplate, $marker, '###|###', TRUE, TRUE);
172
173 return $code;
174 }
175
176 /**
177 * Generates Commands which are needed for sub/cross-domain-tracking.
178 * linkProcessing needs this to handle the domains, which should get a "link" tracker
179 *
180 * @return void
181 */
182 protected function makeDomainConfiguration() {
183 // If the domain configuration has already been handled, this array will not be empty
184 // With this check we avoid handling the configuration twice
185 if (count($this->domainConfig) == 0) {
186 if ($this->modConfig['multipleDomains'] && $this->modConfig['multipleDomains'] != 'false') {
187 // Extract the list of domains
188 $this->domainConfig['multiple'] = t3lib_div::trimExplode(',', $this->modConfig['multipleDomains.']['domainNames'], TRUE);
189 $numberOfDomains = count($this->domainConfig['multiple']);
190 // If there's only one, use it as is
191 if ($numberOfDomains == 1) {
192 $this->selectedDomain = $this->domainConfig['multiple'][0];
193
194 // If there are more than one, try to match to the current domain
195 } elseif ($numberOfDomains > 1) {
196 $currentDomain = t3lib_div::getIndpEnv('TYPO3_HOST_ONLY');
197 for ($i = 0; $i < $numberOfDomains; $i++) {
198 if (strstr($currentDomain, $this->domainConfig['multiple'][$i]) == $this->domainConfig['multiple'][$i]) {
199 $this->selectedDomain = $this->domainConfig['multiple'][$i];
200 break;
201 }
202 }
203 }
204 $this->commands[10] = $this->buildCommand('setDomainName', array((empty($this->selectedDomain)) ? 'none' : $this->selectedDomain));
205 $this->commands[11] = $this->buildCommand('setAllowLinker', array(TRUE));
206
207 } elseif ($this->modConfig['trackSubDomains'] && $this->modConfig['trackSubDomains'] != 'false') {
208 $this->commands[10] = $this->buildCommand('setDomainName', array('.' . $this->modConfig['trackSubDomains.']['domainName']));
209 }
210 }
211 }
212
213 /**
214 * Generates Commands for tracking custom variables
215 *
216 * @return void
217 */
218 protected function makeSpecialVars() {
219 $x = 300;
220 /** @var $cObj tslib_cObj */
221 $cObj = t3lib_div::makeInstance('tslib_cObj');
222
223 // Render CustomVars
224 for ($i = 1; $i <= 5; $i++) {
225 if (is_array($this->modConfig['customVars.'][$i . '.'])) {
226 $data = $cObj->stdWrap('', $this->modConfig['customVars.'][$i . '.']);
227 if (trim($data)) {
228 $this->commands[$x] = $this->buildCommand(
229 'setCustomVar',
230 array(
231 $i, $this->modConfig['customVars.'][$i . '.']['name'],
232 $data, $this->modConfig['customVars.'][$i . '.']['scope']
233 )
234 );
235 }
236 $x++;
237 }
238 }
239
240 // Render customSegment
241 $currentValue = explode('.', $_COOKIE['__utmv']);
242 $currentValue = $currentValue[1];
243 $shouldBe = $cObj->stdWrap('', $this->modConfig['visitorSegment.']);
244 if ($currentValue != $shouldBe && trim($shouldBe) !== '') {
245 $this->commands[$x] = $this->buildCommand('setVar', array($shouldBe));
246 }
247 }
248
249 /**
250 * Generates Commands for e-commerce tracking
251 *
252 * @return void
253 */
254 protected function makeECommerceTracking() {
255 if (!$this->modConfig['eCommerce.']['enableTracking']) {
256 return;
257 }
258
259 // Should be after trackPageView()
260 $i = 2000;
261 foreach ($this->eCommerce['transaction'] AS $trans) {
262 $this->commands[$i] = $this->buildCommand('addTrans', $trans);
263 $i++;
264 }
265 foreach ($this->eCommerce['items'] AS $item) {
266 $this->commands[$i] = $this->buildCommand('addItem', $item);
267 $i++;
268 }
269
270 if (count($this->eCommerce['transaction']) > 0 || count($this->eCommerce['items']) > 0) {
271 $this->commands[$i] = $this->buildCommand('trackTrans', array());
272 }
273 }
274
275 /**
276 * Generates Commands related to search engine configuration
277 * @return void
278 */
279 protected function makeSearchEngineConfiguration() {
280 // Set keywords which should marked as redirect
281 if ($this->modConfig['redirectKeywords']) {
282 $keywords = t3lib_div::trimExplode(',', $this->modConfig['redirectKeywords'], 1);
283 $i = 100;
284 foreach ($keywords AS $val) {
285 $this->commands[$i] = $this->buildCommand('addIgnoredOrganic', array($val));
286 $i++;
287 }
288 }
289 // which referrers should be handled as "own domain"
290 if ($this->modConfig['redirectReferer']) {
291 $domains = t3lib_div::trimExplode(',', $this->modConfig['redirectReferer'], 1);
292 $i = 0;
293 foreach ($domains AS $val) {
294 $this->commands[$i] = $this->buildCommand('addIgnoredRef', array($val));
295 $i++;
296 }
297 }
298 }
299
300 /**
301 * Generates Commands for data tracking
302 *
303 * @return void
304 */
305 protected function makeDataTracking() {
306 if ($this->modConfig['disableDataTracking.']['browserInfo']) {
307 $this->commands[500] = $this->buildCommand('setClientInfo', array(FALSE));
308 }
309 if ($this->modConfig['disableDataTracking.']['flashTest']) {
310 $this->commands[501] = $this->buildCommand('setDetectFlash', array(FALSE));
311 }
312 if ($this->modConfig['disableDataTracking.']['pageTitle']) {
313 $this->commands[502] = $this->buildCommand('setDetectTitle', array(FALSE));
314 }
315 if ($this->modConfig['disableDataTracking.']['anonymizeIp']) {
316 $this->commands[503] = $this->buildGatCommand('anonymizeIp', array());
317 }
318 }
319
320 /**
321 * Assembles a single tracker command
322 *
323 * If the command needs to be applied to the global gat object, use the buildGatCommand() method instead.
324 *
325 * @param string $command The name of the command
326 * @param array $parameter The list of call parameters
327 * @return string The assembled JavaScript command
328 */
329 public function buildCommand($command, array $parameter) {
330 // Generate traditional code
331 if (empty($this->modConfig['asynchronous'])) {
332 $command = "\t" . $this->trackerVar . '._' . $command . '(' . implode(', ', $this->wrapJSParams($parameter)) . ');';
333
334 // Generate asynchronous code
335 } else {
336 $command = "\t_gaq.push(['_" . $command . "'";
337 if (count($parameter) > 0) {
338 $command .= ', ' . implode(', ', $this->wrapJSParams($parameter));
339 }
340 $command .= ']);';
341 }
342 return $command;
343 }
344
345 /**
346 * Assembles a single command for the gat object
347 *
348 * A few commands (mostly anonymizeIp) refer to the global gat object, rather than the page tracker.
349 * This method handles these special needs.
350 *
351 * @param string $command The name of the command
352 * @param array $parameter The list of call parameters
353 * @return string The assembled JavaScript command
354 */
355 public function buildGatCommand($command, array $parameter) {
356 // Generate traditional code
357 if (empty($this->modConfig['asynchronous'])) {
358 $command = "\t" . '_gat._' . $command . '(' . implode(', ', $this->wrapJSParams($parameter)) . ');';
359
360 // Generate asynchronous code
361 } else {
362 $command = "\t_gaq.push(['_gat._" . $command . "'";
363 if (count($parameter) > 0) {
364 $command .= ', ' . implode(', ', $this->wrapJSParams($parameter));
365 }
366 $command .= ']);';
367 }
368 return $command;
369 }
370
371 /**
372 * Wraps and escapes a list of parameters for proper usage in JavaScript
373 *
374 * @param array $parameter List of parameters to handle
375 * @return array The wrapped and escaped parameters
376 */
377 protected function wrapJSParams(array $parameter) {
378 for ($i = 0; $i < count($parameter); $i++) {
379 if (is_bool($parameter[$i])) {
380 $parameter[$i] = ($parameter[$i] ? 'true' : 'false');
381 } else {
382 $parameter[$i] = "'" . str_replace("'", "\'", $parameter[$i]) . "'";
383 }
384 }
385 return $parameter;
386 }
387
388 /**
389 * This method checks whether the URL is in the list to track
390 *
391 * @param string $url filename (with directories from site root) which is linked
392 * @return boolean True if filename is in locations, false if not
393 */
394 protected function checkURL($url) {
395 $locations = t3lib_div::trimExplode(',', $this->modConfig['trackExternals.']['domainList'], 1);
396 foreach ($locations as $location) {
397 if (strpos($url, $location) !== FALSE) {
398 return TRUE;
399 }
400 }
401 return FALSE;
402 }
403
404 /**
405 * Checks whether the given URL should be considered as being cross-domain
406 *
407 * @param string $url The URL to check
408 * @return bool
409 */
410 protected function isUrlCrossDomain($url) {
411 $urlParts = parse_url($url);
412 // Loop on all configured domains
413 $numberOfDomains = count($this->domainConfig['multiple']);
414 for ($i = 0; $i < $numberOfDomains; $i++) {
415 $currentDomain = $this->domainConfig['multiple'][$i];
416 // Stop at the first domain that matches and return true
417 if ($currentDomain != $this->selectedDomain && strpos($urlParts['host'], $currentDomain)) {
418 return TRUE;
419 }
420 }
421 return FALSE;
422 }
423
424 /**
425 * This method checks whether the given file is in the paths to track
426 *
427 * @param string $file Filename (with directories from site root) which is linked
428 * @return boolean True if filename is in locations, false if not
429 */
430 protected function checkFilePath($file) {
431 $locations = t3lib_div::trimExplode(',', $this->modConfig['trackDownloads.']['folderList']);
432 foreach ($locations as $location) {
433 if (strpos($file, $location) !== FALSE) {
434 return TRUE;
435 }
436 }
437 return FALSE;
438 }
439
440 /**
441 * This method checks whether the given file if of a type to track
442 *
443 * @param string $file Filename (with directories from site root) which is linked
444 * @return boolean True if filename is in list, false if not
445 */
446 protected function checkFileType($file) {
447 $pathParts = pathinfo($file);
448 return t3lib_div::inList($this->modConfig['trackDownloads.']['fileTypes'], $pathParts['extension']);
449 }
450
451 /**
452 * Checks whether filePath and Type is in allowed range
453 *
454 * @param string $file filename (with directories from site root) which is linked
455 * @return boolean True if filename is in locations and file type should be tracked, false if not
456 */
457 protected function checkFile(&$file) {
458 return $this->checkFilePath($file) && $this->checkFileType($file);
459
460 }
461
462 /**
463 * Hooks into TYPOLink generation
464 * Classic userFunc hook called in tslib/tslib_content.php
465 * Used to add Google Analytics tracking code to hyperlinks
466 *
467 * @param array $params TypoLink configuration
468 * @param tslib_cObj $reference Back-reference to the calling object
469 * @return void
470 */
471 public function linkPostProcess(&$params, $reference) {
472 if (!$this->isActive()) {
473 return;
474 }
475 // Make sure the domain configuration has been handled
476 $this->makeDomainConfiguration();
477
478 $url = $params['finalTagParts']['url'];
479 $function = FALSE;
480 switch ($params['finalTagParts']['TYPE']) {
481 case 'page':
482 // If cross-domain linking is activated, check if the link is indeed cross-domain
483 // Otherwise, nothing special needs to be done
484 if (!empty($this->selectedDomain)) {
485 // If typolink URL is cross-domain, add GA link code
486 if ($this->isUrlCrossDomain($url)) {
487 $function = 'RsGoogleAnalytics.crossDomainLink(this); return false;';
488 }
489 }
490 break;
491 case 'url' :
492 if ( /*checkInMultiple($url)*/
493 0) {
494 $function = $this->buildCommand('link', array($url)) . 'return false;';
495 } elseif ($this->modConfig['trackExternals'] && ($this->modConfig['trackExternals'] == '!ALL') || $this->checkURL($url)) {
496 $function = $this->buildCommand('trackEvent', array('Leaving Site', 'External URL', $url));
497 }
498 break;
499 case 'file':
500 if ($this->modConfig['trackDownloads']) {
501 $fileInfo = pathinfo($url);
502 // TODO: provide hook where downloader extension can register their transformation function
503
504 if ($this->modConfig['trackDownloads'] == '!ALL' || $this->checkFile($url)) {
505 $function = $this->buildCommand('trackEvent', array('Download', $fileInfo['extension'], $url));
506 }
507 }
508 break;
509 }
510 if (!stripos('onclick', $params['finalTagParts']['aTagParams']) && $function !== FALSE) {
511 $function = str_replace('"', '\'', trim($function));
512 $params['finalTagParts']['aTagParams'] .= ' onclick="' . $function . '"';
513 $params['finalTag'] = str_replace('>', ' onclick="' . $function . '">', $params['finalTag']);
514 }
515 }
516
517 /**
518 * adds an single Item to an eCommerce Transaction to be tracked
519 *
520 * @param string $orderId
521 * @param string $sku
522 * @param string $name
523 * @param string $category
524 * @param string $price
525 * @param string $quantity
526 * @return void
527 */
528 public function addCommerceItem($orderId, $sku, $name, $category, $price, $quantity) {
529 if (!$this->isActive() || !$this->modConfig['eCommerce.']['enableTracking']) {
530 return;
531 }
532
533 if (isset($this->eCommerce['transaction'][$orderId])) {
534 $this->eCommerce['items'][] = array(0 => $orderId, 1 => $sku, 2 => $name, 3 => $category, 4 => $price, 5 => $quantity);
535 }
536 }
537
538 /**
539 * adds an e-commerce transaction to be tracked
540 *
541 * @param string $orderId
542 * @param string $storeName
543 * @param string $total
544 * @param string $tax
545 * @param string $shipping
546 * @param string $city
547 * @param string $state
548 * @param string $country
549 * @return void
550 */
551 public function addCommerceTransaction($orderId, $storeName, $total, $tax, $shipping, $city, $state, $country) {
552 if (!$this->isActive() || !$this->modConfig['eCommerce.']['enableTracking']) return;
553
554 $this->eCommerce['transaction'][$orderId] = array(0 => $orderId, 1 => $storeName, 2 => $total, 3 => $tax, 4 => $shipping, 5 => $city, 6 => $state, 7 => $country);
555 }
556
557 /**
558 * Checks whether the plugin is active
559 *
560 * @return bool
561 */
562 protected function isActive() {
563 return intval($this->modConfig['active']) == 1 && trim($this->modConfig['account']) != '';
564 }
565 }
566
567
568 if (defined("TYPO3_MODE") && $TYPO3_CONF_VARS[TYPO3_MODE]["XCLASS"]["ext/rsgoogleanalytics/class.tx_rsgoogleanalytics.php"]) {
569 include_once($TYPO3_CONF_VARS[TYPO3_MODE]["XCLASS"]["ext/rsgoogleanalytics/class.tx_rsgoogleanalytics.php"]);
570 }
571
572 ?>