PageRenderTime 138ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/core/Piwik.php

https://github.com/quarkness/piwik
PHP | 1974 lines | 1195 code | 170 blank | 609 comment | 154 complexity | ee141491e368278413a35016d2153295 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * @see core/Translate.php
  14. */
  15. require_once PIWIK_INCLUDE_PATH . '/core/Translate.php';
  16. /**
  17. * Main piwik helper class.
  18. * Contains static functions you can call from the plugins.
  19. *
  20. * @package Piwik
  21. */
  22. class Piwik
  23. {
  24. const CLASSES_PREFIX = 'Piwik_';
  25. const COMPRESSED_FILE_LOCATION = '/tmp/assets/';
  26. /*
  27. * Piwik periods
  28. */
  29. public static $idPeriods = array(
  30. 'day' => 1,
  31. 'week' => 2,
  32. 'month' => 3,
  33. 'year' => 4,
  34. 'range' => 5,
  35. );
  36. /**
  37. * Should we process and display Unique Visitors?
  38. * -> Always process for day/week/month periods
  39. * For Year and Range, only process if it was enabled in the config file,
  40. *
  41. * @param string $periodLabel Period label (e.g., 'day')
  42. * @return bool
  43. */
  44. static public function isUniqueVisitorsEnabled($periodLabel)
  45. {
  46. return in_array($periodLabel, array('day', 'week', 'month'))
  47. || Zend_Registry::get('config')->General->enable_processing_unique_visitors_year_and_range ;
  48. }
  49. /*
  50. * Prefix/unprefix class name
  51. */
  52. /**
  53. * Prefix class name (if needed)
  54. *
  55. * @param string $class
  56. * @return string
  57. */
  58. static public function prefixClass( $class )
  59. {
  60. if(!strncmp($class, Piwik::CLASSES_PREFIX, strlen(Piwik::CLASSES_PREFIX)))
  61. {
  62. return $class;
  63. }
  64. return Piwik::CLASSES_PREFIX.$class;
  65. }
  66. /**
  67. * Unprefix class name (if needed)
  68. *
  69. * @param string $class
  70. * @return string
  71. */
  72. static public function unprefixClass( $class )
  73. {
  74. $lenPrefix = strlen(Piwik::CLASSES_PREFIX);
  75. if(!strncmp($class, Piwik::CLASSES_PREFIX, $lenPrefix))
  76. {
  77. return substr($class, $lenPrefix);
  78. }
  79. return $class;
  80. }
  81. /*
  82. * Installation / Uninstallation
  83. */
  84. /**
  85. * Installation helper
  86. */
  87. static public function install()
  88. {
  89. Piwik_Common::mkdir(PIWIK_USER_PATH . '/' . Zend_Registry::get('config')->smarty->compile_dir);
  90. }
  91. /**
  92. * Uninstallation helper
  93. */
  94. static public function uninstall()
  95. {
  96. Piwik_Db_Schema::getInstance()->dropTables();
  97. }
  98. /**
  99. * Returns true if Piwik is installed
  100. *
  101. * @since 0.6.3
  102. *
  103. * @return bool True if installed; false otherwise
  104. */
  105. static public function isInstalled()
  106. {
  107. return Piwik_Db_Schema::getInstance()->hasTables();
  108. }
  109. /**
  110. * Called on Core install, update, plugin enable/disable
  111. * Will clear all cache that could be affected by the change in configuration being made
  112. */
  113. static public function deleteAllCacheOnUpdate()
  114. {
  115. Piwik_AssetManager::removeMergedAssets();
  116. Piwik_View::clearCompiledTemplates();
  117. Piwik_Common::deleteTrackerCache();
  118. }
  119. /**
  120. * Returns the cached the Piwik URL, eg. http://demo.piwik.org/ or http://example.org/piwik/
  121. * If not found, then tries to cache it and returns the value.
  122. *
  123. * If the Piwik URL changes (eg. Piwik moved to new server), the value will automatically be refreshed in the cache.
  124. * @return string
  125. */
  126. static public function getPiwikUrl()
  127. {
  128. $key = 'piwikUrl';
  129. $url = Piwik_GetOption($key);
  130. if(Piwik_Common::isPhpCliMode()
  131. // in case archive.php is triggered with domain localhost
  132. || Piwik_Common::isArchivePhpTriggered())
  133. {
  134. return $url;
  135. }
  136. $currentUrl = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrlWithoutFileName());
  137. if(empty($url)
  138. // if URL changes, always update the cache
  139. || $currentUrl != $url)
  140. {
  141. if(strlen($currentUrl) >= strlen('http://a/'))
  142. {
  143. Piwik_SetOption($key, $currentUrl, $autoload = true);
  144. }
  145. $url = $currentUrl;
  146. }
  147. return $url;
  148. }
  149. /*
  150. * HTTP headers
  151. */
  152. /**
  153. * Returns true if this appears to be a secure HTTPS connection
  154. *
  155. * @return bool
  156. */
  157. static public function isHttps()
  158. {
  159. return Piwik_Url::getCurrentScheme() === 'https';
  160. }
  161. /**
  162. * Set response header, e.g., HTTP/1.0 200 Ok
  163. *
  164. * @param string $status Status
  165. * @return bool
  166. */
  167. static public function setHttpStatus($status)
  168. {
  169. if(substr_compare(PHP_SAPI, '-fcgi', -5))
  170. {
  171. @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status);
  172. }
  173. else
  174. {
  175. // FastCGI
  176. @header('Status: ' . $status);
  177. }
  178. }
  179. /**
  180. * Workaround IE bug when downloading certain document types over SSL and
  181. * cache control headers are present, e.g.,
  182. *
  183. * Cache-Control: no-cache
  184. * Cache-Control: no-store,max-age=0,must-revalidate
  185. * Pragma: no-cache
  186. *
  187. * @see http://support.microsoft.com/kb/316431/
  188. * @see RFC2616
  189. *
  190. * @param string $override One of "public", "private", "no-cache", or "no-store". (optional)
  191. */
  192. static public function overrideCacheControlHeaders($override = null)
  193. {
  194. if($override || self::isHttps())
  195. {
  196. @header('Pragma: ');
  197. @header('Expires: ');
  198. if(in_array($override, array('public', 'private', 'no-cache', 'no-store')))
  199. {
  200. @header("Cache-Control: $override, must-revalidate");
  201. }
  202. else
  203. {
  204. @header('Cache-Control: must-revalidate');
  205. }
  206. }
  207. }
  208. /*
  209. * File and directory operations
  210. */
  211. /**
  212. * Copy recursively from $source to $target.
  213. *
  214. * @param string $source eg. './tmp/latest'
  215. * @param string $target eg. '.'
  216. * @param bool $excludePhp
  217. */
  218. static public function copyRecursive($source, $target, $excludePhp=false )
  219. {
  220. if ( is_dir( $source ) )
  221. {
  222. Piwik_Common::mkdir( $target, false );
  223. $d = dir( $source );
  224. while ( false !== ( $entry = $d->read() ) )
  225. {
  226. if ( $entry == '.' || $entry == '..' )
  227. {
  228. continue;
  229. }
  230. $sourcePath = $source . '/' . $entry;
  231. if ( is_dir( $sourcePath ) )
  232. {
  233. self::copyRecursive( $sourcePath, $target . '/' . $entry, $excludePhp );
  234. continue;
  235. }
  236. $destPath = $target . '/' . $entry;
  237. self::copy($sourcePath, $destPath, $excludePhp);
  238. }
  239. $d->close();
  240. }
  241. else
  242. {
  243. self::copy($source, $target, $excludePhp);
  244. }
  245. }
  246. /**
  247. * Copy individual file from $source to $target.
  248. *
  249. * @param string $source eg. './tmp/latest/index.php'
  250. * @param string $target eg. './index.php'
  251. * @param bool $excludePhp
  252. * @return bool
  253. */
  254. static public function copy($source, $dest, $excludePhp=false)
  255. {
  256. static $phpExtensions = array('php', 'tpl');
  257. if($excludePhp)
  258. {
  259. $path_parts = pathinfo($source);
  260. if(in_array($path_parts['extension'], $phpExtensions))
  261. {
  262. return true;
  263. }
  264. }
  265. if(!@copy( $source, $dest ))
  266. {
  267. @chmod($dest, 0755);
  268. if(!@copy( $source, $dest ))
  269. {
  270. $message = "Error while copying file to <code>$dest</code>. <br />"
  271. . "Please check that the web server has enough permission to overwrite this file. <br />";
  272. if(Piwik_Common::isWindows())
  273. {
  274. $message .= "On Windows, you can try to execute:<br />"
  275. . "<code>cacls ".Piwik_Common::getPathToPiwikRoot()." /t /g ".get_current_user().":f</code><br />";
  276. }
  277. else
  278. {
  279. $message = "For example, on a Linux server, if your Apache httpd user is www-data you can try to execute:<br />"
  280. . "<code>chown -R www-data:www-data ".Piwik_Common::getPathToPiwikRoot()."</code><br />"
  281. . "<code>chmod -R 0755 ".Piwik_Common::getPathToPiwikRoot()."</code><br />";
  282. }
  283. throw new Exception($message);
  284. }
  285. }
  286. return true;
  287. }
  288. /**
  289. * Recursively delete a directory
  290. *
  291. * @param string $dir Directory name
  292. * @param boolean $deleteRootToo Delete specified top-level directory as well
  293. */
  294. static public function unlinkRecursive($dir, $deleteRootToo)
  295. {
  296. if(!$dh = @opendir($dir))
  297. {
  298. return;
  299. }
  300. while (false !== ($obj = readdir($dh)))
  301. {
  302. if($obj == '.' || $obj == '..')
  303. {
  304. continue;
  305. }
  306. if (!@unlink($dir . '/' . $obj))
  307. {
  308. self::unlinkRecursive($dir.'/'.$obj, true);
  309. }
  310. }
  311. closedir($dh);
  312. if ($deleteRootToo)
  313. {
  314. @rmdir($dir);
  315. }
  316. return;
  317. }
  318. /**
  319. * Recursively find pathnames that match a pattern
  320. * @see glob()
  321. *
  322. * @param string $sDir directory
  323. * @param string $sPattern pattern
  324. * @param int $nFlags glob() flags
  325. * @return array
  326. */
  327. public static function globr($sDir, $sPattern, $nFlags = NULL)
  328. {
  329. if(($aFiles = _glob("$sDir/$sPattern", $nFlags)) == false)
  330. {
  331. $aFiles = array();
  332. }
  333. if(($aDirs = _glob("$sDir/*", GLOB_ONLYDIR)) != false)
  334. {
  335. foreach ($aDirs as $sSubDir)
  336. {
  337. $aSubFiles = self::globr($sSubDir, $sPattern, $nFlags);
  338. $aFiles = array_merge($aFiles, $aSubFiles);
  339. }
  340. }
  341. return $aFiles;
  342. }
  343. /**
  344. * Checks that the directories Piwik needs write access are actually writable
  345. * Displays a nice error page if permissions are missing on some directories
  346. *
  347. * @param array $directoriesToCheck Array of directory names to check
  348. */
  349. static public function checkDirectoriesWritableOrDie( $directoriesToCheck = null )
  350. {
  351. $resultCheck = Piwik::checkDirectoriesWritable( $directoriesToCheck );
  352. if( array_search(false, $resultCheck) === false )
  353. {
  354. return;
  355. }
  356. $directoryList = '';
  357. foreach($resultCheck as $dir => $bool)
  358. {
  359. $realpath = Piwik_Common::realpath($dir);
  360. if(!empty($realpath) && $bool === false)
  361. {
  362. if(Piwik_Common::isWindows())
  363. {
  364. $directoryList .= "<code>cacls $realpath /t /g ".get_current_user().":f</code><br />";
  365. }
  366. else
  367. {
  368. $directoryList .= "<code>chmod 0777 $realpath</code><br />";
  369. }
  370. }
  371. }
  372. $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> <p>Try to Execute the following commands on your server:</p>"
  373. . "<blockquote>$directoryList</blockquote>"
  374. . "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 0777 (with your FTP software, right click on the directories, permissions).</p>"
  375. . "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>.</p>"
  376. . "<p>If you need more help, try <a href='?module=Proxy&action=redirect&url=http://piwik.org'>Piwik.org</a>.</p>";
  377. Piwik_ExitWithMessage($directoryMessage, false, true);
  378. }
  379. /**
  380. * Checks if directories are writable and create them if they do not exist.
  381. *
  382. * @param array $directoriesToCheck array of directories to check - if not given default Piwik directories that needs write permission are checked
  383. * @return array directory name => true|false (is writable)
  384. */
  385. static public function checkDirectoriesWritable($directoriesToCheck = null)
  386. {
  387. if( $directoriesToCheck == null )
  388. {
  389. $directoriesToCheck = array(
  390. '/config/',
  391. '/tmp/',
  392. '/tmp/templates_c/',
  393. '/tmp/cache/',
  394. '/tmp/assets/',
  395. '/tmp/latest/',
  396. '/tmp/tcpdf/',
  397. );
  398. }
  399. $resultCheck = array();
  400. foreach($directoriesToCheck as $directoryToCheck)
  401. {
  402. if( !preg_match('/^'.preg_quote(PIWIK_USER_PATH, '/').'/', $directoryToCheck) )
  403. {
  404. $directoryToCheck = PIWIK_USER_PATH . $directoryToCheck;
  405. }
  406. if(!file_exists($directoryToCheck))
  407. {
  408. Piwik_Common::mkdir($directoryToCheck);
  409. }
  410. $directory = Piwik_Common::realpath($directoryToCheck);
  411. $resultCheck[$directory] = false;
  412. if($directory !== false // realpath() returns FALSE on failure
  413. && is_writable($directoryToCheck))
  414. {
  415. $resultCheck[$directory] = true;
  416. }
  417. }
  418. return $resultCheck;
  419. }
  420. /**
  421. * Check if this installation can be auto-updated.
  422. *
  423. * For performance, we look for clues rather than an exhaustive test.
  424. */
  425. static public function canAutoUpdate()
  426. {
  427. if(!is_writable(PIWIK_INCLUDE_PATH . '/') ||
  428. !is_writable(PIWIK_DOCUMENT_ROOT . '/index.php') ||
  429. !is_writable(PIWIK_INCLUDE_PATH . '/core') ||
  430. !is_writable(PIWIK_USER_PATH . '/config/global.ini.php'))
  431. {
  432. return false;
  433. }
  434. return true;
  435. }
  436. /**
  437. * Generate default robots.txt, favicon.ico, etc to suppress
  438. * 404 (Not Found) errors in the web server logs, if Piwik
  439. * is installed in the web root (or top level of subdomain).
  440. *
  441. * @see misc/crossdomain.xml
  442. */
  443. static public function createWebRootFiles()
  444. {
  445. $filesToCreate = array(
  446. '/robots.txt',
  447. '/favicon.ico',
  448. );
  449. foreach($filesToCreate as $file)
  450. {
  451. @file_put_contents(PIWIK_DOCUMENT_ROOT . $file, '');
  452. }
  453. }
  454. /**
  455. * Generate Apache .htaccess files to restrict access
  456. */
  457. static public function createHtAccessFiles()
  458. {
  459. // deny access to these folders
  460. $directoriesToProtect = array(
  461. '/config',
  462. '/core',
  463. '/lang',
  464. '/tmp',
  465. );
  466. foreach($directoriesToProtect as $directoryToProtect)
  467. {
  468. Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect);
  469. }
  470. // more selective allow/deny filters
  471. $allowAny = "<Files \"*\">\nAllow from all\nSatisfy any\n</Files>\n";
  472. $allowStaticAssets = "<Files ~ \"\\.(test\.php|gif|ico|jpg|png|js|css|swf)$\">\nSatisfy any\nAllow from all\n</Files>\n";
  473. $denyDirectPhp = "<Files ~ \"\\.(php|php4|php5|inc|tpl|in)$\">\nDeny from all\n</Files>\n";
  474. $directoriesToProtect = array(
  475. '/js' => $allowAny,
  476. '/libs' => $denyDirectPhp . $allowStaticAssets,
  477. '/plugins' => $denyDirectPhp . $allowStaticAssets,
  478. '/themes' => $denyDirectPhp . $allowStaticAssets,
  479. );
  480. foreach($directoriesToProtect as $directoryToProtect => $content)
  481. {
  482. Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect, $content);
  483. }
  484. }
  485. /**
  486. * Generate IIS web.config files to restrict access
  487. *
  488. * Note: for IIS 7 and above
  489. */
  490. static public function createWebConfigFiles()
  491. {
  492. @file_put_contents(PIWIK_INCLUDE_PATH . '/web.config',
  493. '<?xml version="1.0" encoding="UTF-8"?>
  494. <configuration>
  495. <system.webServer>
  496. <security>
  497. <requestFiltering>
  498. <hiddenSegments>
  499. <add segment="config" />
  500. <add segment="core" />
  501. <add segment="lang" />
  502. <add segment="tmp" />
  503. </hiddenSegments>
  504. <fileExtensions>
  505. <add fileExtension=".tpl" allowed="false" />
  506. <add fileExtension=".php4" allowed="false" />
  507. <add fileExtension=".php5" allowed="false" />
  508. <add fileExtension=".inc" allowed="false" />
  509. <add fileExtension=".in" allowed="false" />
  510. </fileExtensions>
  511. </requestFiltering>
  512. </security>
  513. <directoryBrowse enabled="false" />
  514. <defaultDocument>
  515. <files>
  516. <remove value="index.php" />
  517. <add value="index.php" />
  518. </files>
  519. </defaultDocument>
  520. </system.webServer>
  521. </configuration>');
  522. // deny direct access to .php files
  523. $directoriesToProtect = array(
  524. '/libs',
  525. '/plugins',
  526. );
  527. foreach($directoriesToProtect as $directoryToProtect)
  528. {
  529. @file_put_contents(PIWIK_INCLUDE_PATH . $directoryToProtect . '/web.config',
  530. '<?xml version="1.0" encoding="UTF-8"?>
  531. <configuration>
  532. <system.webServer>
  533. <security>
  534. <requestFiltering>
  535. <denyUrlSequences>
  536. <add sequence=".php" />
  537. </denyUrlSequences>
  538. </requestFiltering>
  539. </security>
  540. </system.webServer>
  541. </configuration>');
  542. }
  543. }
  544. /**
  545. * Get file integrity information (in PIWIK_INCLUDE_PATH).
  546. *
  547. * @return array(bool, string, ...) Return code (true/false), followed by zero or more error messages
  548. */
  549. static public function getFileIntegrityInformation()
  550. {
  551. $messages = array();
  552. $messages[] = true;
  553. // ignore dev environments
  554. if(file_exists(PIWIK_INCLUDE_PATH . '/.svn'))
  555. {
  556. $messages[] = Piwik_Translate('General_WarningFileIntegritySkipped');
  557. return $messages;
  558. }
  559. $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php';
  560. if(!file_exists($manifest))
  561. {
  562. $messages[] = Piwik_Translate('General_WarningFileIntegrityNoManifest');
  563. return $messages;
  564. }
  565. require_once $manifest;
  566. $files = Manifest::$files;
  567. $hasMd5file = function_exists('md5_file');
  568. $hasMd5 = function_exists('md5');
  569. foreach($files as $path => $props)
  570. {
  571. $file = PIWIK_INCLUDE_PATH . '/' . $path;
  572. if(!file_exists($file))
  573. {
  574. $messages[] = Piwik_Translate('General_ExceptionMissingFile', $file);
  575. }
  576. else if(filesize($file) != $props[0])
  577. {
  578. if(!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf')))
  579. {
  580. // files that contain binary data (e.g., images) must match the file size
  581. $messages[] = Piwik_Translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
  582. }
  583. else
  584. {
  585. // convert end-of-line characters and re-test text files
  586. $content = @file_get_contents($file);
  587. $content = str_replace("\r\n", "\n", $content);
  588. if((strlen($content) != $props[0])
  589. || (@md5($content) !== $props[1]))
  590. {
  591. $messages[] = Piwik_Translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
  592. }
  593. }
  594. }
  595. else if($hasMd5file && (@md5_file($file) !== $props[1]))
  596. {
  597. $messages[] = Piwik_Translate('General_ExceptionFileIntegrity', $file);
  598. }
  599. }
  600. if(count($messages) > 1)
  601. {
  602. $messages[0] = false;
  603. }
  604. if(!$hasMd5file)
  605. {
  606. $messages[] = Piwik_Translate('General_WarningFileIntegrityNoMd5file');
  607. }
  608. return $messages;
  609. }
  610. /**
  611. * Test if php output is compressed
  612. *
  613. * @return bool True if php output is (or suspected/likely) to be compressed
  614. */
  615. static public function isPhpOutputCompressed()
  616. {
  617. // Off = ''; On = '1'; otherwise, it's a buffer size
  618. $zlibOutputCompression = ini_get('zlib.output_compression');
  619. // could be ob_gzhandler, ob_deflatehandler, etc
  620. $outputHandler = ini_get('output_handler');
  621. // output handlers can be stacked
  622. $obHandlers = array_filter( ob_list_handlers(), create_function('$var', 'return $var !== "default output handler";') );
  623. // user defined handler via wrapper
  624. $autoPrependFile = ini_get('auto_prepend_file');
  625. $autoAppendFile = ini_get('auto_append_file');
  626. return !empty($zlibOutputCompression) ||
  627. !empty($outputHandler) ||
  628. !empty($obHandlers) ||
  629. !empty($autoPrependFile) ||
  630. !empty($autoAppendFile);
  631. }
  632. /**
  633. * Serve static files through php proxy.
  634. *
  635. * It performs the following actions:
  636. * - Checks the file is readable or returns "HTTP/1.0 404 Not Found"
  637. * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE
  638. * with the modification date of the static file
  639. * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in
  640. * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file
  641. * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension.
  642. * Using manually compressed static files requires you to manually update the compressed file when
  643. * the static file is updated.
  644. * - Overrides server cache control config to allow caching
  645. * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according
  646. * to users encoding capacities.
  647. *
  648. * Warning:
  649. * Compressed filed are stored in the /tmp directory.
  650. * If this method is used with two files bearing the same name but located in different locations,
  651. * there is a risk of conflict. One file could be served with the content of the other.
  652. * A future upgrade of this method would be to recreate the directory structure of the static file
  653. * within a /tmp/compressed-static-files directory.
  654. *
  655. * @param string $file The location of the static file to serve
  656. * @param string $contentType The content type of the static file.
  657. * @param bool $expireFarFuture If set to true, will set Expires: header in far future.
  658. * Should be set to false for files that don't have a cache buster (eg. piwik.js)
  659. */
  660. static public function serveStaticFile($file, $contentType, $expireFarFuture = true)
  661. {
  662. if (file_exists($file))
  663. {
  664. // conditional GET
  665. $modifiedSince = '';
  666. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
  667. {
  668. $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  669. // strip any trailing data appended to header
  670. if (false !== ($semicolon = strpos($modifiedSince, ';')))
  671. {
  672. $modifiedSince = substr($modifiedSince, 0, $semicolon);
  673. }
  674. }
  675. $fileModifiedTime = @filemtime($file);
  676. $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT';
  677. // set HTTP response headers
  678. self::overrideCacheControlHeaders('public');
  679. @header('Vary: Accept-Encoding');
  680. @header('Content-Disposition: inline; filename='.basename($file));
  681. if($expireFarFuture)
  682. {
  683. // Required by proxy caches potentially in between the browser and server to cache the request indeed
  684. @header("Expires: ".gmdate('D, d M Y H:i:s', time() + 86400 * 100) . ' GMT');
  685. }
  686. // Returns 304 if not modified since
  687. if ($modifiedSince === $lastModified)
  688. {
  689. self::setHttpStatus('304 Not Modified');
  690. }
  691. else
  692. {
  693. // optional compression
  694. $compressed = false;
  695. $encoding = '';
  696. $compressedFileLocation = PIWIK_USER_PATH . self::COMPRESSED_FILE_LOCATION . basename($file);
  697. $phpOutputCompressionEnabled = self::isPhpOutputCompressed();
  698. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled)
  699. {
  700. $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
  701. if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents'))
  702. {
  703. if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches))
  704. {
  705. $encoding = 'deflate';
  706. $filegz = $compressedFileLocation .'.deflate';
  707. }
  708. else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches))
  709. {
  710. $encoding = $matches[1];
  711. $filegz = $compressedFileLocation .'.gz';
  712. }
  713. if (!empty($encoding))
  714. {
  715. // compress-on-demand and use cache
  716. if(!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz)))
  717. {
  718. $data = file_get_contents($file);
  719. if ($encoding == 'deflate')
  720. {
  721. $data = gzdeflate($data, 9);
  722. }
  723. else if ($encoding == 'gzip' || $encoding == 'x-gzip')
  724. {
  725. $data = gzencode($data, 9);
  726. }
  727. file_put_contents($filegz, $data);
  728. }
  729. $compressed = true;
  730. $file = $filegz;
  731. }
  732. }
  733. else
  734. {
  735. // manually compressed
  736. $filegz = $compressedFileLocation .'.gz';
  737. if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz)))
  738. {
  739. $encoding = $matches[1];
  740. $compressed = true;
  741. $file = $filegz;
  742. }
  743. }
  744. }
  745. @header('Last-Modified: ' . $lastModified);
  746. if(!$phpOutputCompressionEnabled)
  747. {
  748. @header('Content-Length: ' . filesize($file));
  749. }
  750. if(!empty($contentType))
  751. {
  752. @header('Content-Type: '.$contentType);
  753. }
  754. if($compressed)
  755. {
  756. @header('Content-Encoding: ' . $encoding);
  757. }
  758. if(!_readfile($file))
  759. {
  760. self::setHttpStatus('505 Internal server error');
  761. }
  762. }
  763. }
  764. else
  765. {
  766. self::setHttpStatus('404 Not Found');
  767. }
  768. }
  769. /**
  770. * Create CSV (or other delimited) files
  771. *
  772. * @param string $filePath
  773. * @param array $fileSpec File specifications (delimeter, line terminator, etc)
  774. * @param array $rows Array of array corresponding to rows of values
  775. * @throw Exception if unable to create or write to file
  776. */
  777. static public function createCSVFile($filePath, $fileSpec, $rows)
  778. {
  779. // Set up CSV delimiters, quotes, etc
  780. $delim = $fileSpec['delim'];
  781. $quote = $fileSpec['quote'];
  782. $eol = $fileSpec['eol'];
  783. $null = $fileSpec['null'];
  784. $escapespecial_cb = $fileSpec['escapespecial_cb'];
  785. $fp = @fopen($filePath, 'wb');
  786. if (!$fp)
  787. {
  788. throw new Exception('Error creating the tmp file '.$filePath.', please check that the webserver has write permission to write this file.');
  789. }
  790. foreach ($rows as $row)
  791. {
  792. $output = '';
  793. foreach($row as $value)
  794. {
  795. if(!isset($value) || is_null($value) || $value === false)
  796. {
  797. $output .= $null.$delim;
  798. }
  799. else
  800. {
  801. $output .= $quote.$escapespecial_cb($value).$quote.$delim;
  802. }
  803. }
  804. // Replace delim with eol
  805. $output = substr_replace($output, $eol, -1);
  806. $ret = fwrite($fp, $output);
  807. if (!$ret) {
  808. fclose($fp);
  809. throw new Exception('Error writing to the tmp file '.$filePath);
  810. }
  811. }
  812. fclose($fp);
  813. @chmod($filePath, 0777);
  814. }
  815. /*
  816. * PHP environment settings
  817. */
  818. /**
  819. * Set maximum script execution time.
  820. *
  821. * @param int max execution time in seconds (0 = no limit)
  822. */
  823. static public function setMaxExecutionTime($executionTime)
  824. {
  825. // in the event one or the other is disabled...
  826. @ini_set('max_execution_time', $executionTime);
  827. @set_time_limit($executionTime);
  828. }
  829. /**
  830. * Get php memory_limit (in Megabytes)
  831. *
  832. * Prior to PHP 5.2.1, or on Windows, --enable-memory-limit is not a
  833. * compile-time default, so ini_get('memory_limit') may return false.
  834. *
  835. * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
  836. * @return int|false memory limit in megabytes, or false if there is no limit
  837. */
  838. static public function getMemoryLimitValue()
  839. {
  840. if(($memory = ini_get('memory_limit')) > 0)
  841. {
  842. // handle shorthand byte options (case-insensitive)
  843. $shorthandByteOption = substr($memory, -1);
  844. switch($shorthandByteOption)
  845. {
  846. case 'G':
  847. case 'g':
  848. return substr($memory, 0, -1) * 1024;
  849. case 'M':
  850. case 'm':
  851. return substr($memory, 0, -1);
  852. case 'K':
  853. case 'k':
  854. return substr($memory, 0, -1) / 1024;
  855. }
  856. return $memory / 1048576;
  857. }
  858. // no memory limit
  859. return false;
  860. }
  861. /**
  862. * Set PHP memory limit
  863. *
  864. * Note: system settings may prevent scripts from overriding the master value
  865. *
  866. * @param int $minimumMemoryLimit
  867. * @return bool true if set; false otherwise
  868. */
  869. static public function setMemoryLimit($minimumMemoryLimit)
  870. {
  871. // in Megabytes
  872. $currentValue = self::getMemoryLimitValue();
  873. if( $currentValue === false
  874. || ($currentValue < $minimumMemoryLimit && @ini_set('memory_limit', $minimumMemoryLimit.'M')))
  875. {
  876. return true;
  877. }
  878. return false;
  879. }
  880. /**
  881. * Raise PHP memory limit if below the minimum required
  882. *
  883. * @return bool true if set; false otherwise
  884. */
  885. static public function raiseMemoryLimitIfNecessary()
  886. {
  887. $memoryLimit = self::getMemoryLimitValue();
  888. if($memoryLimit === false)
  889. {
  890. return false;
  891. }
  892. $minimumMemoryLimit = Zend_Registry::get('config')->General->minimum_memory_limit;
  893. if(Piwik_Common::isArchivePhpTriggered()
  894. && Piwik::isUserIsSuperUser())
  895. {
  896. // archive.php: no time limit, high memory limit
  897. self::setMaxExecutionTime(0);
  898. $minimumMemoryLimitWhenArchiving = Zend_Registry::get('config')->General->minimum_memory_limit_when_archiving;
  899. if($memoryLimit < $minimumMemoryLimitWhenArchiving)
  900. {
  901. return self::setMemoryLimit($minimumMemoryLimitWhenArchiving);
  902. }
  903. return false;
  904. }
  905. if($memoryLimit < $minimumMemoryLimit)
  906. {
  907. return self::setMemoryLimit($minimumMemoryLimit);
  908. }
  909. return false;
  910. }
  911. /*
  912. * Logging and error handling
  913. */
  914. /**
  915. * Log a message
  916. *
  917. * @param string $message
  918. */
  919. static public function log($message = '')
  920. {
  921. static $shouldLog = null;
  922. if(is_null($shouldLog))
  923. {
  924. $shouldLog = self::shouldLoggerLog();
  925. // It is possible that the logger is not setup:
  926. // - Tracker request, and debug disabled,
  927. // - and some scheduled tasks call code that tries and log something
  928. try {
  929. Zend_Registry::get('logger_message');
  930. } catch(Exception $e) {
  931. $shouldLog = false;
  932. }
  933. }
  934. if($shouldLog)
  935. {
  936. Zend_Registry::get('logger_message')->logEvent($message);
  937. }
  938. }
  939. static public function shouldLoggerLog()
  940. {
  941. try {
  942. $shouldLog = (Piwik_Common::isPhpCliMode()
  943. || Zend_Registry::get('config')->log->log_only_when_cli == 0)
  944. &&
  945. ( Zend_Registry::get('config')->log->log_only_when_debug_parameter == 0
  946. || isset($_REQUEST['debug']))
  947. ;
  948. } catch(Exception $e) {
  949. $shouldLog = false;
  950. }
  951. return $shouldLog;
  952. }
  953. /**
  954. * Trigger E_USER_ERROR with optional message
  955. *
  956. * @param string $message
  957. */
  958. static public function error($message = '')
  959. {
  960. trigger_error($message, E_USER_ERROR);
  961. }
  962. /**
  963. * Display the message in a nice red font with a nice icon
  964. * ... and dies
  965. *
  966. * @param string $message
  967. */
  968. static public function exitWithErrorMessage( $message )
  969. {
  970. $output = "<style>a{color:red;}</style>\n".
  971. "<div style='color:red;font-family:Georgia;font-size:120%'>".
  972. "<p><img src='themes/default/images/error_medium.png' style='vertical-align:middle; float:left;padding:20 20 20 20' />".
  973. $message.
  974. "</p></div>";
  975. print(Piwik_Log_Formatter_ScreenFormatter::getFormattedString($output));
  976. exit;
  977. }
  978. /*
  979. * Profiling
  980. */
  981. /**
  982. * Get total number of queries
  983. *
  984. * @return int number of queries
  985. */
  986. static public function getQueryCount()
  987. {
  988. $profiler = Zend_Registry::get('db')->getProfiler();
  989. return $profiler->getTotalNumQueries();
  990. }
  991. /**
  992. * Get total elapsed time (in seconds)
  993. *
  994. * @return int elapsed time
  995. */
  996. static public function getDbElapsedSecs()
  997. {
  998. $profiler = Zend_Registry::get('db')->getProfiler();
  999. return $profiler->getTotalElapsedSecs();
  1000. }
  1001. /**
  1002. * Print number of queries and elapsed time
  1003. */
  1004. static public function printQueryCount()
  1005. {
  1006. $totalTime = self::getDbElapsedSecs();
  1007. $queryCount = self::getQueryCount();
  1008. Piwik::log("Total queries = $queryCount (total sql time = ".round($totalTime,2)."s)");
  1009. }
  1010. /**
  1011. * Print profiling report for the tracker
  1012. *
  1013. * @param Piwik_Tracker_Db $db Tracker database object (or null)
  1014. */
  1015. static public function printSqlProfilingReportTracker( $db = null )
  1016. {
  1017. if(!function_exists('maxSumMsFirst'))
  1018. {
  1019. function maxSumMsFirst($a,$b)
  1020. {
  1021. return $a['sum_time_ms'] < $b['sum_time_ms'];
  1022. }
  1023. }
  1024. if(is_null($db))
  1025. {
  1026. $db = Piwik_Tracker::getDatabase();
  1027. }
  1028. $tableName = Piwik_Common::prefixTable('log_profiling');
  1029. $all = $db->fetchAll('SELECT * FROM '.$tableName );
  1030. if($all === false)
  1031. {
  1032. return;
  1033. }
  1034. uasort($all, 'maxSumMsFirst');
  1035. $infoIndexedByQuery = array();
  1036. foreach($all as $infoQuery)
  1037. {
  1038. $query = $infoQuery['query'];
  1039. $count = $infoQuery['count'];
  1040. $sum_time_ms = $infoQuery['sum_time_ms'];
  1041. $infoIndexedByQuery[$query] = array('count' => $count, 'sumTimeMs' => $sum_time_ms);
  1042. }
  1043. Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery);
  1044. }
  1045. /**
  1046. * Outputs SQL Profiling reports
  1047. * It is automatically called when enabling the SQL profiling in the config file enable_sql_profiler
  1048. */
  1049. static function printSqlProfilingReportZend()
  1050. {
  1051. $profiler = Zend_Registry::get('db')->getProfiler();
  1052. if(!$profiler->getEnabled())
  1053. {
  1054. throw new Exception("To display the profiler you should enable enable_sql_profiler on your config/config.ini.php file");
  1055. }
  1056. $infoIndexedByQuery = array();
  1057. foreach($profiler->getQueryProfiles() as $query)
  1058. {
  1059. if(isset($infoIndexedByQuery[$query->getQuery()]))
  1060. {
  1061. $existing = $infoIndexedByQuery[$query->getQuery()];
  1062. }
  1063. else
  1064. {
  1065. $existing = array( 'count' => 0, 'sumTimeMs' => 0);
  1066. }
  1067. $new = array( 'count' => $existing['count'] + 1,
  1068. 'sumTimeMs' => $existing['count'] + $query->getElapsedSecs() * 1000);
  1069. $infoIndexedByQuery[$query->getQuery()] = $new;
  1070. }
  1071. if(!function_exists('sortTimeDesc'))
  1072. {
  1073. function sortTimeDesc($a,$b)
  1074. {
  1075. return $a['sumTimeMs'] < $b['sumTimeMs'];
  1076. }
  1077. }
  1078. uasort( $infoIndexedByQuery, 'sortTimeDesc');
  1079. $str = '<hr /><b>SQL Profiler</b><hr /><b>Summary</b><br/>';
  1080. $totalTime = $profiler->getTotalElapsedSecs();
  1081. $queryCount = $profiler->getTotalNumQueries();
  1082. $longestTime = 0;
  1083. $longestQuery = null;
  1084. foreach ($profiler->getQueryProfiles() as $query) {
  1085. if ($query->getElapsedSecs() > $longestTime) {
  1086. $longestTime = $query->getElapsedSecs();
  1087. $longestQuery = $query->getQuery();
  1088. }
  1089. }
  1090. $str .= 'Executed ' . $queryCount . ' queries in ' . round($totalTime,3) . ' seconds';
  1091. $str .= '(Average query length: ' . round($totalTime / $queryCount,3) . ' seconds)';
  1092. $str .= '<br />Queries per second: ' . round($queryCount / $totalTime,1) ;
  1093. $str .= '<br />Longest query length: ' . round($longestTime,3) . " seconds (<code>$longestQuery</code>)";
  1094. Piwik::log($str);
  1095. Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery);
  1096. }
  1097. /**
  1098. * Log a breakdown by query
  1099. *
  1100. * @param array $infoIndexedByQuery
  1101. */
  1102. static private function getSqlProfilingQueryBreakdownOutput( $infoIndexedByQuery )
  1103. {
  1104. $output = '<hr /><b>Breakdown by query</b><br/>';
  1105. foreach($infoIndexedByQuery as $query => $queryInfo)
  1106. {
  1107. $timeMs = round($queryInfo['sumTimeMs'],1);
  1108. $count = $queryInfo['count'];
  1109. $avgTimeString = '';
  1110. if($count > 1)
  1111. {
  1112. $avgTimeMs = $timeMs / $count;
  1113. $avgTimeString = " (average = <b>". round($avgTimeMs,1) . "ms</b>)";
  1114. }
  1115. $query = preg_replace('/([\t\n\r ]+)/', ' ', $query);
  1116. $output .= "Executed <b>$count</b> time". ($count==1?'':'s') ." in <b>".$timeMs."ms</b> $avgTimeString <pre>\t$query</pre>";
  1117. }
  1118. Piwik::log($output);
  1119. }
  1120. /**
  1121. * Print timer
  1122. */
  1123. static public function printTimer()
  1124. {
  1125. Piwik::log(Zend_Registry::get('timer'));
  1126. }
  1127. /**
  1128. * Print memory leak
  1129. *
  1130. * @param string $prefix
  1131. * @param string $suffix
  1132. */
  1133. static public function printMemoryLeak($prefix = '', $suffix = '<br />')
  1134. {
  1135. echo $prefix;
  1136. echo Zend_Registry::get('timer')->getMemoryLeak();
  1137. echo $suffix;
  1138. }
  1139. /**
  1140. * Print memory usage
  1141. *
  1142. * @param string $prefixString
  1143. */
  1144. static public function getMemoryUsage()
  1145. {
  1146. $memory = false;
  1147. if(function_exists('xdebug_memory_usage'))
  1148. {
  1149. $memory = xdebug_memory_usage();
  1150. }
  1151. elseif(function_exists('memory_get_usage'))
  1152. {
  1153. $memory = memory_get_usage();
  1154. }
  1155. if($memory === false)
  1156. {
  1157. return "Memory usage function not found.";
  1158. }
  1159. $usage = number_format( round($memory / 1024 / 1024, 2), 2);
  1160. return "$usage Mb";
  1161. }
  1162. /*
  1163. * Amounts, Percentages, Currency, Time, Math Operations, and Pretty Printing
  1164. */
  1165. /**
  1166. * Returns a list of currency symbols
  1167. *
  1168. * @return array array( currencyCode => symbol, ... )
  1169. */
  1170. static public function getCurrencyList()
  1171. {
  1172. static $currenciesList = null;
  1173. if(is_null($currenciesList))
  1174. {
  1175. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php';
  1176. $currenciesList = $GLOBALS['Piwik_CurrencyList'];
  1177. }
  1178. return $currenciesList;
  1179. }
  1180. /**
  1181. * Computes the division of i1 by i2. If either i1 or i2 are not number, or if i2 has a value of zero
  1182. * we return 0 to avoid the division by zero.
  1183. *
  1184. * @param numeric $i1
  1185. * @param numeric $i2
  1186. * @return numeric The result of the division or zero
  1187. */
  1188. static public function secureDiv( $i1, $i2 )
  1189. {
  1190. if ( is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0)
  1191. {
  1192. return $i1 / $i2;
  1193. }
  1194. return 0;
  1195. }
  1196. /**
  1197. * Safely compute a percentage. Return 0 to avoid division by zero.
  1198. *
  1199. * @param numeric $dividend
  1200. * @param numeric $divisor
  1201. * @param int $precision
  1202. * @return numeric
  1203. */
  1204. static public function getPercentageSafe($dividend, $divisor, $precision = 0)
  1205. {
  1206. if($divisor == 0)
  1207. {
  1208. return 0;
  1209. }
  1210. return round(100 * $dividend / $divisor, $precision);
  1211. }
  1212. /**
  1213. * Get currency symbol for a site
  1214. *
  1215. * @param int $idSite
  1216. * @return string
  1217. */
  1218. static public function getCurrency($idSite)
  1219. {
  1220. $symbols = self::getCurrencyList();
  1221. $site = new Piwik_Site($idSite);
  1222. $currency = $site->getCurrency();
  1223. if(isset($symbols[$currency]))
  1224. {
  1225. return $symbols[$currency][0];
  1226. }
  1227. return '';
  1228. }
  1229. /**
  1230. * For the given value, based on the column name, will apply: pretty time, pretty money
  1231. * @param int $idSite
  1232. * @param string $columnName
  1233. * @param mixed $value
  1234. * @param bool $htmlAllowed
  1235. * @param string $timeAsSentence
  1236. * @return string
  1237. */
  1238. static public function getPrettyValue($idSite, $columnName, $value, $htmlAllowed, $timeAsSentence)
  1239. {
  1240. // Display time in human readable
  1241. if(strpos($columnName, 'time') !== false)
  1242. {
  1243. return Piwik::getPrettyTimeFromSeconds($value, $timeAsSentence);
  1244. }
  1245. // Add revenue symbol to revenues
  1246. if(strpos($columnName, 'revenue') !== false)
  1247. {
  1248. return Piwik::getPrettyMoney($value, $idSite, $htmlAllowed);
  1249. }
  1250. // Add % symbol to rates
  1251. if(strpos($columnName, '_rate') !== false)
  1252. {
  1253. if(strpos($value, "%") === false)
  1254. {
  1255. return $value . "%";
  1256. }
  1257. }
  1258. return $value;
  1259. }
  1260. /**
  1261. * Pretty format monetary value for a site
  1262. *
  1263. * @param numeric|string $value
  1264. * @param int $idSite
  1265. * @return string
  1266. */
  1267. static public function getPrettyMoney($value, $idSite, $htmlAllowed = true)
  1268. {
  1269. $currencyBefore = self::getCurrency($idSite);
  1270. $space = ' ';
  1271. if($htmlAllowed)
  1272. {
  1273. $space = '&nbsp;';
  1274. }
  1275. $currencyAfter = '';
  1276. // manually put the currency symbol after the amount for euro
  1277. // (maybe more currencies prefer this notation?)
  1278. if(in_array($currencyBefore,array('€')))
  1279. {
  1280. $currencyAfter = $space.$currencyBefore;
  1281. $currencyBefore = '';
  1282. }
  1283. // if the input is a number (it could be a string or INPUT form),
  1284. // and if this number is not an int, we round to precision 2
  1285. if(is_numeric($value))
  1286. {
  1287. if($value == round($value))
  1288. {
  1289. // 0.0 => 0
  1290. $value = round($value);
  1291. }
  1292. else
  1293. {
  1294. $precision = Piwik_Tracker_GoalManager::REVENUE_PRECISION;
  1295. $value = sprintf( "%01.".$precision."f", $value);
  1296. }
  1297. }
  1298. $prettyMoney = $currencyBefore . $space . $value . $currencyAfter;
  1299. return $prettyMoney;
  1300. }
  1301. /**
  1302. * Pretty format a memory size value
  1303. *
  1304. * @param numeric $size in bytes
  1305. * @return string
  1306. */
  1307. static public function getPrettySizeFromBytes($size)
  1308. {
  1309. $bytes = array('','K','M','G','T');
  1310. foreach($bytes as $val)
  1311. {
  1312. if($size > 1024)
  1313. {
  1314. $size = $size / 1024;
  1315. }
  1316. else
  1317. {
  1318. break;
  1319. }
  1320. }
  1321. return round($size, 1)." ".$val;
  1322. }
  1323. /**
  1324. * Pretty format a time
  1325. *
  1326. * @param numeric $numberOfSeconds
  1327. * @param bool If set to true, will output "5min 17s", if false "00:05:17"
  1328. * @return string
  1329. */
  1330. static public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true)
  1331. {
  1332. $numberOfSeconds = (int)$numberOfSeconds;
  1333. // Display 01:45:17 time format
  1334. if($displayTimeAsSentence === false)
  1335. {
  1336. $hours = floor( $numberOfSeconds / 3600);
  1337. $minutes = floor( ($reminder = ($numberOfSeconds - $hours * 3600)) / 60 );
  1338. $seconds = $reminder - $minutes * 60;
  1339. return sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) .':'. sprintf("%02s", $seconds);
  1340. }
  1341. $secondsInYear = 86400 * 365.25;
  1342. $years = floor($numberOfSeconds / $secondsInYear);
  1343. $minusYears = $numberOfSeconds - $years * $secondsInYear;
  1344. $days = floor($minusYears / 86400);
  1345. $minusDays = $numberOfSeconds - $days * 86400;
  1346. $hours = floor($minusDays / 3600);
  1347. $minusDaysAndHours = $minusDays - $hours * 3600;
  1348. $minutes = floor($minusDaysAndHours / 60 );
  1349. $seconds = $minusDaysAndHours - $minutes * 60;
  1350. if($years > 0)
  1351. {
  1352. $return = sprintf(Piwik_Translate('General_YearsDays'), $years, $days);
  1353. }
  1354. elseif($days > 0)
  1355. {
  1356. $return = sprintf(Piwik_Translate('General_DaysHours'), $days, $hours);
  1357. }
  1358. elseif($hours > 0)
  1359. {
  1360. $return = sprintf(Piwik_Translate('General_HoursMinutes'), $hours, $minutes);
  1361. }
  1362. elseif($minutes > 0)
  1363. {
  1364. $return = sprintf(Piwik_Translate('General_MinutesSeconds'), $minutes, $seconds);
  1365. }
  1366. else
  1367. {
  1368. $return = sprintf(Piwik_Translate('General_Seconds'), $seconds);
  1369. }
  1370. if($isHtml)
  1371. {
  1372. return str_replace(' ', '&nbsp;', $return);
  1373. }
  1374. return $return;
  1375. }
  1376. /**
  1377. * Returns the Javascript code to be inserted on every page to track
  1378. *
  1379. * @param int $idSite
  1380. * @param string $piwikUrl http://path/to/piwik/directory/
  1381. * @return string
  1382. */
  1383. static public function getJavascriptCode($idSite, $piwikUrl)
  1384. {
  1385. $jsCode = file_get_contents( PIWIK_INCLUDE_PATH . "/core/Tracker/javascriptCode.tpl");
  1386. $jsCode = nl2br(htmlentities($jsCode));
  1387. $piwikUrl = preg_match('~^(http|https)://(.*)$~D', $piwikUrl, $matches);
  1388. $piwikUrl = @$matches[2];
  1389. $jsCode = str_replace('{$idSite}', $idSite, $jsCode);
  1390. $jsCode = str_replace('{$piwikUrl}', Piwik_Common::sanitizeInputValue($piwikUrl), $jsCode);
  1391. $jsCode = str_replace('{$hrefTitle}', Piwik::getRandomTitle(), $jsCode);
  1392. return $jsCode;
  1393. }
  1394. /**
  1395. * Generate a title for image tags
  1396. *
  1397. * @return string
  1398. */
  1399. static public function getRandomTitle()
  1400. {
  1401. static $titles = array(
  1402. 'Web analytics',
  1403. 'Real Time Web Analytics',
  1404. 'Analytics',
  1405. 'Real Time Analytics',
  1406. 'Open Source Analytics',
  1407. 'Open Source Web Analytics',
  1408. 'Free Website Analytics',
  1409. 'Free Web Analytics',
  1410. );
  1411. $id = abs(intval(md5(Piwik_Url::getCurrentHost())));
  1412. $title = $titles[ $id % count($titles)];
  1413. return $title;
  1414. }
  1415. /**
  1416. * Number of websites to show in the Website selector
  1417. *
  1418. * @return int
  1419. */
  1420. static public function getWebsitesCountToDisplay()
  1421. {
  1422. $count = max(Zend_Registry::get('config')->General->site_selector_max_sites,
  1423. Zend_Registry::get('config')->General->autocomplete_min_sites);
  1424. return (int)$count;
  1425. }
  1426. /**
  1427. * Segments to pre-process
  1428. */
  1429. static public function getKnownSegmentsToArchive()
  1430. {
  1431. static $cachedResult = null;
  1432. if (is_null($cachedResult))
  1433. {
  1434. $segments = Zend_Registry::get('config')->Segments->toArray();
  1435. $cachedResult = isset($segments['Segments']) ? $segments['Segments'] : '';
  1436. }
  1437. return $cachedResult;
  1438. }
  1439. /*
  1440. * Access
  1441. */
  1442. /**
  1443. * Get current user email address
  1444. *
  1445. * @return string
  1446. */
  1447. static public function getCurrentUserEmail()
  1448. {
  1449. if(!Piwik::isUserIsSuperUser())
  1450. {
  1451. $user = Piwik_UsersManager_API::getInstance()->getUser(Piwik::getCurrentUserLogin());
  1452. return $user['email'];
  1453. }
  1454. return self::getSuperUserEmail();
  1455. }
  1456. /**
  1457. * Returns Super User email
  1458. *
  1459. * @return string
  1460. */
  1461. static public function getSuperUserEmail()
  1462. {
  1463. $superuser = Zend_Registry::get('config')->superuser;
  1464. return $superuser->email;
  1465. }
  1466. /**
  1467. * Get current user login
  1468. *
  1469. * @return string login ID
  1470. */
  1471. static public function getCurrentUserLogin()
  1472. {
  1473. return Zend_Registry::get('access')->getLogin();
  1474. }
  1475. /**
  1476. * Get current user's token auth
  1477. *
  1478. * @return string Token auth
  1479. */
  1480. static public function getCurrentUserTokenAuth()
  1481. {
  1482. return Zend_Registry::get('access')->getTokenAuth();
  1483. }
  1484. /**
  1485. * Returns true if the current user is either the super user, or the user $theUser
  1486. * Used when modifying user preference: this usually requires super user or being the user itself.
  1487. *
  1488. * @param string $theUser
  1489. * @return bool
  1490. */
  1491. static public function isUserIsSuperUserOrTheUser( $theUser )
  1492. {
  1493. try{
  1494. self::checkUserIsSuperUserOrTheUser( $theUser );
  1495. return true;
  1496. } catch( Exception $e){
  1497. return false;
  1498. }
  1499. }
  1500. /**
  1501. * Check that current user is either the specified user or the superuser
  1502. *
  1503. * @param string $theUser
  1504. * @throws exception if the user is neither the super user nor the user $theUser
  1505. */
  1506. static public function checkUserIsSuperUserOrTheUser( $theUser )
  1507. {
  1508. try{
  1509. if( Piwik::getCurrentUserLogin() !== $theUser)
  1510. {
  1511. // or to the super user
  1512. Piwik::checkUserIsSuperUser();
  1513. }
  1514. } catch( Piwik_Access_NoAccessException $e){
  1515. throw new Piwik_Access_NoAccessException("The user has to be either the Super User or the user '$theUser' itself.");
  1516. }
  1517. }
  1518. /**
  1519. * Returns true if the current user is the Super User
  1520. *
  1521. * @return bool
  1522. */
  1523. static public function isUserIsSuperUser()
  1524. {
  1525. try{
  1526. self::checkUserIsSuperUser();
  1527. return true;
  1528. } catch( Exception $e){
  1529. return false;
  1530. }
  1531. }
  1532. /**
  1533. * Is user the anonymous user?
  1534. *
  1535. * @return bool True if anonymouse; false otherwise
  1536. */
  1537. static public function isUserIsAnonymous()
  1538. {
  1539. return Piwik::getCurrentUserLogin() == 'anonymous';
  1540. }
  1541. /**
  1542. * Checks if user is not the anonymous user.
  1543. *
  1544. * @throws Exception if user is anonymous.
  1545. */
  1546. static public function checkUserIsNotAnonymous()
  1547. {
  1548. if(self::isUserIsAnonymous())
  1549. {
  1550. throw new Exception(Piwik_Translate('General_YouMustBeLoggedIn'));
  1551. }
  1552. }
  1553. /**
  1554. * Helper method user to set the current as Super User.
  1555. * This should be used with great care as this gives the user all permissions.
  1556. *
  1557. * @param bool True to set current user as super user
  1558. */
  1559. static public function setUserIsSuperUser( $bool = true )
  1560. {
  1561. Zend_Registry::get('access')->setSuperUser($bool);
  1562. }
  1563. /**
  1564. * Check that user is the superuser
  1565. *
  1566. * @throws Exception if not the superuser
  1567. */
  1568. static public function checkUserIsSuperUser()
  1569. {
  1570. Zend_Registry::get('access')->checkUserIsSuperUser();
  1571. }
  1572. /**
  1573. * Returns true if the user has admin access to the sites
  1574. *
  1575. * @param mixed $idSites
  1576. * @return bool
  1577. */
  1578. static public function isUserHasAdminAccess( $idSites )
  1579. {
  1580. try{
  1581. self::checkUserHasAdminAccess( $idSites );
  1582. return true;
  1583. } catch( Exception $e){
  1584. return false;
  1585. }
  1586. }
  1587. /**
  1588. * Check user has admin access to the sites
  1589. *
  1590. * @param mixed $idSites
  1591. * @throws Exception if user doesn't have admin access to the sites
  1592. */
  1593. static public function checkUserHasAdminAccess( $idSites )
  1594. {
  1595. Zend_Registry::get('access')->checkUserHasAdminAccess( $idSites );
  1596. }
  1597. /**
  1598. * Returns true if the user has admin access to any sites
  1599. *
  1600. * @return bool
  1601. */
  1602. static public function isUserHasSomeAdminAccess()
  1603. {
  1604. try{
  1605. self::checkUserHasSomeAdminAccess();
  1606. return true;
  1607. } catch( Exception $e){
  1608. return false;
  1609. }
  1610. }
  1611. /**
  1612. * Check user has admin access to any sites
  1613. *
  1614. * @throws Exception if user doesn't have admin access to any sites
  1615. */
  1616. static public function checkUserHasSomeAdminAccess()
  1617. {
  1618. Zend_Registry::get('access')->checkUserHasSomeAdminAccess();
  1619. }
  1620. /**
  1621. * Returns true if the user has view access to the sites
  1622. *
  1623. * @param mixed $idSites
  1624. * @return bool
  1625. */
  1626. static public function isUserHasViewAccess( $idSites )
  1627. {
  1628. try{
  1629. self::checkUserHasViewAccess( $idSites );
  1630. return true;
  1631. } catch( Exception $e){
  1632. return false;
  1633. }
  1634. }
  1635. /**
  1636. * Check user has view access to the sites
  1637. *
  1638. * @param mixed $idSites
  1639. * @throws Exception if user doesn't have view access to sites
  1640. */
  1641. static public function checkUserHasViewAccess( $idSites )
  1642. {
  1643. Zend_Registry::get('access')->checkUserHasViewAccess( $idSites );
  1644. }
  1645. /**
  1646. * Returns true if the user has view access to any sites
  1647. *
  1648. * @return bool
  1649. */
  1650. static public function isUserHasSomeViewAccess()
  1651. {
  1652. try{
  1653. self::checkUserHasSomeViewAccess();
  1654. return true;
  1655. } catch( Exception $e){
  1656. return false;
  1657. }
  1658. }
  1659. /**
  1660. * Check user has view access to any sites
  1661. *
  1662. * @throws Exception if user doesn't have view access to any sites
  1663. */
  1664. static public function checkUserHasSomeViewAccess()
  1665. {
  1666. Zend_Registry::get('access')->checkUserHasSomeViewAccess();
  1667. }
  1668. /*
  1669. * Current module, action, plugin
  1670. */
  1671. /**
  1672. * Returns the name of the Login plugin currently being used.
  1673. * Must be used since it is not allowed to hardcode 'Login' in URLs
  1674. * in case another Login plugin is being used.
  1675. *
  1676. * @return string
  1677. */
  1678. static public function getLoginPluginName()
  1679. {
  1680. return Zend_Registry::get('auth')->getName();
  1681. }
  1682. /**
  1683. * Returns the plugin currently being used to display the page
  1684. *
  1685. * @return Piwik_Plugin
  1686. */
  1687. static public function getCurrentPlugin()
  1688. {
  1689. return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule());
  1690. }
  1691. /**
  1692. * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.)
  1693. *
  1694. * @return string
  1695. */
  1696. static public function getModule()
  1697. {
  1698. return Piwik_Common::getRequestVar('module', '', 'string');
  1699. }
  1700. /**
  1701. * Returns the current action read from the URL
  1702. *
  1703. * @return string
  1704. */
  1705. static public function getAction()
  1706. {
  1707. return Piwik_Common::getRequestVar('action', '', 'string');
  1708. }
  1709. /**
  1710. * Helper method used in API function to introduce array elements in API parameters.
  1711. * Array elements can be passed by comma separated values, or using the notation
  1712. * array[]=value1&array[]=value2 in the URL.
  1713. * This function will handle both cases and return the array.
  1714. *
  1715. * @param array|string $columns String or array
  1716. * @return array
  1717. */
  1718. static public function getArrayFromApiParameter($columns)
  1719. {
  1720. return $columns === false
  1721. ? array()
  1722. : (is_array($columns)
  1723. ? $columns
  1724. : explode(',', $columns)
  1725. );
  1726. }
  1727. /**
  1728. * Redirect to module (and action)
  1729. *
  1730. * @param string $newModule Target module
  1731. * @param string $newAction Target action
  1732. * @param array $parameters Parameters to modify in the URL
  1733. * @return bool false if the URL to redirect to is already this URL
  1734. */
  1735. static public function redirectToModule( $newModule, $newAction = '', $parameters = array() )
  1736. {
  1737. $newUrl = 'index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified(
  1738. array('module' => $newModule, 'action' => $newAction)
  1739. + $parameters
  1740. );
  1741. Piwik_Url::redirectToUrl($newUrl);
  1742. }
  1743. /*
  1744. * Global database object
  1745. */
  1746. /**
  1747. * Create database object and connect to database
  1748. */
  1749. static public function createDatabaseObject( $dbInfos = null )
  1750. {
  1751. $config = Zend_Registry::get('config');
  1752. if(is_null($dbInfos))
  1753. {
  1754. $dbInfos = $config->database->toArray();
  1755. }
  1756. $dbInfos['profiler'] = $config->Debug->enable_sql_profiler;
  1757. $db = null;
  1758. Piwik_PostEvent('Reporting.createDatabase', $db);
  1759. if(is_null($db))
  1760. {
  1761. $adapter = $dbInfos['adapter'];
  1762. $db = @Piwik_Db_Adapter::factory($adapter, $dbInfos);
  1763. }
  1764. Zend_Registry::set('db', $db);
  1765. }
  1766. /**
  1767. * Disconnect from database
  1768. */
  1769. static public function disconnectDatabase()
  1770. {
  1771. Zend_Registry::get('db')->closeConnection();
  1772. }
  1773. /**
  1774. * Checks the database server version against the required minimum
  1775. * version.
  1776. *
  1777. * @see config/global.ini.php
  1778. * @since 0.4.4
  1779. * @throws Exception if server version is less than the required version
  1780. */
  1781. static public function checkDatabaseVersion()
  1782. {
  1783. Zend_Registry::get('db')->checkServerVersion();
  1784. }
  1785. /**
  1786. * Check database connection character set is utf8.
  1787. *
  1788. * @return bool True if it is (or doesn't matter); false otherwise
  1789. */
  1790. static public function isDatabaseConnectionUTF8()
  1791. {
  1792. return Zend_Registry::get('db')->isConnectionUTF8();
  1793. }
  1794. /*
  1795. * Global log object
  1796. */
  1797. /**
  1798. * Create log object
  1799. */
  1800. static public function createLogObject()
  1801. {
  1802. $configAPI = Zend_Registry::get('config')->log;
  1803. $aLoggers = array(