Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbenakamoorthi <benaka.moorthi@gmail.com>2012-11-27 02:14:37 +0400
committerbenakamoorthi <benaka.moorthi@gmail.com>2012-11-27 02:14:37 +0400
commitdc98a971829c9ed278f79e759ba41b51881ba2fc (patch)
tree8acbffb943baa97c66ea954099e86fcdf243351a
parent4caab2461a50d6d4f81435cf52816a43525780ff (diff)
Fixes #3456, added updating scheduled task for GeoIP databases w/ manager UI and easy-install for GeoLiteCity.
Notes: * Added new PEAR lib Archive_Tar and new Unzip implmentations for .tar.gz, .tar.bz2 & .gz files. * Modified Http class to allow use of Range HTTP header. * Added ability to download file in chunks to Http class. * Moved GeoIP admin page styles to separate CSS file. * Allowed monthly scheduled tasks to specify day of week to run on. git-svn-id: http://dev.piwik.org/svn/trunk@7550 59fd770c-687e-43c8-a1e3-f5a4ff64c105
-rw-r--r--core/Http.php275
-rw-r--r--core/ScheduledTime/Monthly.php48
-rw-r--r--core/Unzip.php12
-rwxr-xr-xcore/Unzip/Gzip.php87
-rwxr-xr-xcore/Unzip/Tar.php86
-rw-r--r--lang/en.php24
-rwxr-xr-xlibs/Archive_Tar/Tar.php1970
-rw-r--r--plugins/UserCountry/Controller.php229
-rwxr-xr-xplugins/UserCountry/GeoIPAutoUpdater.php397
-rwxr-xr-xplugins/UserCountry/LocationProvider/GeoIp.php29
-rw-r--r--plugins/UserCountry/UserCountry.php39
-rwxr-xr-xplugins/UserCountry/templates/admin.js140
-rwxr-xr-xplugins/UserCountry/templates/adminIndex.tpl42
-rwxr-xr-xplugins/UserCountry/templates/styles.css69
-rwxr-xr-xplugins/UserCountry/templates/updaterSetup.tpl53
-rw-r--r--tests/PHPUnit/Core/HttpTest.php65
-rw-r--r--tests/PHPUnit/Core/ScheduledTimeTest.php21
-rwxr-xr-xtests/PHPUnit/Core/Unzip/test.gzbin0 -> 41 bytes
-rwxr-xr-xtests/PHPUnit/Core/Unzip/test.tar.gzbin0 -> 202 bytes
-rw-r--r--tests/PHPUnit/Core/UnzipTest.php45
-rw-r--r--themes/default/common.css1
21 files changed, 3599 insertions, 33 deletions
diff --git a/core/Http.php b/core/Http.php
index ff64da6898..2ba6943388 100644
--- a/core/Http.php
+++ b/core/Http.php
@@ -53,10 +53,15 @@ class Piwik_Http
* @param string $destinationPath
* @param int $followDepth
* @param bool $acceptLanguage
+ * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024)
+ * Doesn't work w/ fopen method.
+ * @param bool $getExtendedInfo True to return status code, headers & response, false if just response.
+ * @param string $httpMethod The HTTP method to use. Defaults to 'GET'.
* @throws Exception
* @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx)
*/
- static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false)
+ static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false, $byteRange = false, $getExtendedInfo = false, $httpMethod = 'GET'
+)
{
// create output file
$file = null;
@@ -71,7 +76,7 @@ class Piwik_Http
}
$acceptLanguage = $acceptLanguage ? 'Accept-Language: '.$acceptLanguage : '';
- return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage);
+ return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod);
}
/**
@@ -86,10 +91,27 @@ class Piwik_Http
* @param int $followDepth
* @param bool|string $acceptLanguage Accept-language header
* @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked
+ * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024)
+ * Doesn't work w/ fopen method.
+ * @param bool $getExtendedInfo True to return status code, headers & response, false if just response.
+ * @param string $httpMethod The HTTP method to use. Defaults to 'GET'.
* @throws Exception
- * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx)
+ * @return bool true (or string/array) on success; false on HTTP response error code (1xx or 4xx)
*/
- static public function sendHttpRequestBy($method = 'socket', $aUrl, $timeout, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = false, $acceptInvalidSslCertificate = false)
+ static public function sendHttpRequestBy(
+ $method = 'socket',
+ $aUrl,
+ $timeout,
+ $userAgent = null,
+ $destinationPath = null,
+ $file = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $acceptInvalidSslCertificate = false,
+ $byteRange = false,
+ $getExtendedInfo = false,
+ $httpMethod = 'GET'
+ )
{
if ($followDepth > 5)
{
@@ -114,11 +136,22 @@ class Piwik_Http
. Piwik_Version::VERSION . ' '
. ($userAgent ? " ($userAgent)" : '');
+ // range header
+ $rangeHeader = '';
+ if (!empty($byteRange))
+ {
+ $rangeHeader = 'Range: bytes='.$byteRange[0].'-'.$byteRange[1]."\r\n";
+ }
+
// proxy configuration
$proxyHost = Piwik_Config::getInstance()->proxy['host'];
$proxyPort = Piwik_Config::getInstance()->proxy['port'];
$proxyUser = Piwik_Config::getInstance()->proxy['username'];
$proxyPassword = Piwik_Config::getInstance()->proxy['password'];
+
+ // other result data
+ $status = null;
+ $headers = array();
if($method == 'socket')
{
@@ -142,6 +175,16 @@ class Piwik_Http
}
$errno = null;
$errstr = null;
+
+ if ((!empty($proxyHost) && !empty($proxyPort))
+ || !empty($byteRange))
+ {
+ $httpVer = '1.1';
+ }
+ else
+ {
+ $httpVer = '1.0';
+ }
$proxyAuth = null;
if(!empty($proxyHost) && !empty($proxyPort))
@@ -152,13 +195,13 @@ class Piwik_Http
{
$proxyAuth = 'Proxy-Authorization: Basic '.base64_encode("$proxyUser:$proxyPassword") ."\r\n";
}
- $requestHeader = "GET $aUrl HTTP/1.1\r\n";
+ $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n";
}
else
{
$connectHost = $host;
$connectPort = $port;
- $requestHeader = "GET $path HTTP/1.0\r\n";
+ $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n";
}
// connection attempt
@@ -176,6 +219,7 @@ class Piwik_Http
. ($acceptLanguage ? $acceptLanguage ."\r\n" : '')
.$xff."\r\n"
.$via."\r\n"
+ .$rangeHeader
."Connection: close\r\n"
."\r\n";
fwrite($fsock, $requestHeader);
@@ -232,7 +276,15 @@ class Piwik_Http
{
if(is_resource($file)) { @fclose($file); }
@fclose($fsock);
- return false;
+
+ if (!$getExtendedInfo)
+ {
+ return false;
+ }
+ else
+ {
+ return array('status' => $status);
+ }
}
continue;
@@ -248,7 +300,20 @@ class Piwik_Http
{
throw new Exception('Unexpected redirect to Location: '.rtrim($line).' for status code '.$status);
}
- return self::sendHttpRequestBy($method, trim($m[1]), $timeout, $userAgent, $destinationPath, $file, $followDepth+1, $acceptLanguage);
+ return self::sendHttpRequestBy(
+ $method,
+ trim($m[1]),
+ $timeout,
+ $userAgent,
+ $destinationPath,
+ $file,
+ $followDepth+1,
+ $acceptLanguage,
+ $acceptInvalidSslCertificate = false,
+ $byteRange,
+ $getExtendedInfo,
+ $httpMethod
+ );
}
// save expected content length for later verification
@@ -256,9 +321,12 @@ class Piwik_Http
{
$contentLength = (integer) $m[1];
}
+
+ self::parseHeaderLine($headers, $line);
}
- if(feof($fsock))
+ if (feof($fsock)
+ && $httpMethod != 'HEAD')
{
throw new Exception('Unexpected end of transmission');
}
@@ -312,7 +380,8 @@ class Piwik_Http
'header' => 'User-Agent: '.$userAgent."\r\n"
.($acceptLanguage ? $acceptLanguage."\r\n" : '')
.$xff."\r\n"
- .$via."\r\n",
+ .$via."\r\n"
+ .$rangeHeader,
'max_redirects' => 5, // PHP 5.1.0
'timeout' => $timeout, // PHP 5.2.1
)
@@ -379,9 +448,11 @@ class Piwik_Http
CURLOPT_HTTPHEADER => array(
$xff,
$via,
+ $rangeHeader,
$acceptLanguage
),
- CURLOPT_HEADER => false,
+ // only get header info if not saving directly to file
+ CURLOPT_HEADER => is_resource($file) ? false : true,
CURLOPT_CONNECTTIMEOUT => $timeout,
);
// Case archive.php is triggering archiving on https:// and the certificate is not valid
@@ -392,6 +463,11 @@ class Piwik_Http
CURLOPT_SSL_VERIFYPEER => false,
);
}
+ @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod);
+ if ($httpMethod == 'HEAD')
+ {
+ @curl_setopt($ch, CURLOPT_NOBODY, true);
+ }
@curl_setopt_array($ch, $curl_options);
self::configCurlCertificate($ch);
@@ -439,9 +515,24 @@ class Piwik_Http
}
$response = '';
}
+ else
+ {
+ // redirects are included in the output html, so we look for the last line that starts w/ HTTP/...
+ // to split the response
+ while (substr($response, 0, 5) == "HTTP/")
+ {
+ list($header, $response) = explode("\r\n\r\n", $response, 2);
+ }
+
+ foreach (explode("\r\n", $header) as $line)
+ {
+ self::parseHeaderLine($headers, $line);
+ }
+ }
$contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
$fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response);
+ $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE);
@curl_close($ch);
unset($ch);
@@ -464,11 +555,134 @@ class Piwik_Http
return true;
}
- if(($contentLength > 0) && ($fileLength != $contentLength))
+ if ($contentLength > 0
+ && $fileLength != $contentLength
+ && $httpMethod != 'HEAD')
{
throw new Exception('Content length error: expected '.$contentLength.' bytes; received '.$fileLength.' bytes');
}
- return trim($response);
+
+ if (!$getExtendedInfo)
+ {
+ return trim($response);
+ }
+ else
+ {
+ return array(
+ 'status' => $status,
+ 'headers' => $headers,
+ 'data' => $response
+ );
+ }
+ }
+
+ /**
+ * Downloads the next chunk of a specific file. The next chunk's byte range
+ * is determined by the existing file's size and the expected file size, which
+ * is stored in the piwik_option table before starting a download.
+ *
+ * Note this function uses the Range HTTP header to accomplish downloading in
+ * parts.
+ *
+ * @param string $url The url to download from.
+ * @param string $outputPath The path to the file to save/append to.
+ * @param bool $isContinuation True if this is the continuation of a download,
+ * or if we're starting a fresh one.
+ */
+ public static function downloadChunk( $url, $outputPath, $isContinuation )
+ {
+ // make sure file doesn't already exist if we're starting a new download
+ if (!$isContinuation
+ && file_exists($outputPath))
+ {
+ throw new Exception(
+ Piwik_Translate('General_DownloadFail_FileExists', "'".$outputPath."'")
+ . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting'));
+ }
+
+ // if we're starting a download, get the expected file size & save as an option
+ $downloadOption = $outputPath.'_expectedDownloadSize';
+ if (!$isContinuation)
+ {
+ $expectedFileSizeResult = Piwik_Http::sendHttpRequest(
+ $url,
+ $timeout = 300,
+ $userAgent = null,
+ $destinationPath = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $byteRange = false,
+ $getExtendedInfo = true,
+ $httpMethod = 'HEAD'
+ );
+
+ $expectedFileSize = 0;
+ if (isset($expectedFileSizeResult['headers']['Content-Length']))
+ {
+ $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length'];
+ }
+
+ if ($expectedFileSize == 0)
+ {
+ Piwik::log("HEAD request for '$url' failed, got following: ".print_r($expectedFileSizeResult, true));
+ throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail'));
+ }
+
+ Piwik_SetOption($downloadOption, $expectedFileSize);
+ }
+ else
+ {
+ $expectedFileSize = (int)Piwik_GetOption($downloadOption);
+ if ($expectedFileSize === false) // sanity check
+ {
+ throw new Exception(
+ "Trying to continue a download that never started?! That's not supposed to happen...");
+ }
+ }
+
+ // if existing file is already big enough, then fail so we don't accidentally overwrite
+ // existing DB
+ $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0;
+ if ($existingSize >= $expectedFileSize)
+ {
+ throw new Exception(
+ Piwik_Translate('General_DownloadFail_FileExistsContinue', "'".$outputPath."'")
+ . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting'));
+ }
+
+ // download a chunk of the file
+ $result = Piwik_Http::sendHttpRequest(
+ $url,
+ $timeout = 300,
+ $userAgent = null,
+ $destinationPath = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)),
+ $getExtendedInfo = true
+ );
+
+ if ($result === false
+ || $result['status'] < 200
+ || $result['status'] > 299)
+ {
+ $result['data'] = self::truncateStr($result['data'], 1024);
+ Piwik::log("Failed to download range '".$byteRange[0]."-".$byteRange[1]
+ . "' of file from url '$url'. Got result: ".print_r($result, true));
+
+ throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail'));
+ }
+
+ // write chunk to file
+ $f = fopen($outputPath, 'ab');
+ fwrite($f, $result['data']);
+ fclose($f);
+
+ clearstatcache($clear_realpath_cache = true, $outputPath);
+ return array(
+ 'current_size' => filesize($outputPath),
+ 'expected_file_size' => $expectedFileSize,
+ );
}
/**
@@ -505,4 +719,39 @@ class Piwik_Http
Piwik::setMaxExecutionTime(0);
return self::sendHttpRequest($url, 10, 'Update', $destinationPath, $tries);
}
+
+ /**
+ * Utility function, parses an HTTP header line into key/value & sets header
+ * array with them.
+ *
+ * @param array $headers
+ * @param string $line
+ */
+ private static function parseHeaderLine( &$headers, $line )
+ {
+ $parts = explode(':', $line, 2);
+ if (count($parts) == 1)
+ {
+ return;
+ }
+
+ list($name, $value) = $parts;
+ $headers[trim($name)] = trim($value);
+ }
+
+ /**
+ * Utility function that truncates a string to an arbitrary limit.
+ *
+ * @param string $str The string to truncate.
+ * @param int $limit The maximum length of the truncated string.
+ * @return string
+ */
+ private static function truncateStr( $str, $limit )
+ {
+ if (strlen($str) > $limit)
+ {
+ return substr($str, 0, $limit).'...';
+ }
+ return $str;
+ }
}
diff --git a/core/ScheduledTime/Monthly.php b/core/ScheduledTime/Monthly.php
index 72521a3548..eca3249abb 100644
--- a/core/ScheduledTime/Monthly.php
+++ b/core/ScheduledTime/Monthly.php
@@ -19,6 +19,20 @@
*/
class Piwik_ScheduledTime_Monthly extends Piwik_ScheduledTime
{
+ /**
+ * Day of the week for scheduled time.
+ *
+ * @var int
+ */
+ private $dayOfWeek = null;
+
+ /**
+ * Week number for scheduled time.
+ *
+ * @var int
+ */
+ private $week = null;
+
/**
* @return int
*/
@@ -44,6 +58,17 @@ class Piwik_ScheduledTime_Monthly extends Piwik_ScheduledTime
{
$scheduledDay = $this->day;
}
+
+ if ($this->dayOfWeek !== null
+ && $this->week !== null)
+ {
+ $newTime = $rescheduledTime + $this->week * 7 * 86400;
+ while (date("w", $newTime) != $this->dayOfWeek % 7) // modulus for sanity check
+ {
+ $newTime += 86400;
+ }
+ $scheduledDay = ($newTime - $rescheduledTime) / 86400 + 1;
+ }
// Caps scheduled day
if ( $scheduledDay > $nextMonthLength )
@@ -73,4 +98,27 @@ class Piwik_ScheduledTime_Monthly extends Piwik_ScheduledTime
$this->day = $_day;
}
+
+ /**
+ * Makes this scheduled time execute on a particular day of the week on each month.
+ *
+ * @param int $_day the day of the week to use, between 0-6 (inclusive). 0 -> Sunday
+ * @param int $_week the week to use, between 0-3 (inclusive)
+ * @throws Exception if either parameter is invalid
+ */
+ public function setDayOfWeek($_day, $_week)
+ {
+ if (!($_day >= 0 && $_day < 7))
+ {
+ throw new Exception("Invalid day of week parameter, must be >= 0 & < 7");
+ }
+
+ if (!($_week >= 0 && $_week < 4))
+ {
+ throw new Exception("Invalid week number, must be >= 1 & < 4");
+ }
+
+ $this->dayOfWeek = $_day;
+ $this->week = $_week;
+ }
}
diff --git a/core/Unzip.php b/core/Unzip.php
index 1031df4540..612b321f1d 100644
--- a/core/Unzip.php
+++ b/core/Unzip.php
@@ -31,10 +31,20 @@ class Piwik_Unzip
case 'ZipArchive':
if(class_exists('ZipArchive', false))
return new Piwik_Unzip_ZipArchive($filename);
-
+ break;
+ case 'tar.gz':
+ return new Piwik_Unzip_Tar($filename, 'gz');
+ case 'tar.bz2':
+ return new Piwik_Unzip_Tar($filename, 'bz2');
+ case 'gz':
+ if (function_exists('gzopen'))
+ return new Piwik_Unzip_Gzip($filename);
+ break;
case 'PclZip':
default:
return new Piwik_Unzip_PclZip($filename);
}
+
+ return new Piwik_Unzip_PclZip($filename);
}
}
diff --git a/core/Unzip/Gzip.php b/core/Unzip/Gzip.php
new file mode 100755
index 0000000000..9d61c7658c
--- /dev/null
+++ b/core/Unzip/Gzip.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+
+/**
+ * Unzip implementation for .gz files.
+ *
+ * @package Piwik
+ * @subpackage Piwik_Unzip
+ */
+class Piwik_Unzip_Gzip implements Piwik_Unzip_Interface
+{
+ /**
+ * Name of .gz file.
+ *
+ * @var string
+ */
+ private $filename = null;
+
+ /**
+ * Error string.
+ *
+ * @var string
+ */
+ private $error = null;
+
+ /**
+ * Constructor.
+ *
+ * @param string $filename Name of .gz file.
+ */
+ public function __construct($filename)
+ {
+ $this->filename = $filename;
+ }
+
+ /**
+ * Extracts the contents of the .gz file to $pathExtracted.
+ *
+ * @param string $pathExtracted Must be file, not directory.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extract($pathExtracted)
+ {
+ $file = gzopen($this->filename, 'r');
+ if ($file === false)
+ {
+ $this->error = "gzopen failed";
+ return false;
+ }
+
+ $output = fopen($pathExtracted, 'w');
+ while (!feof($file))
+ {
+ fwrite($output, fread($file, 1024*1024));
+ }
+ fclose($output);
+
+ $success = gzclose($file);
+ if ($success === false)
+ {
+ $this->error = "gzclose failed";
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get error status string for the latest error.
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ return $this->error;
+ }
+}
+
diff --git a/core/Unzip/Tar.php b/core/Unzip/Tar.php
new file mode 100755
index 0000000000..77f90bf073
--- /dev/null
+++ b/core/Unzip/Tar.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik
+ * @package Piwik
+ */
+
+/**
+ * @see libs/Archive_Tar
+ */
+require_once PIWIK_INCLUDE_PATH . '/libs/Archive_Tar/Tar.php';
+
+/**
+ * Unzip implementation for Archive_Tar PEAR lib.
+ *
+ * @package Piwik
+ * @subpackage Piwik_Unzip
+ */
+class Piwik_Unzip_Tar implements Piwik_Unzip_Interface
+{
+ /**
+ * Archive_Tar instance.
+ *
+ * @var Archive_Tar
+ */
+ private $tarArchive = null;
+
+ /**
+ * Constructor.
+ *
+ * @param string $filename Path to tar file.
+ * @param string|null $compression Either 'gz', 'bz2' or null for no compression.
+ */
+ public function __construct($filename, $compression = null)
+ {
+ $this->tarArchive = new Archive_Tar($filename, $compression);
+ }
+
+ /**
+ * Extracts the contents of the tar file to $pathExtracted.
+ *
+ * @param string $pathExtracted Directory to extract into.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extract($pathExtracted)
+ {
+ return $this->tarArchive->extract($pathExtracted);
+ }
+
+ /**
+ * Extracts one file held in a tar archive and returns the deflated file
+ * as a string.
+ *
+ * @param string $inArchivePath Path to file in the tar archive.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extractInString($inArchivePath)
+ {
+ return $this->tarArchive->extractInString($inArchivePath);
+ }
+
+ /**
+ * Lists the files held in the tar archive.
+ *
+ * @return array List of paths describing everything held in the tar archive.
+ */
+ public function listContent()
+ {
+ return $this->tarArchive->listContent();
+ }
+
+ /**
+ * Get error status string for the latest error.
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ return $this->tarArchive->error_object->getMessage();
+ }
+}
diff --git a/lang/en.php b/lang/en.php
index 1270f30580..bce1a54826 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -348,7 +348,14 @@ $translations = array(
'General_Broken' => 'Broken',
'General_InfoFor' => 'Info for %s',
'General_Add' => 'Add',
+ 'General_CannotUnzipFile' => 'Cannot unzip file %1$s: %2$s',
+ 'General_GetStarted' => 'Get started',
+ 'General_Continue' => 'Continue',
'General_Note' => 'Note',
+ 'General_DownloadFail_FileExists' => 'The file %s already exists!',
+ 'General_DownloadPleaseRemoveExisting' => 'If you want it to be replaced, please remove the existing file.',
+ 'General_DownloadFail_HttpRequestFail' => 'Could not download the file! Something might be wrong with the website you\'re downloading from. You could try again later or get the file by yourself.',
+ 'General_DownloadFail_FileExistsContinue' => 'Trying to continue the download for %s, but a fully downloaded file already exists!',
'Actions_PluginDescription' => 'Reports about the page views, the outlinks and downloads. Outlinks and Downloads tracking is automatic! You can also track your internal website\'s Search Engine.',
'Actions_Actions' => 'Actions',
'Actions_SubmenuPages' => 'Pages',
@@ -1519,6 +1526,9 @@ And thank you for using Piwik!',
'UserCountryMap_worldMap' => 'World Map',
'UserCountry_HttpServerModule' => 'HTTP Server Module',
'UserCountryMap_toggleFullscreen' => 'Toggle fullscreen',
+ 'UserCountry_GeoIPDatabases' => 'GeoIP Databases',
+ 'UserCountry_PiwikNotManagingGeoIPDBs' => 'Piwik is not currently managing any GeoIP databases.',
+ 'UserCountry_IWantToDownloadFreeGeoIP' => 'I want to download the free GeoIP database...',
'UserCountry_DefaultLocationProviderDesc1' => 'The default location provider guesses a visitor\'s country based on the language they use.',
'UserCountry_DefaultLocationProviderDesc2' => 'This is not very accurate, so %1$swe recommend installing and using %2$sGeoIP%3$s.%4$s',
'UserCountry_HowToInstallGeoIPDatabases' => 'How do I get the GeoIP databases?',
@@ -1538,6 +1548,7 @@ And thank you for using Piwik!',
'UserCountry_GeoIPImplHasAccessTo' => 'This GeoIP implementation has access to the following types of databases',
'UserCountry_CannotFindGeoIPServerVar' => 'The %s variable is not set. Your server may not be configured correctly.',
'UserCountry_GeoIPCannotFindMbstringExtension' => 'Cannot find the %1$s function. Please make sure the %2$s extension is installed and loaded.',
+ 'UserCountry_InvalidGeoIPUpdatePeriod' => 'Invalid period for the GeoIP updater: %1$s. Valid values are %2$s.',
'UserCountry_LocationProvider' => 'Location Provider',
'UserCountry_TestIPLocatorFailed' => 'Piwik tried checking the location of a known IP address (%1$s), but your server returned %2$s. If this provider were configured correctly, it would return %3$s.',
'UserCountry_CannotLocalizeLocalIP' => 'IP address %s is a local address and cannot be geolocated.',
@@ -1559,6 +1570,19 @@ And thank you for using Piwik!',
'UserCountry_AssumingNonApache' => "Cannot find apache_get_modules function, assuming non-Apache webserver.",
'UserCountry_FoundApacheModules' => 'Piwik found the following Apache modules',
'UserCountry_GeoIPNoServerVars' => 'Piwik cannot find any GeoIP %s variables.',
+ 'UserCountry_IPurchasedGeoIPDBs' => 'I purchased more accurate databases from MaxMind and want to setup automatic updates.',
+ 'UserCountry_GeoIPUpdaterInstructions' => 'Enter the download links for your databases below. If you\'ve purchased databases from %3$sMaxMind%4$s, you can find these links %1$shere%2$s. Please contact %3$sMaxMind%4$s if you have trouble accessing them.',
+ 'UserCountry_GeoIPUpdaterIntro' => 'Piwik is currently managing updates for the following GeoIP databases',
+ 'UserCountry_LocationDatabase' => 'Location Database',
+ 'UserCountry_LocationDatabaseHint' => 'A location database is either a country, region or city database.',
+ 'UserCountry_ISPDatabase' => 'ISP Database',
+ 'UserCountry_OrgDatabase' => 'Organization Database',
+ 'UserCountry_DownloadingDb' => 'Downloading %s',
+ 'UserCountry_CannotSetupGeoIPAutoUpdating' => 'It seems like you\'re storing your GeoIP databases outside of Piwik (we can tell since there are no databases in the misc subdirectory, but your GeoIP is working). Piwik cannot automatically update your GeoIP databases if they are located outside of the misc directory.',
+ 'UserCountry_CannotListContent' => 'Couldn\'t list content for %1$s: %2$s',
+ 'UserCountry_CannotFindGeoIPDatabaseInArchive' => 'Cannot find %1$s file in tar archive %2$s!',
+ 'UserCountry_CannotUnzipDatFile' => 'Could not unzip dat file in %1$s: %2$s',
+ 'UserCountry_UnsupportedArchiveType' => 'Encountered unsupported archive type %1$s.',
'UserSettings_VisitorSettings' => 'Visitor Settings',
'UserSettings_BrowserFamilies' => 'Browser families',
'UserSettings_Browsers' => 'Browsers',
diff --git a/libs/Archive_Tar/Tar.php b/libs/Archive_Tar/Tar.php
new file mode 100755
index 0000000000..33c7d395d0
--- /dev/null
+++ b/libs/Archive_Tar/Tar.php
@@ -0,0 +1,1970 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * File::CSV
+ *
+ * PHP versions 4 and 5
+ *
+ * Copyright (c) 1997-2008,
+ * Vincent Blavet <vincent@phpconcept.net>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category File_Formats
+ * @package Archive_Tar
+ * @author Vincent Blavet <vincent@phpconcept.net>
+ * @copyright 1997-2010 The Authors
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Tar.php 324840 2012-04-05 08:44:41Z mrook $
+ * @link http://pear.php.net/package/Archive_Tar
+ */
+
+require_once 'PEAR.php';
+
+define('ARCHIVE_TAR_ATT_SEPARATOR', 90001);
+define('ARCHIVE_TAR_END_BLOCK', pack("a512", ''));
+
+/**
+* Creates a (compressed) Tar archive
+*
+* @package Archive_Tar
+* @author Vincent Blavet <vincent@phpconcept.net>
+* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+* @version $Revision: 324840 $
+*/
+class Archive_Tar extends PEAR
+{
+ /**
+ * @var string Name of the Tar
+ */
+ var $_tarname='';
+
+ /**
+ * @var boolean if true, the Tar file will be gzipped
+ */
+ var $_compress=false;
+
+ /**
+ * @var string Type of compression : 'none', 'gz' or 'bz2'
+ */
+ var $_compress_type='none';
+
+ /**
+ * @var string Explode separator
+ */
+ var $_separator=' ';
+
+ /**
+ * @var file descriptor
+ */
+ var $_file=0;
+
+ /**
+ * @var string Local Tar name of a remote Tar (http:// or ftp://)
+ */
+ var $_temp_tarname='';
+
+ /**
+ * @var string regular expression for ignoring files or directories
+ */
+ var $_ignore_regexp='';
+
+ /**
+ * @var object PEAR_Error object
+ */
+ var $error_object=null;
+
+ // {{{ constructor
+ /**
+ * Archive_Tar Class constructor. This flavour of the constructor only
+ * declare a new Archive_Tar object, identifying it by the name of the
+ * tar file.
+ * If the compress argument is set the tar will be read or created as a
+ * gzip or bz2 compressed TAR file.
+ *
+ * @param string $p_tarname The name of the tar archive to create
+ * @param string $p_compress can be null, 'gz' or 'bz2'. This
+ * parameter indicates if gzip or bz2 compression
+ * is required. For compatibility reason the
+ * boolean value 'true' means 'gz'.
+ *
+ * @access public
+ */
+ function Archive_Tar($p_tarname, $p_compress = null)
+ {
+ $this->PEAR();
+ $this->_compress = false;
+ $this->_compress_type = 'none';
+ if (($p_compress === null) || ($p_compress == '')) {
+ if (@file_exists($p_tarname)) {
+ if ($fp = @fopen($p_tarname, "rb")) {
+ // look for gzip magic cookie
+ $data = fread($fp, 2);
+ fclose($fp);
+ if ($data == "\37\213") {
+ $this->_compress = true;
+ $this->_compress_type = 'gz';
+ // No sure it's enought for a magic code ....
+ } elseif ($data == "BZ") {
+ $this->_compress = true;
+ $this->_compress_type = 'bz2';
+ }
+ }
+ } else {
+ // probably a remote file or some file accessible
+ // through a stream interface
+ if (substr($p_tarname, -2) == 'gz') {
+ $this->_compress = true;
+ $this->_compress_type = 'gz';
+ } elseif ((substr($p_tarname, -3) == 'bz2') ||
+ (substr($p_tarname, -2) == 'bz')) {
+ $this->_compress = true;
+ $this->_compress_type = 'bz2';
+ }
+ }
+ } else {
+ if (($p_compress === true) || ($p_compress == 'gz')) {
+ $this->_compress = true;
+ $this->_compress_type = 'gz';
+ } else if ($p_compress == 'bz2') {
+ $this->_compress = true;
+ $this->_compress_type = 'bz2';
+ } else {
+ $this->_error("Unsupported compression type '$p_compress'\n".
+ "Supported types are 'gz' and 'bz2'.\n");
+ return false;
+ }
+ }
+ $this->_tarname = $p_tarname;
+ if ($this->_compress) { // assert zlib or bz2 extension support
+ if ($this->_compress_type == 'gz')
+ $extname = 'zlib';
+ else if ($this->_compress_type == 'bz2')
+ $extname = 'bz2';
+
+ if (!extension_loaded($extname)) {
+ PEAR::loadExtension($extname);
+ }
+ if (!extension_loaded($extname)) {
+ $this->_error("The extension '$extname' couldn't be found.\n".
+ "Please make sure your version of PHP was built ".
+ "with '$extname' support.\n");
+ return false;
+ }
+ }
+ }
+ // }}}
+
+ // {{{ destructor
+ function _Archive_Tar()
+ {
+ $this->_close();
+ // ----- Look for a local copy to delete
+ if ($this->_temp_tarname != '')
+ @unlink($this->_temp_tarname);
+ $this->_PEAR();
+ }
+ // }}}
+
+ // {{{ create()
+ /**
+ * This method creates the archive file and add the files / directories
+ * that are listed in $p_filelist.
+ * If a file with the same name exist and is writable, it is replaced
+ * by the new tar.
+ * The method return false and a PEAR error text.
+ * The $p_filelist parameter can be an array of string, each string
+ * representing a filename or a directory name with their path if
+ * needed. It can also be a single string with names separated by a
+ * single blank.
+ * For each directory added in the archive, the files and
+ * sub-directories are also added.
+ * See also createModify() method for more details.
+ *
+ * @param array $p_filelist An array of filenames and directory names, or a
+ * single string with names separated by a single
+ * blank space.
+ *
+ * @return true on success, false on error.
+ * @see createModify()
+ * @access public
+ */
+ function create($p_filelist)
+ {
+ return $this->createModify($p_filelist, '', '');
+ }
+ // }}}
+
+ // {{{ add()
+ /**
+ * This method add the files / directories that are listed in $p_filelist in
+ * the archive. If the archive does not exist it is created.
+ * The method return false and a PEAR error text.
+ * The files and directories listed are only added at the end of the archive,
+ * even if a file with the same name is already archived.
+ * See also createModify() method for more details.
+ *
+ * @param array $p_filelist An array of filenames and directory names, or a
+ * single string with names separated by a single
+ * blank space.
+ *
+ * @return true on success, false on error.
+ * @see createModify()
+ * @access public
+ */
+ function add($p_filelist)
+ {
+ return $this->addModify($p_filelist, '', '');
+ }
+ // }}}
+
+ // {{{ extract()
+ function extract($p_path='', $p_preserve=false)
+ {
+ return $this->extractModify($p_path, '', $p_preserve);
+ }
+ // }}}
+
+ // {{{ listContent()
+ function listContent()
+ {
+ $v_list_detail = array();
+
+ if ($this->_openRead()) {
+ if (!$this->_extractList('', $v_list_detail, "list", '', '')) {
+ unset($v_list_detail);
+ $v_list_detail = 0;
+ }
+ $this->_close();
+ }
+
+ return $v_list_detail;
+ }
+ // }}}
+
+ // {{{ createModify()
+ /**
+ * This method creates the archive file and add the files / directories
+ * that are listed in $p_filelist.
+ * If the file already exists and is writable, it is replaced by the
+ * new tar. It is a create and not an add. If the file exists and is
+ * read-only or is a directory it is not replaced. The method return
+ * false and a PEAR error text.
+ * The $p_filelist parameter can be an array of string, each string
+ * representing a filename or a directory name with their path if
+ * needed. It can also be a single string with names separated by a
+ * single blank.
+ * The path indicated in $p_remove_dir will be removed from the
+ * memorized path of each file / directory listed when this path
+ * exists. By default nothing is removed (empty path '')
+ * The path indicated in $p_add_dir will be added at the beginning of
+ * the memorized path of each file / directory listed. However it can
+ * be set to empty ''. The adding of a path is done after the removing
+ * of path.
+ * The path add/remove ability enables the user to prepare an archive
+ * for extraction in a different path than the origin files are.
+ * See also addModify() method for file adding properties.
+ *
+ * @param array $p_filelist An array of filenames and directory names,
+ * or a single string with names separated by
+ * a single blank space.
+ * @param string $p_add_dir A string which contains a path to be added
+ * to the memorized path of each element in
+ * the list.
+ * @param string $p_remove_dir A string which contains a path to be
+ * removed from the memorized path of each
+ * element in the list, when relevant.
+ *
+ * @return boolean true on success, false on error.
+ * @access public
+ * @see addModify()
+ */
+ function createModify($p_filelist, $p_add_dir, $p_remove_dir='')
+ {
+ $v_result = true;
+
+ if (!$this->_openWrite())
+ return false;
+
+ if ($p_filelist != '') {
+ if (is_array($p_filelist))
+ $v_list = $p_filelist;
+ elseif (is_string($p_filelist))
+ $v_list = explode($this->_separator, $p_filelist);
+ else {
+ $this->_cleanFile();
+ $this->_error('Invalid file list');
+ return false;
+ }
+
+ $v_result = $this->_addList($v_list, $p_add_dir, $p_remove_dir);
+ }
+
+ if ($v_result) {
+ $this->_writeFooter();
+ $this->_close();
+ } else
+ $this->_cleanFile();
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ addModify()
+ /**
+ * This method add the files / directories listed in $p_filelist at the
+ * end of the existing archive. If the archive does not yet exists it
+ * is created.
+ * The $p_filelist parameter can be an array of string, each string
+ * representing a filename or a directory name with their path if
+ * needed. It can also be a single string with names separated by a
+ * single blank.
+ * The path indicated in $p_remove_dir will be removed from the
+ * memorized path of each file / directory listed when this path
+ * exists. By default nothing is removed (empty path '')
+ * The path indicated in $p_add_dir will be added at the beginning of
+ * the memorized path of each file / directory listed. However it can
+ * be set to empty ''. The adding of a path is done after the removing
+ * of path.
+ * The path add/remove ability enables the user to prepare an archive
+ * for extraction in a different path than the origin files are.
+ * If a file/dir is already in the archive it will only be added at the
+ * end of the archive. There is no update of the existing archived
+ * file/dir. However while extracting the archive, the last file will
+ * replace the first one. This results in a none optimization of the
+ * archive size.
+ * If a file/dir does not exist the file/dir is ignored. However an
+ * error text is send to PEAR error.
+ * If a file/dir is not readable the file/dir is ignored. However an
+ * error text is send to PEAR error.
+ *
+ * @param array $p_filelist An array of filenames and directory
+ * names, or a single string with names
+ * separated by a single blank space.
+ * @param string $p_add_dir A string which contains a path to be
+ * added to the memorized path of each
+ * element in the list.
+ * @param string $p_remove_dir A string which contains a path to be
+ * removed from the memorized path of
+ * each element in the list, when
+ * relevant.
+ *
+ * @return true on success, false on error.
+ * @access public
+ */
+ function addModify($p_filelist, $p_add_dir, $p_remove_dir='')
+ {
+ $v_result = true;
+
+ if (!$this->_isArchive())
+ $v_result = $this->createModify($p_filelist, $p_add_dir,
+ $p_remove_dir);
+ else {
+ if (is_array($p_filelist))
+ $v_list = $p_filelist;
+ elseif (is_string($p_filelist))
+ $v_list = explode($this->_separator, $p_filelist);
+ else {
+ $this->_error('Invalid file list');
+ return false;
+ }
+
+ $v_result = $this->_append($v_list, $p_add_dir, $p_remove_dir);
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ addString()
+ /**
+ * This method add a single string as a file at the
+ * end of the existing archive. If the archive does not yet exists it
+ * is created.
+ *
+ * @param string $p_filename A string which contains the full
+ * filename path that will be associated
+ * with the string.
+ * @param string $p_string The content of the file added in
+ * the archive.
+ *
+ * @return true on success, false on error.
+ * @access public
+ */
+ function addString($p_filename, $p_string)
+ {
+ $v_result = true;
+
+ if (!$this->_isArchive()) {
+ if (!$this->_openWrite()) {
+ return false;
+ }
+ $this->_close();
+ }
+
+ if (!$this->_openAppend())
+ return false;
+
+ // Need to check the get back to the temporary file ? ....
+ $v_result = $this->_addString($p_filename, $p_string);
+
+ $this->_writeFooter();
+
+ $this->_close();
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ extractModify()
+ /**
+ * This method extract all the content of the archive in the directory
+ * indicated by $p_path. When relevant the memorized path of the
+ * files/dir can be modified by removing the $p_remove_path path at the
+ * beginning of the file/dir path.
+ * While extracting a file, if the directory path does not exists it is
+ * created.
+ * While extracting a file, if the file already exists it is replaced
+ * without looking for last modification date.
+ * While extracting a file, if the file already exists and is write
+ * protected, the extraction is aborted.
+ * While extracting a file, if a directory with the same name already
+ * exists, the extraction is aborted.
+ * While extracting a directory, if a file with the same name already
+ * exists, the extraction is aborted.
+ * While extracting a file/directory if the destination directory exist
+ * and is write protected, or does not exist but can not be created,
+ * the extraction is aborted.
+ * If after extraction an extracted file does not show the correct
+ * stored file size, the extraction is aborted.
+ * When the extraction is aborted, a PEAR error text is set and false
+ * is returned. However the result can be a partial extraction that may
+ * need to be manually cleaned.
+ *
+ * @param string $p_path The path of the directory where the
+ * files/dir need to by extracted.
+ * @param string $p_remove_path Part of the memorized path that can be
+ * removed if present at the beginning of
+ * the file/dir path.
+ * @param boolean $p_preserve Preserve user/group ownership of files
+ *
+ * @return boolean true on success, false on error.
+ * @access public
+ * @see extractList()
+ */
+ function extractModify($p_path, $p_remove_path, $p_preserve=false)
+ {
+ $v_result = true;
+ $v_list_detail = array();
+
+ if ($v_result = $this->_openRead()) {
+ $v_result = $this->_extractList($p_path, $v_list_detail,
+ "complete", 0, $p_remove_path, $p_preserve);
+ $this->_close();
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ extractInString()
+ /**
+ * This method extract from the archive one file identified by $p_filename.
+ * The return value is a string with the file content, or NULL on error.
+ *
+ * @param string $p_filename The path of the file to extract in a string.
+ *
+ * @return a string with the file content or NULL.
+ * @access public
+ */
+ function extractInString($p_filename)
+ {
+ if ($this->_openRead()) {
+ $v_result = $this->_extractInString($p_filename);
+ $this->_close();
+ } else {
+ $v_result = null;
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ extractList()
+ /**
+ * This method extract from the archive only the files indicated in the
+ * $p_filelist. These files are extracted in the current directory or
+ * in the directory indicated by the optional $p_path parameter.
+ * If indicated the $p_remove_path can be used in the same way as it is
+ * used in extractModify() method.
+ *
+ * @param array $p_filelist An array of filenames and directory names,
+ * or a single string with names separated
+ * by a single blank space.
+ * @param string $p_path The path of the directory where the
+ * files/dir need to by extracted.
+ * @param string $p_remove_path Part of the memorized path that can be
+ * removed if present at the beginning of
+ * the file/dir path.
+ * @param boolean $p_preserve Preserve user/group ownership of files
+ *
+ * @return true on success, false on error.
+ * @access public
+ * @see extractModify()
+ */
+ function extractList($p_filelist, $p_path='', $p_remove_path='', $p_preserve=false)
+ {
+ $v_result = true;
+ $v_list_detail = array();
+
+ if (is_array($p_filelist))
+ $v_list = $p_filelist;
+ elseif (is_string($p_filelist))
+ $v_list = explode($this->_separator, $p_filelist);
+ else {
+ $this->_error('Invalid string list');
+ return false;
+ }
+
+ if ($v_result = $this->_openRead()) {
+ $v_result = $this->_extractList($p_path, $v_list_detail, "partial",
+ $v_list, $p_remove_path, $p_preserve);
+ $this->_close();
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ setAttribute()
+ /**
+ * This method set specific attributes of the archive. It uses a variable
+ * list of parameters, in the format attribute code + attribute values :
+ * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ',');
+ *
+ * @param mixed $argv variable list of attributes and values
+ *
+ * @return true on success, false on error.
+ * @access public
+ */
+ function setAttribute()
+ {
+ $v_result = true;
+
+ // ----- Get the number of variable list of arguments
+ if (($v_size = func_num_args()) == 0) {
+ return true;
+ }
+
+ // ----- Get the arguments
+ $v_att_list = &func_get_args();
+
+ // ----- Read the attributes
+ $i=0;
+ while ($i<$v_size) {
+
+ // ----- Look for next option
+ switch ($v_att_list[$i]) {
+ // ----- Look for options that request a string value
+ case ARCHIVE_TAR_ATT_SEPARATOR :
+ // ----- Check the number of parameters
+ if (($i+1) >= $v_size) {
+ $this->_error('Invalid number of parameters for '
+ .'attribute ARCHIVE_TAR_ATT_SEPARATOR');
+ return false;
+ }
+
+ // ----- Get the value
+ $this->_separator = $v_att_list[$i+1];
+ $i++;
+ break;
+
+ default :
+ $this->_error('Unknow attribute code '.$v_att_list[$i].'');
+ return false;
+ }
+
+ // ----- Next attribute
+ $i++;
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ setIgnoreRegexp()
+ /**
+ * This method sets the regular expression for ignoring files and directories
+ * at import, for example:
+ * $arch->setIgnoreRegexp("#CVS|\.svn#");
+ *
+ * @param string $regexp regular expression defining which files or directories to ignore
+ *
+ * @access public
+ */
+ function setIgnoreRegexp($regexp)
+ {
+ $this->_ignore_regexp = $regexp;
+ }
+ // }}}
+
+ // {{{ setIgnoreList()
+ /**
+ * This method sets the regular expression for ignoring all files and directories
+ * matching the filenames in the array list at import, for example:
+ * $arch->setIgnoreList(array('CVS', '.svn', 'bin/tool'));
+ *
+ * @param array $list a list of file or directory names to ignore
+ *
+ * @access public
+ */
+ function setIgnoreList($list)
+ {
+ $regexp = str_replace(array('#', '.', '^', '$'), array('\#', '\.', '\^', '\$'), $list);
+ $regexp = '#/'.join('$|/', $list).'#';
+ $this->setIgnoreRegexp($regexp);
+ }
+ // }}}
+
+ // {{{ _error()
+ function _error($p_message)
+ {
+ $this->error_object = &$this->raiseError($p_message);
+ }
+ // }}}
+
+ // {{{ _warning()
+ function _warning($p_message)
+ {
+ $this->error_object = &$this->raiseError($p_message);
+ }
+ // }}}
+
+ // {{{ _isArchive()
+ function _isArchive($p_filename=null)
+ {
+ if ($p_filename == null) {
+ $p_filename = $this->_tarname;
+ }
+ clearstatcache();
+ return @is_file($p_filename) && !@is_link($p_filename);
+ }
+ // }}}
+
+ // {{{ _openWrite()
+ function _openWrite()
+ {
+ if ($this->_compress_type == 'gz' && function_exists('gzopen'))
+ $this->_file = @gzopen($this->_tarname, "wb9");
+ else if ($this->_compress_type == 'bz2' && function_exists('bzopen'))
+ $this->_file = @bzopen($this->_tarname, "w");
+ else if ($this->_compress_type == 'none')
+ $this->_file = @fopen($this->_tarname, "wb");
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ if ($this->_file == 0) {
+ $this->_error('Unable to open in write mode \''
+ .$this->_tarname.'\'');
+ return false;
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _openRead()
+ function _openRead()
+ {
+ if (strtolower(substr($this->_tarname, 0, 7)) == 'http://') {
+
+ // ----- Look if a local copy need to be done
+ if ($this->_temp_tarname == '') {
+ $this->_temp_tarname = uniqid('tar').'.tmp';
+ if (!$v_file_from = @fopen($this->_tarname, 'rb')) {
+ $this->_error('Unable to open in read mode \''
+ .$this->_tarname.'\'');
+ $this->_temp_tarname = '';
+ return false;
+ }
+ if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) {
+ $this->_error('Unable to open in write mode \''
+ .$this->_temp_tarname.'\'');
+ $this->_temp_tarname = '';
+ return false;
+ }
+ while ($v_data = @fread($v_file_from, 1024))
+ @fwrite($v_file_to, $v_data);
+ @fclose($v_file_from);
+ @fclose($v_file_to);
+ }
+
+ // ----- File to open if the local copy
+ $v_filename = $this->_temp_tarname;
+
+ } else
+ // ----- File to open if the normal Tar file
+ $v_filename = $this->_tarname;
+
+ if ($this->_compress_type == 'gz')
+ $this->_file = @gzopen($v_filename, "rb");
+ else if ($this->_compress_type == 'bz2')
+ $this->_file = @bzopen($v_filename, "r");
+ else if ($this->_compress_type == 'none')
+ $this->_file = @fopen($v_filename, "rb");
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ if ($this->_file == 0) {
+ $this->_error('Unable to open in read mode \''.$v_filename.'\'');
+ return false;
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _openReadWrite()
+ function _openReadWrite()
+ {
+ if ($this->_compress_type == 'gz')
+ $this->_file = @gzopen($this->_tarname, "r+b");
+ else if ($this->_compress_type == 'bz2') {
+ $this->_error('Unable to open bz2 in read/write mode \''
+ .$this->_tarname.'\' (limitation of bz2 extension)');
+ return false;
+ } else if ($this->_compress_type == 'none')
+ $this->_file = @fopen($this->_tarname, "r+b");
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ if ($this->_file == 0) {
+ $this->_error('Unable to open in read/write mode \''
+ .$this->_tarname.'\'');
+ return false;
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _close()
+ function _close()
+ {
+ //if (isset($this->_file)) {
+ if (is_resource($this->_file)) {
+ if ($this->_compress_type == 'gz')
+ @gzclose($this->_file);
+ else if ($this->_compress_type == 'bz2')
+ @bzclose($this->_file);
+ else if ($this->_compress_type == 'none')
+ @fclose($this->_file);
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ $this->_file = 0;
+ }
+
+ // ----- Look if a local copy need to be erase
+ // Note that it might be interesting to keep the url for a time : ToDo
+ if ($this->_temp_tarname != '') {
+ @unlink($this->_temp_tarname);
+ $this->_temp_tarname = '';
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _cleanFile()
+ function _cleanFile()
+ {
+ $this->_close();
+
+ // ----- Look for a local copy
+ if ($this->_temp_tarname != '') {
+ // ----- Remove the local copy but not the remote tarname
+ @unlink($this->_temp_tarname);
+ $this->_temp_tarname = '';
+ } else {
+ // ----- Remove the local tarname file
+ @unlink($this->_tarname);
+ }
+ $this->_tarname = '';
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _writeBlock()
+ function _writeBlock($p_binary_data, $p_len=null)
+ {
+ if (is_resource($this->_file)) {
+ if ($p_len === null) {
+ if ($this->_compress_type == 'gz')
+ @gzputs($this->_file, $p_binary_data);
+ else if ($this->_compress_type == 'bz2')
+ @bzwrite($this->_file, $p_binary_data);
+ else if ($this->_compress_type == 'none')
+ @fputs($this->_file, $p_binary_data);
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+ } else {
+ if ($this->_compress_type == 'gz')
+ @gzputs($this->_file, $p_binary_data, $p_len);
+ else if ($this->_compress_type == 'bz2')
+ @bzwrite($this->_file, $p_binary_data, $p_len);
+ else if ($this->_compress_type == 'none')
+ @fputs($this->_file, $p_binary_data, $p_len);
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ }
+ }
+ return true;
+ }
+ // }}}
+
+ // {{{ _readBlock()
+ function _readBlock()
+ {
+ $v_block = null;
+ if (is_resource($this->_file)) {
+ if ($this->_compress_type == 'gz')
+ $v_block = @gzread($this->_file, 512);
+ else if ($this->_compress_type == 'bz2')
+ $v_block = @bzread($this->_file, 512);
+ else if ($this->_compress_type == 'none')
+ $v_block = @fread($this->_file, 512);
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+ }
+ return $v_block;
+ }
+ // }}}
+
+ // {{{ _jumpBlock()
+ function _jumpBlock($p_len=null)
+ {
+ if (is_resource($this->_file)) {
+ if ($p_len === null)
+ $p_len = 1;
+
+ if ($this->_compress_type == 'gz') {
+ @gzseek($this->_file, gztell($this->_file)+($p_len*512));
+ }
+ else if ($this->_compress_type == 'bz2') {
+ // ----- Replace missing bztell() and bzseek()
+ for ($i=0; $i<$p_len; $i++)
+ $this->_readBlock();
+ } else if ($this->_compress_type == 'none')
+ @fseek($this->_file, $p_len*512, SEEK_CUR);
+ else
+ $this->_error('Unknown or missing compression type ('
+ .$this->_compress_type.')');
+
+ }
+ return true;
+ }
+ // }}}
+
+ // {{{ _writeFooter()
+ function _writeFooter()
+ {
+ if (is_resource($this->_file)) {
+ // ----- Write the last 0 filled block for end of archive
+ $v_binary_data = pack('a1024', '');
+ $this->_writeBlock($v_binary_data);
+ }
+ return true;
+ }
+ // }}}
+
+ // {{{ _addList()
+ function _addList($p_list, $p_add_dir, $p_remove_dir)
+ {
+ $v_result=true;
+ $v_header = array();
+
+ // ----- Remove potential windows directory separator
+ $p_add_dir = $this->_translateWinPath($p_add_dir);
+ $p_remove_dir = $this->_translateWinPath($p_remove_dir, false);
+
+ if (!$this->_file) {
+ $this->_error('Invalid file descriptor');
+ return false;
+ }
+
+ if (sizeof($p_list) == 0)
+ return true;
+
+ foreach ($p_list as $v_filename) {
+ if (!$v_result) {
+ break;
+ }
+
+ // ----- Skip the current tar name
+ if ($v_filename == $this->_tarname)
+ continue;
+
+ if ($v_filename == '')
+ continue;
+
+ // ----- ignore files and directories matching the ignore regular expression
+ if ($this->_ignore_regexp && preg_match($this->_ignore_regexp, '/'.$v_filename)) {
+ $this->_warning("File '$v_filename' ignored");
+ continue;
+ }
+
+ if (!file_exists($v_filename) && !is_link($v_filename)) {
+ $this->_warning("File '$v_filename' does not exist");
+ continue;
+ }
+
+ // ----- Add the file or directory header
+ if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir))
+ return false;
+
+ if (@is_dir($v_filename) && !@is_link($v_filename)) {
+ if (!($p_hdir = opendir($v_filename))) {
+ $this->_warning("Directory '$v_filename' can not be read");
+ continue;
+ }
+ while (false !== ($p_hitem = readdir($p_hdir))) {
+ if (($p_hitem != '.') && ($p_hitem != '..')) {
+ if ($v_filename != ".")
+ $p_temp_list[0] = $v_filename.'/'.$p_hitem;
+ else
+ $p_temp_list[0] = $p_hitem;
+
+ $v_result = $this->_addList($p_temp_list,
+ $p_add_dir,
+ $p_remove_dir);
+ }
+ }
+
+ unset($p_temp_list);
+ unset($p_hdir);
+ unset($p_hitem);
+ }
+ }
+
+ return $v_result;
+ }
+ // }}}
+
+ // {{{ _addFile()
+ function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir)
+ {
+ if (!$this->_file) {
+ $this->_error('Invalid file descriptor');
+ return false;
+ }
+
+ if ($p_filename == '') {
+ $this->_error('Invalid file name');
+ return false;
+ }
+
+ // ----- Calculate the stored filename
+ $p_filename = $this->_translateWinPath($p_filename, false);;
+ $v_stored_filename = $p_filename;
+ if (strcmp($p_filename, $p_remove_dir) == 0) {
+ return true;
+ }
+ if ($p_remove_dir != '') {
+ if (substr($p_remove_dir, -1) != '/')
+ $p_remove_dir .= '/';
+
+ if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir)
+ $v_stored_filename = substr($p_filename, strlen($p_remove_dir));
+ }
+ $v_stored_filename = $this->_translateWinPath($v_stored_filename);
+ if ($p_add_dir != '') {
+ if (substr($p_add_dir, -1) == '/')
+ $v_stored_filename = $p_add_dir.$v_stored_filename;
+ else
+ $v_stored_filename = $p_add_dir.'/'.$v_stored_filename;
+ }
+
+ $v_stored_filename = $this->_pathReduction($v_stored_filename);
+
+ if ($this->_isArchive($p_filename)) {
+ if (($v_file = @fopen($p_filename, "rb")) == 0) {
+ $this->_warning("Unable to open file '".$p_filename
+ ."' in binary read mode");
+ return true;
+ }
+
+ if (!$this->_writeHeader($p_filename, $v_stored_filename))
+ return false;
+
+ while (($v_buffer = fread($v_file, 512)) != '') {
+ $v_binary_data = pack("a512", "$v_buffer");
+ $this->_writeBlock($v_binary_data);
+ }
+
+ fclose($v_file);
+
+ } else {
+ // ----- Only header for dir
+ if (!$this->_writeHeader($p_filename, $v_stored_filename))
+ return false;
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _addString()
+ function _addString($p_filename, $p_string)
+ {
+ if (!$this->_file) {
+ $this->_error('Invalid file descriptor');
+ return false;
+ }
+
+ if ($p_filename == '') {
+ $this->_error('Invalid file name');
+ return false;
+ }
+
+ // ----- Calculate the stored filename
+ $p_filename = $this->_translateWinPath($p_filename, false);;
+
+ if (!$this->_writeHeaderBlock($p_filename, strlen($p_string),
+ time(), 384, "", 0, 0))
+ return false;
+
+ $i=0;
+ while (($v_buffer = substr($p_string, (($i++)*512), 512)) != '') {
+ $v_binary_data = pack("a512", $v_buffer);
+ $this->_writeBlock($v_binary_data);
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _writeHeader()
+ function _writeHeader($p_filename, $p_stored_filename)
+ {
+ if ($p_stored_filename == '')
+ $p_stored_filename = $p_filename;
+ $v_reduce_filename = $this->_pathReduction($p_stored_filename);
+
+ if (strlen($v_reduce_filename) > 99) {
+ if (!$this->_writeLongHeader($v_reduce_filename))
+ return false;
+ }
+
+ $v_info = lstat($p_filename);
+ $v_uid = sprintf("%07s", DecOct($v_info[4]));
+ $v_gid = sprintf("%07s", DecOct($v_info[5]));
+ $v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777));
+
+ $v_mtime = sprintf("%011s", DecOct($v_info['mtime']));
+
+ $v_linkname = '';
+
+ if (@is_link($p_filename)) {
+ $v_typeflag = '2';
+ $v_linkname = readlink($p_filename);
+ $v_size = sprintf("%011s", DecOct(0));
+ } elseif (@is_dir($p_filename)) {
+ $v_typeflag = "5";
+ $v_size = sprintf("%011s", DecOct(0));
+ } else {
+ $v_typeflag = '0';
+ clearstatcache();
+ $v_size = sprintf("%011s", DecOct($v_info['size']));
+ }
+
+ $v_magic = 'ustar ';
+
+ $v_version = ' ';
+
+ if (function_exists('posix_getpwuid'))
+ {
+ $userinfo = posix_getpwuid($v_info[4]);
+ $groupinfo = posix_getgrgid($v_info[5]);
+
+ $v_uname = $userinfo['name'];
+ $v_gname = $groupinfo['name'];
+ }
+ else
+ {
+ $v_uname = '';
+ $v_gname = '';
+ }
+
+ $v_devmajor = '';
+
+ $v_devminor = '';
+
+ $v_prefix = '';
+
+ $v_binary_data_first = pack("a100a8a8a8a12a12",
+ $v_reduce_filename, $v_perms, $v_uid,
+ $v_gid, $v_size, $v_mtime);
+ $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12",
+ $v_typeflag, $v_linkname, $v_magic,
+ $v_version, $v_uname, $v_gname,
+ $v_devmajor, $v_devminor, $v_prefix, '');
+
+ // ----- Calculate the checksum
+ $v_checksum = 0;
+ // ..... First part of the header
+ for ($i=0; $i<148; $i++)
+ $v_checksum += ord(substr($v_binary_data_first,$i,1));
+ // ..... Ignore the checksum value and replace it by ' ' (space)
+ for ($i=148; $i<156; $i++)
+ $v_checksum += ord(' ');
+ // ..... Last part of the header
+ for ($i=156, $j=0; $i<512; $i++, $j++)
+ $v_checksum += ord(substr($v_binary_data_last,$j,1));
+
+ // ----- Write the first 148 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_first, 148);
+
+ // ----- Write the calculated checksum
+ $v_checksum = sprintf("%06s ", DecOct($v_checksum));
+ $v_binary_data = pack("a8", $v_checksum);
+ $this->_writeBlock($v_binary_data, 8);
+
+ // ----- Write the last 356 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_last, 356);
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _writeHeaderBlock()
+ function _writeHeaderBlock($p_filename, $p_size, $p_mtime=0, $p_perms=0,
+ $p_type='', $p_uid=0, $p_gid=0)
+ {
+ $p_filename = $this->_pathReduction($p_filename);
+
+ if (strlen($p_filename) > 99) {
+ if (!$this->_writeLongHeader($p_filename))
+ return false;
+ }
+
+ if ($p_type == "5") {
+ $v_size = sprintf("%011s", DecOct(0));
+ } else {
+ $v_size = sprintf("%011s", DecOct($p_size));
+ }
+
+ $v_uid = sprintf("%07s", DecOct($p_uid));
+ $v_gid = sprintf("%07s", DecOct($p_gid));
+ $v_perms = sprintf("%07s", DecOct($p_perms & 000777));
+
+ $v_mtime = sprintf("%11s", DecOct($p_mtime));
+
+ $v_linkname = '';
+
+ $v_magic = 'ustar ';
+
+ $v_version = ' ';
+
+ if (function_exists('posix_getpwuid'))
+ {
+ $userinfo = posix_getpwuid($p_uid);
+ $groupinfo = posix_getgrgid($p_gid);
+
+ $v_uname = $userinfo['name'];
+ $v_gname = $groupinfo['name'];
+ }
+ else
+ {
+ $v_uname = '';
+ $v_gname = '';
+ }
+
+ $v_devmajor = '';
+
+ $v_devminor = '';
+
+ $v_prefix = '';
+
+ $v_binary_data_first = pack("a100a8a8a8a12A12",
+ $p_filename, $v_perms, $v_uid, $v_gid,
+ $v_size, $v_mtime);
+ $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12",
+ $p_type, $v_linkname, $v_magic,
+ $v_version, $v_uname, $v_gname,
+ $v_devmajor, $v_devminor, $v_prefix, '');
+
+ // ----- Calculate the checksum
+ $v_checksum = 0;
+ // ..... First part of the header
+ for ($i=0; $i<148; $i++)
+ $v_checksum += ord(substr($v_binary_data_first,$i,1));
+ // ..... Ignore the checksum value and replace it by ' ' (space)
+ for ($i=148; $i<156; $i++)
+ $v_checksum += ord(' ');
+ // ..... Last part of the header
+ for ($i=156, $j=0; $i<512; $i++, $j++)
+ $v_checksum += ord(substr($v_binary_data_last,$j,1));
+
+ // ----- Write the first 148 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_first, 148);
+
+ // ----- Write the calculated checksum
+ $v_checksum = sprintf("%06s ", DecOct($v_checksum));
+ $v_binary_data = pack("a8", $v_checksum);
+ $this->_writeBlock($v_binary_data, 8);
+
+ // ----- Write the last 356 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_last, 356);
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _writeLongHeader()
+ function _writeLongHeader($p_filename)
+ {
+ $v_size = sprintf("%11s ", DecOct(strlen($p_filename)));
+
+ $v_typeflag = 'L';
+
+ $v_linkname = '';
+
+ $v_magic = '';
+
+ $v_version = '';
+
+ $v_uname = '';
+
+ $v_gname = '';
+
+ $v_devmajor = '';
+
+ $v_devminor = '';
+
+ $v_prefix = '';
+
+ $v_binary_data_first = pack("a100a8a8a8a12a12",
+ '././@LongLink', 0, 0, 0, $v_size, 0);
+ $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12",
+ $v_typeflag, $v_linkname, $v_magic,
+ $v_version, $v_uname, $v_gname,
+ $v_devmajor, $v_devminor, $v_prefix, '');
+
+ // ----- Calculate the checksum
+ $v_checksum = 0;
+ // ..... First part of the header
+ for ($i=0; $i<148; $i++)
+ $v_checksum += ord(substr($v_binary_data_first,$i,1));
+ // ..... Ignore the checksum value and replace it by ' ' (space)
+ for ($i=148; $i<156; $i++)
+ $v_checksum += ord(' ');
+ // ..... Last part of the header
+ for ($i=156, $j=0; $i<512; $i++, $j++)
+ $v_checksum += ord(substr($v_binary_data_last,$j,1));
+
+ // ----- Write the first 148 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_first, 148);
+
+ // ----- Write the calculated checksum
+ $v_checksum = sprintf("%06s ", DecOct($v_checksum));
+ $v_binary_data = pack("a8", $v_checksum);
+ $this->_writeBlock($v_binary_data, 8);
+
+ // ----- Write the last 356 bytes of the header in the archive
+ $this->_writeBlock($v_binary_data_last, 356);
+
+ // ----- Write the filename as content of the block
+ $i=0;
+ while (($v_buffer = substr($p_filename, (($i++)*512), 512)) != '') {
+ $v_binary_data = pack("a512", "$v_buffer");
+ $this->_writeBlock($v_binary_data);
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _readHeader()
+ function _readHeader($v_binary_data, &$v_header)
+ {
+ if (strlen($v_binary_data)==0) {
+ $v_header['filename'] = '';
+ return true;
+ }
+
+ if (strlen($v_binary_data) != 512) {
+ $v_header['filename'] = '';
+ $this->_error('Invalid block size : '.strlen($v_binary_data));
+ return false;
+ }
+
+ if (!is_array($v_header)) {
+ $v_header = array();
+ }
+ // ----- Calculate the checksum
+ $v_checksum = 0;
+ // ..... First part of the header
+ for ($i=0; $i<148; $i++)
+ $v_checksum+=ord(substr($v_binary_data,$i,1));
+ // ..... Ignore the checksum value and replace it by ' ' (space)
+ for ($i=148; $i<156; $i++)
+ $v_checksum += ord(' ');
+ // ..... Last part of the header
+ for ($i=156; $i<512; $i++)
+ $v_checksum+=ord(substr($v_binary_data,$i,1));
+
+ $v_data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" .
+ "a8checksum/a1typeflag/a100link/a6magic/a2version/" .
+ "a32uname/a32gname/a8devmajor/a8devminor/a131prefix",
+ $v_binary_data);
+
+ if (strlen($v_data["prefix"]) > 0) {
+ $v_data["filename"] = "$v_data[prefix]/$v_data[filename]";
+ }
+
+ // ----- Extract the checksum
+ $v_header['checksum'] = OctDec(trim($v_data['checksum']));
+ if ($v_header['checksum'] != $v_checksum) {
+ $v_header['filename'] = '';
+
+ // ----- Look for last block (empty block)
+ if (($v_checksum == 256) && ($v_header['checksum'] == 0))
+ return true;
+
+ $this->_error('Invalid checksum for file "'.$v_data['filename']
+ .'" : '.$v_checksum.' calculated, '
+ .$v_header['checksum'].' expected');
+ return false;
+ }
+
+ // ----- Extract the properties
+ $v_header['filename'] = $v_data['filename'];
+ if ($this->_maliciousFilename($v_header['filename'])) {
+ $this->_error('Malicious .tar detected, file "' . $v_header['filename'] .
+ '" will not install in desired directory tree');
+ return false;
+ }
+ $v_header['mode'] = OctDec(trim($v_data['mode']));
+ $v_header['uid'] = OctDec(trim($v_data['uid']));
+ $v_header['gid'] = OctDec(trim($v_data['gid']));
+ $v_header['size'] = OctDec(trim($v_data['size']));
+ $v_header['mtime'] = OctDec(trim($v_data['mtime']));
+ if (($v_header['typeflag'] = $v_data['typeflag']) == "5") {
+ $v_header['size'] = 0;
+ }
+ $v_header['link'] = trim($v_data['link']);
+ /* ----- All these fields are removed form the header because
+ they do not carry interesting info
+ $v_header[magic] = trim($v_data[magic]);
+ $v_header[version] = trim($v_data[version]);
+ $v_header[uname] = trim($v_data[uname]);
+ $v_header[gname] = trim($v_data[gname]);
+ $v_header[devmajor] = trim($v_data[devmajor]);
+ $v_header[devminor] = trim($v_data[devminor]);
+ */
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _maliciousFilename()
+ /**
+ * Detect and report a malicious file name
+ *
+ * @param string $file
+ *
+ * @return bool
+ * @access private
+ */
+ function _maliciousFilename($file)
+ {
+ if (strpos($file, '/../') !== false) {
+ return true;
+ }
+ if (strpos($file, '../') === 0) {
+ return true;
+ }
+ return false;
+ }
+ // }}}
+
+ // {{{ _readLongHeader()
+ function _readLongHeader(&$v_header)
+ {
+ $v_filename = '';
+ $n = floor($v_header['size']/512);
+ for ($i=0; $i<$n; $i++) {
+ $v_content = $this->_readBlock();
+ $v_filename .= $v_content;
+ }
+ if (($v_header['size'] % 512) != 0) {
+ $v_content = $this->_readBlock();
+ $v_filename .= trim($v_content);
+ }
+
+ // ----- Read the next header
+ $v_binary_data = $this->_readBlock();
+
+ if (!$this->_readHeader($v_binary_data, $v_header))
+ return false;
+
+ $v_filename = trim($v_filename);
+ $v_header['filename'] = $v_filename;
+ if ($this->_maliciousFilename($v_filename)) {
+ $this->_error('Malicious .tar detected, file "' . $v_filename .
+ '" will not install in desired directory tree');
+ return false;
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _extractInString()
+ /**
+ * This method extract from the archive one file identified by $p_filename.
+ * The return value is a string with the file content, or null on error.
+ *
+ * @param string $p_filename The path of the file to extract in a string.
+ *
+ * @return a string with the file content or null.
+ * @access private
+ */
+ function _extractInString($p_filename)
+ {
+ $v_result_str = "";
+
+ While (strlen($v_binary_data = $this->_readBlock()) != 0)
+ {
+ if (!$this->_readHeader($v_binary_data, $v_header))
+ return null;
+
+ if ($v_header['filename'] == '')
+ continue;
+
+ // ----- Look for long filename
+ if ($v_header['typeflag'] == 'L') {
+ if (!$this->_readLongHeader($v_header))
+ return null;
+ }
+
+ if ($v_header['filename'] == $p_filename) {
+ if ($v_header['typeflag'] == "5") {
+ $this->_error('Unable to extract in string a directory '
+ .'entry {'.$v_header['filename'].'}');
+ return null;
+ } else {
+ $n = floor($v_header['size']/512);
+ for ($i=0; $i<$n; $i++) {
+ $v_result_str .= $this->_readBlock();
+ }
+ if (($v_header['size'] % 512) != 0) {
+ $v_content = $this->_readBlock();
+ $v_result_str .= substr($v_content, 0,
+ ($v_header['size'] % 512));
+ }
+ return $v_result_str;
+ }
+ } else {
+ $this->_jumpBlock(ceil(($v_header['size']/512)));
+ }
+ }
+
+ return null;
+ }
+ // }}}
+
+ // {{{ _extractList()
+ function _extractList($p_path, &$p_list_detail, $p_mode,
+ $p_file_list, $p_remove_path, $p_preserve=false)
+ {
+ $v_result=true;
+ $v_nb = 0;
+ $v_extract_all = true;
+ $v_listing = false;
+
+ $p_path = $this->_translateWinPath($p_path, false);
+ if ($p_path == '' || (substr($p_path, 0, 1) != '/'
+ && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':'))) {
+ $p_path = "./".$p_path;
+ }
+ $p_remove_path = $this->_translateWinPath($p_remove_path);
+
+ // ----- Look for path to remove format (should end by /)
+ if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/'))
+ $p_remove_path .= '/';
+ $p_remove_path_size = strlen($p_remove_path);
+
+ switch ($p_mode) {
+ case "complete" :
+ $v_extract_all = true;
+ $v_listing = false;
+ break;
+ case "partial" :
+ $v_extract_all = false;
+ $v_listing = false;
+ break;
+ case "list" :
+ $v_extract_all = false;
+ $v_listing = true;
+ break;
+ default :
+ $this->_error('Invalid extract mode ('.$p_mode.')');
+ return false;
+ }
+
+ clearstatcache();
+
+ while (strlen($v_binary_data = $this->_readBlock()) != 0)
+ {
+ $v_extract_file = FALSE;
+ $v_extraction_stopped = 0;
+
+ if (!$this->_readHeader($v_binary_data, $v_header))
+ return false;
+
+ if ($v_header['filename'] == '') {
+ continue;
+ }
+
+ // ----- Look for long filename
+ if ($v_header['typeflag'] == 'L') {
+ if (!$this->_readLongHeader($v_header))
+ return false;
+ }
+
+ if ((!$v_extract_all) && (is_array($p_file_list))) {
+ // ----- By default no unzip if the file is not found
+ $v_extract_file = false;
+
+ for ($i=0; $i<sizeof($p_file_list); $i++) {
+ // ----- Look if it is a directory
+ if (substr($p_file_list[$i], -1) == '/') {
+ // ----- Look if the directory is in the filename path
+ if ((strlen($v_header['filename']) > strlen($p_file_list[$i]))
+ && (substr($v_header['filename'], 0, strlen($p_file_list[$i]))
+ == $p_file_list[$i])) {
+ $v_extract_file = true;
+ break;
+ }
+ }
+
+ // ----- It is a file, so compare the file names
+ elseif ($p_file_list[$i] == $v_header['filename']) {
+ $v_extract_file = true;
+ break;
+ }
+ }
+ } else {
+ $v_extract_file = true;
+ }
+
+ // ----- Look if this file need to be extracted
+ if (($v_extract_file) && (!$v_listing))
+ {
+ if (($p_remove_path != '')
+ && (substr($v_header['filename'], 0, $p_remove_path_size)
+ == $p_remove_path))
+ $v_header['filename'] = substr($v_header['filename'],
+ $p_remove_path_size);
+ if (($p_path != './') && ($p_path != '/')) {
+ while (substr($p_path, -1) == '/')
+ $p_path = substr($p_path, 0, strlen($p_path)-1);
+
+ if (substr($v_header['filename'], 0, 1) == '/')
+ $v_header['filename'] = $p_path.$v_header['filename'];
+ else
+ $v_header['filename'] = $p_path.'/'.$v_header['filename'];
+ }
+ if (file_exists($v_header['filename'])) {
+ if ( (@is_dir($v_header['filename']))
+ && ($v_header['typeflag'] == '')) {
+ $this->_error('File '.$v_header['filename']
+ .' already exists as a directory');
+ return false;
+ }
+ if ( ($this->_isArchive($v_header['filename']))
+ && ($v_header['typeflag'] == "5")) {
+ $this->_error('Directory '.$v_header['filename']
+ .' already exists as a file');
+ return false;
+ }
+ if (!is_writeable($v_header['filename'])) {
+ $this->_error('File '.$v_header['filename']
+ .' already exists and is write protected');
+ return false;
+ }
+ if (filemtime($v_header['filename']) > $v_header['mtime']) {
+ // To be completed : An error or silent no replace ?
+ }
+ }
+
+ // ----- Check the directory availability and create it if necessary
+ elseif (($v_result
+ = $this->_dirCheck(($v_header['typeflag'] == "5"
+ ?$v_header['filename']
+ :dirname($v_header['filename'])))) != 1) {
+ $this->_error('Unable to create path for '.$v_header['filename']);
+ return false;
+ }
+
+ if ($v_extract_file) {
+ if ($v_header['typeflag'] == "5") {
+ if (!@file_exists($v_header['filename'])) {
+ if (!@mkdir($v_header['filename'], 0777)) {
+ $this->_error('Unable to create directory {'
+ .$v_header['filename'].'}');
+ return false;
+ }
+ }
+ } elseif ($v_header['typeflag'] == "2") {
+ if (@file_exists($v_header['filename'])) {
+ @unlink($v_header['filename']);
+ }
+ if (!@symlink($v_header['link'], $v_header['filename'])) {
+ $this->_error('Unable to extract symbolic link {'
+ .$v_header['filename'].'}');
+ return false;
+ }
+ } else {
+ if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) {
+ $this->_error('Error while opening {'.$v_header['filename']
+ .'} in write binary mode');
+ return false;
+ } else {
+ $n = floor($v_header['size']/512);
+ for ($i=0; $i<$n; $i++) {
+ $v_content = $this->_readBlock();
+ fwrite($v_dest_file, $v_content, 512);
+ }
+ if (($v_header['size'] % 512) != 0) {
+ $v_content = $this->_readBlock();
+ fwrite($v_dest_file, $v_content, ($v_header['size'] % 512));
+ }
+
+ @fclose($v_dest_file);
+
+ if ($p_preserve) {
+ @chown($v_header['filename'], $v_header['uid']);
+ @chgrp($v_header['filename'], $v_header['gid']);
+ }
+
+ // ----- Change the file mode, mtime
+ @touch($v_header['filename'], $v_header['mtime']);
+ if ($v_header['mode'] & 0111) {
+ // make file executable, obey umask
+ $mode = fileperms($v_header['filename']) | (~umask() & 0111);
+ @chmod($v_header['filename'], $mode);
+ }
+ }
+
+ // ----- Check the file size
+ clearstatcache();
+ if (!is_file($v_header['filename'])) {
+ $this->_error('Extracted file '.$v_header['filename']
+ .'does not exist. Archive may be corrupted.');
+ return false;
+ }
+
+ $filesize = filesize($v_header['filename']);
+ if ($filesize != $v_header['size']) {
+ $this->_error('Extracted file '.$v_header['filename']
+ .' does not have the correct file size \''
+ .$filesize
+ .'\' ('.$v_header['size']
+ .' expected). Archive may be corrupted.');
+ return false;
+ }
+ }
+ } else {
+ $this->_jumpBlock(ceil(($v_header['size']/512)));
+ }
+ } else {
+ $this->_jumpBlock(ceil(($v_header['size']/512)));
+ }
+
+ /* TBC : Seems to be unused ...
+ if ($this->_compress)
+ $v_end_of_file = @gzeof($this->_file);
+ else
+ $v_end_of_file = @feof($this->_file);
+ */
+
+ if ($v_listing || $v_extract_file || $v_extraction_stopped) {
+ // ----- Log extracted files
+ if (($v_file_dir = dirname($v_header['filename']))
+ == $v_header['filename'])
+ $v_file_dir = '';
+ if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == ''))
+ $v_file_dir = '/';
+
+ $p_list_detail[$v_nb++] = $v_header;
+ if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) {
+ return true;
+ }
+ }
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _openAppend()
+ function _openAppend()
+ {
+ if (filesize($this->_tarname) == 0)
+ return $this->_openWrite();
+
+ if ($this->_compress) {
+ $this->_close();
+
+ if (!@rename($this->_tarname, $this->_tarname.".tmp")) {
+ $this->_error('Error while renaming \''.$this->_tarname
+ .'\' to temporary file \''.$this->_tarname
+ .'.tmp\'');
+ return false;
+ }
+
+ if ($this->_compress_type == 'gz')
+ $v_temp_tar = @gzopen($this->_tarname.".tmp", "rb");
+ elseif ($this->_compress_type == 'bz2')
+ $v_temp_tar = @bzopen($this->_tarname.".tmp", "r");
+
+ if ($v_temp_tar == 0) {
+ $this->_error('Unable to open file \''.$this->_tarname
+ .'.tmp\' in binary read mode');
+ @rename($this->_tarname.".tmp", $this->_tarname);
+ return false;
+ }
+
+ if (!$this->_openWrite()) {
+ @rename($this->_tarname.".tmp", $this->_tarname);
+ return false;
+ }
+
+ if ($this->_compress_type == 'gz') {
+ $end_blocks = 0;
+
+ while (!@gzeof($v_temp_tar)) {
+ $v_buffer = @gzread($v_temp_tar, 512);
+ if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) {
+ $end_blocks++;
+ // do not copy end blocks, we will re-make them
+ // after appending
+ continue;
+ } elseif ($end_blocks > 0) {
+ for ($i = 0; $i < $end_blocks; $i++) {
+ $this->_writeBlock(ARCHIVE_TAR_END_BLOCK);
+ }
+ $end_blocks = 0;
+ }
+ $v_binary_data = pack("a512", $v_buffer);
+ $this->_writeBlock($v_binary_data);
+ }
+
+ @gzclose($v_temp_tar);
+ }
+ elseif ($this->_compress_type == 'bz2') {
+ $end_blocks = 0;
+
+ while (strlen($v_buffer = @bzread($v_temp_tar, 512)) > 0) {
+ if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) {
+ $end_blocks++;
+ // do not copy end blocks, we will re-make them
+ // after appending
+ continue;
+ } elseif ($end_blocks > 0) {
+ for ($i = 0; $i < $end_blocks; $i++) {
+ $this->_writeBlock(ARCHIVE_TAR_END_BLOCK);
+ }
+ $end_blocks = 0;
+ }
+ $v_binary_data = pack("a512", $v_buffer);
+ $this->_writeBlock($v_binary_data);
+ }
+
+ @bzclose($v_temp_tar);
+ }
+
+ if (!@unlink($this->_tarname.".tmp")) {
+ $this->_error('Error while deleting temporary file \''
+ .$this->_tarname.'.tmp\'');
+ }
+
+ } else {
+ // ----- For not compressed tar, just add files before the last
+ // one or two 512 bytes block
+ if (!$this->_openReadWrite())
+ return false;
+
+ clearstatcache();
+ $v_size = filesize($this->_tarname);
+
+ // We might have zero, one or two end blocks.
+ // The standard is two, but we should try to handle
+ // other cases.
+ fseek($this->_file, $v_size - 1024);
+ if (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) {
+ fseek($this->_file, $v_size - 1024);
+ }
+ elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) {
+ fseek($this->_file, $v_size - 512);
+ }
+ }
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _append()
+ function _append($p_filelist, $p_add_dir='', $p_remove_dir='')
+ {
+ if (!$this->_openAppend())
+ return false;
+
+ if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir))
+ $this->_writeFooter();
+
+ $this->_close();
+
+ return true;
+ }
+ // }}}
+
+ // {{{ _dirCheck()
+
+ /**
+ * Check if a directory exists and create it (including parent
+ * dirs) if not.
+ *
+ * @param string $p_dir directory to check
+ *
+ * @return bool true if the directory exists or was created
+ */
+ function _dirCheck($p_dir)
+ {
+ clearstatcache();
+ if ((@is_dir($p_dir)) || ($p_dir == ''))
+ return true;
+
+ $p_parent_dir = dirname($p_dir);
+
+ if (($p_parent_dir != $p_dir) &&
+ ($p_parent_dir != '') &&
+ (!$this->_dirCheck($p_parent_dir)))
+ return false;
+
+ if (!@mkdir($p_dir, 0777)) {
+ $this->_error("Unable to create directory '$p_dir'");
+ return false;
+ }
+
+ return true;
+ }
+
+ // }}}
+
+ // {{{ _pathReduction()
+
+ /**
+ * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar",
+ * rand emove double slashes.
+ *
+ * @param string $p_dir path to reduce
+ *
+ * @return string reduced path
+ *
+ * @access private
+ *
+ */
+ function _pathReduction($p_dir)
+ {
+ $v_result = '';
+
+ // ----- Look for not empty path
+ if ($p_dir != '') {
+ // ----- Explode path by directory names
+ $v_list = explode('/', $p_dir);
+
+ // ----- Study directories from last to first
+ for ($i=sizeof($v_list)-1; $i>=0; $i--) {
+ // ----- Look for current path
+ if ($v_list[$i] == ".") {
+ // ----- Ignore this directory
+ // Should be the first $i=0, but no check is done
+ }
+ else if ($v_list[$i] == "..") {
+ // ----- Ignore it and ignore the $i-1
+ $i--;
+ }
+ else if ( ($v_list[$i] == '')
+ && ($i!=(sizeof($v_list)-1))
+ && ($i!=0)) {
+ // ----- Ignore only the double '//' in path,
+ // but not the first and last /
+ } else {
+ $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?'/'
+ .$v_result:'');
+ }
+ }
+ }
+
+ if (defined('OS_WINDOWS') && OS_WINDOWS) {
+ $v_result = strtr($v_result, '\\', '/');
+ }
+
+ return $v_result;
+ }
+
+ // }}}
+
+ // {{{ _translateWinPath()
+ function _translateWinPath($p_path, $p_remove_disk_letter=true)
+ {
+ if (defined('OS_WINDOWS') && OS_WINDOWS) {
+ // ----- Look for potential disk letter
+ if ( ($p_remove_disk_letter)
+ && (($v_position = strpos($p_path, ':')) != false)) {
+ $p_path = substr($p_path, $v_position+1);
+ }
+ // ----- Change potential windows directory separator
+ if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) {
+ $p_path = strtr($p_path, '\\', '/');
+ }
+ }
+ return $p_path;
+ }
+ // }}}
+
+}
+?>
diff --git a/plugins/UserCountry/Controller.php b/plugins/UserCountry/Controller.php
index e7ed5c458a..1df06697c8 100644
--- a/plugins/UserCountry/Controller.php
+++ b/plugins/UserCountry/Controller.php
@@ -14,7 +14,7 @@
*
* @package Piwik_UserCountry
*/
-class Piwik_UserCountry_Controller extends Piwik_Controller
+class Piwik_UserCountry_Controller extends Piwik_Controller_Admin
{
function index()
{
@@ -41,6 +41,7 @@ class Piwik_UserCountry_Controller extends Piwik_Controller
$view->locationProviders = $allProviderInfo;
$view->currentProviderId = Piwik_UserCountry_LocationProvider::getCurrentProviderId();
$view->thisIP = Piwik_IP::getIpFromHeader();
+ $view->geoIPDatabasesInstalled = Piwik_UserCountry_LocationProvider_GeoIp::isDatabaseInstalled();
// check if there is a working provider (that isn't the default one)
$isThereWorkingProvider = false;
@@ -54,6 +55,20 @@ class Piwik_UserCountry_Controller extends Piwik_Controller
}
}
$view->isThereWorkingProvider = $isThereWorkingProvider;
+
+ // if using either the Apache or PECL module, they are working and there are no databases
+ // in misc, then the databases are located outside of Piwik, so we cannot update them
+ $view->showGeoIPUpdateSection = true;
+ $currentProviderId = Piwik_UserCountry_LocationProvider::getCurrentProviderId();
+ if (!$view->geoIPDatabasesInstalled
+ && ($currentProviderId == Piwik_UserCountry_LocationProvider_GeoIp_ServerBased::ID
+ || $currentProviderId == Piwik_UserCountry_LocationProvider_GeoIp_Pecl::ID)
+ && $allProviderInfo[$currentProviderId]['status'] == Piwik_UserCountry_LocationProvider::INSTALLED)
+ {
+ $view->showGeoIPUpdateSection = false;
+ }
+
+ $this->setUpdaterManageVars($view);
$this->setBasicVariablesView($view);
Piwik_Controller_Admin::setBasicVariablesAdminView($view);
$view->menu = Piwik_GetAdminMenu();
@@ -62,6 +77,197 @@ class Piwik_UserCountry_Controller extends Piwik_Controller
}
/**
+ * Starts or continues download of GeoLiteCity.dat.
+ *
+ * To avoid a server/PHP timeout & to show progress of the download to the user, we
+ * use the HTTP Range header to download one chunk of the file at a time. After each
+ * chunk, it is the browser's responsibility to call the method again to continue the download.
+ *
+ * Input:
+ * 'continue' query param - if set to 1, will assume we are currently downloading & use
+ * Range: HTTP header to get another chunk of the file.
+ *
+ * Output (in JSON):
+ * 'current_size' - Current size of the partially downloaded file on disk.
+ * 'expected_file_size' - The expected finished file size as returned by the HTTP server.
+ * 'next_screen' - When the download finishes, this is the next screen that should be shown.
+ * 'error' - When an error occurs, the message is returned in this property.
+ */
+ public function downloadFreeGeoIPDB()
+ {
+ Piwik::checkUserIsSuperUser();
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ $this->checkTokenInUrl();
+
+ $outputPath = Piwik_UserCountry_LocationProvider_GeoIp::getPathForGeoIpDatabase('GeoIPCity.dat').'.gz';
+ try
+ {
+ $result = Piwik_Http::downloadChunk(
+ $url = Piwik_UserCountry_LocationProvider_GeoIp::GEO_LITE_URL,
+ $outputPath,
+ $continue = Piwik_Common::getRequestVar('continue', true, 'int')
+ );
+
+ // if the file is done
+ if ($result['current_size'] >= $result['expected_file_size'])
+ {
+ Piwik_UserCountry_GeoIPAutoUpdater::unzipDownloadedFile($outputPath, $unlink = true);
+
+ // setup the auto updater
+ Piwik_UserCountry_GeoIPAutoUpdater::setUpdaterOptions(array(
+ 'loc_db' => Piwik_UserCountry_LocationProvider_GeoIp::GEO_LITE_URL,
+ 'period' => Piwik_UserCountry_GeoIPAutoUpdater::SCHEDULE_PERIOD_MONTHLY,
+ ));
+
+ // make sure to echo out the geoip updater management screen
+ $result['next_screen'] = $this->getGeoIpUpdaterManageScreen();
+ }
+
+ echo json_encode($result);
+ }
+ catch (Exception $ex)
+ {
+ echo json_encode(array('error' => $ex->getMessage()));
+ }
+ }
+ }
+
+ /**
+ * Renders and returns the HTML that manages the GeoIP auto-updater.
+ *
+ * @return string
+ */
+ private function getGeoIpUpdaterManageScreen()
+ {
+ $view = Piwik_View::factory('updaterSetup');
+ $view->geoIPDatabasesInstalled = true;
+ $this->setUpdaterManageVars($view);
+ return $view->render();
+ }
+
+ /**
+ * Sets some variables needed by the updaterSetup.tpl template.
+ *
+ * @param Piwik_View $view
+ */
+ private function setUpdaterManageVars( $view )
+ {
+ $urls = Piwik_UserCountry_GeoIPAutoUpdater::getConfiguredUrls();
+
+ $view->geoIPLocUrl = $urls['loc'];
+ $view->geoIPIspUrl = $urls['isp'];
+ $view->geoIPOrgUrl = $urls['org'];
+ $view->geoIPUpdatePeriod = Piwik_UserCountry_GeoIPAutoUpdater::getSchedulePeriod();
+ }
+
+ /**
+ * Sets the URLs used to download new versions of the installed GeoIP databases.
+ *
+ * Input (query params):
+ * 'loc_db' - URL for a GeoIP location database.
+ * 'isp_db' - URL for a GeoIP ISP database (optional).
+ * 'org_db' - URL for a GeoIP Org database (optional).
+ * 'period' - 'weekly' or 'monthly'. Determines how often update is run.
+ *
+ * Output (json):
+ * 'error' - if an error occurs its message is set as the resulting JSON object's
+ * 'error' property.
+ */
+ public function updateGeoIPLinks()
+ {
+ Piwik::checkUserIsSuperUser();
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ try
+ {
+ $this->checkTokenInUrl();
+
+ Piwik_UserCountry_GeoIPAutoUpdater::setUpdaterOptionsFromUrl();
+
+ // if there is a updater URL for a database, but its missing from the misc dir, tell
+ // the browser so it can download it next
+ $info = $this->getNextMissingDbUrlInfo();
+ if ($info !== false)
+ {
+ echo json_encode($info);
+ return;
+ }
+ }
+ catch (Exception $ex)
+ {
+ echo json_encode(array('error' => $ex->getMessage()));
+ }
+ }
+ }
+
+ /**
+ * Starts or continues a download for a missing GeoIP database. A database is missing if
+ * it has an update URL configured, but the actual database is not available in the misc
+ * directory.
+ *
+ * Input:
+ * 'url' - The URL to download the database from.
+ * 'continue' - 1 if we're continuing a download, 0 if we're starting one.
+ *
+ * Output:
+ * 'error' - If an error occurs this describes the error.
+ * 'to_download' - The URL of a missing database that should be downloaded next (if any).
+ * 'to_download_label' - The label to use w/ the progress bar that describes what we're
+ * downloading.
+ * 'current_size' - Size of the current file on disk.
+ * 'expected_file_size' - Size of the completely downloaded file.
+ */
+ public function downloadMissingGeoIpDb()
+ {
+ Piwik::checkUserIsSuperUser();
+ if ($_SERVER["REQUEST_METHOD"] == "POST")
+ {
+ try
+ {
+ $this->checkTokenInUrl();
+
+ // based on the database type (provided by the 'key' query param) determine the
+ // url & output file name
+ $key = Piwik_Common::getRequestVar('key', null, 'string');
+ $url = Piwik_UserCountry_GeoIPAutoUpdater::getConfiguredUrl($key);
+
+ $ext = Piwik_UserCountry_GeoIPAutoUpdater::getGeoIPUrlExtension($url);
+ $filename = Piwik_UserCountry_LocationProvider_GeoIp::$dbNames[$key][0].'.'.$ext;
+
+ if (substr($filename, 0, 15) == 'GeoLiteCity.dat')
+ {
+ $filename = 'GeoIPCity.dat'.substr($filename, 15);
+ }
+ $outputPath = Piwik_UserCountry_LocationProvider_GeoIp::getPathForGeoIpDatabase($filename);
+
+ // download part of the file
+ $result = Piwik_Http::downloadChunk(
+ $url, $outputPath, Piwik_Common::getRequestVar('continue', true, 'int'));
+
+ // if the file is done
+ if ($result['current_size'] >= $result['expected_file_size'])
+ {
+ Piwik_UserCountry_GeoIPAutoUpdater::unzipDownloadedFile($outputPath, $unlink = true);
+
+ $info = $this->getNextMissingDbUrlInfo();
+ if ($info !== false)
+ {
+ echo json_encode($info);
+ return;
+ }
+ }
+
+ echo json_encode($result);
+ }
+ catch (Exception $ex)
+ {
+ echo json_encode(array('error' => $ex->getMessage()));
+ }
+ }
+ }
+
+ /**
* Sets the current LocationProvider type.
*
* Input:
@@ -243,4 +449,25 @@ class Piwik_UserCountry_Controller extends Piwik_Controller
$realView->properties = $properties;
}
}
+
+ /**
+ * Gets information for the first missing GeoIP database (if any).
+ *
+ * @return bool
+ */
+ private function getNextMissingDbUrlInfo()
+ {
+ $missingDbs = Piwik_UserCountry_GeoIPAutoUpdater::getMissingDatabases();
+ if (!empty($missingDbs))
+ {
+ $missingDbKey = $missingDbs[0];
+ $missingDbName = Piwik_UserCountry_LocationProvider_GeoIp::$dbNames[$missingDbKey][0];
+
+ return array(
+ 'to_download' => $missingDbKey,
+ 'to_download_label' => Piwik_Translate('UserCountry_DownloadingDb', $missingDbName).'...',
+ );
+ }
+ return false;
+ }
}
diff --git a/plugins/UserCountry/GeoIPAutoUpdater.php b/plugins/UserCountry/GeoIPAutoUpdater.php
new file mode 100755
index 0000000000..391a226937
--- /dev/null
+++ b/plugins/UserCountry/GeoIPAutoUpdater.php
@@ -0,0 +1,397 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ * @version $Id$
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_UserCountry
+ */
+
+/**
+ * Used to automatically update installed GeoIP databases, and manages the updater's
+ * scheduled task.
+ */
+class Piwik_UserCountry_GeoIPAutoUpdater
+{
+ const SCHEDULE_PERIOD_MONTHLY = 'month';
+ const SCHEDULE_PERIOD_WEEKLY = 'week';
+
+ const SCHEDULE_PERIOD_OPTION_NAME = 'geoip.updater_period';
+ const LOC_URL_OPTION_NAME = 'geoip.loc_db_url';
+ const ISP_URL_OPTION_NAME = 'geoip.isp_db_url';
+ const ORG_URL_OPTION_NAME = 'geoip.org_db_url';
+
+ private static $urlOptions = array(
+ 'loc' => self::LOC_URL_OPTION_NAME,
+ 'isp' => self::ISP_URL_OPTION_NAME,
+ 'org' => self::ORG_URL_OPTION_NAME,
+ );
+
+ /**
+ * Attempts to download new location, ISP & organization GeoIP databases and
+ * replace the existing ones w/ them.
+ */
+ public function update()
+ {
+ try
+ {
+ $locUrl = Piwik_GetOption(self::LOC_URL_OPTION_NAME);
+ if ($locUrl !== false)
+ {
+ $this->downloadFile('loc', $locUrl);
+ }
+
+ $ispUrl = Piwik_GetOption(self::ISP_URL_OPTION_NAME);
+ if ($ispUrl !== false)
+ {
+ $this->downloadFile('isp', $ispUrl);
+ }
+
+ $orgUrl = Piwik_GetOption(self::ORG_URL_OPTION_NAME);
+ if ($orgUrl !== false)
+ {
+ $this->downloadFile('org', $orgUrl);
+ }
+ }
+ catch (Exception $ex)
+ {
+ // message will already be prefixed w/ 'Piwik_UserCountry_GeoIPAutoUpdater: '
+ Piwik::log($ex->getMessage());
+ throw $ex;
+ }
+ }
+
+ /**
+ * Downloads a GeoIP database archive, extracts the .dat file and overwrites the existing
+ * old database.
+ *
+ * If something happens that causes the download to fail, no exception is thrown, but
+ * an error is logged.
+ *
+ * @param string $url URL to the database to download. The type of database is determined
+ * from this URL.
+ */
+ private function downloadFile( $dbType, $url )
+ {
+ $ext = Piwik_UserCountry_GeoIPAutoUpdater::getGeoIPUrlExtension($url);
+ $zippedFilename = Piwik_UserCountry_LocationProvider_GeoIp::$dbNames[$dbType][0].'.'.$ext;
+
+ $zippedOutputPath = Piwik_UserCountry_LocationProvider_GeoIp::getPathForGeoIpDatabase($zippedFilename);
+
+ // download zipped file to misc dir
+ try
+ {
+ $success = Piwik_Http::sendHttpRequest($url, $timeout = 3600, $userAgent = null, $zippedOutputPath);
+ }
+ catch (Exception $ex)
+ {
+ throw new Exception("Piwik_UserCountry_GeoIPAutoUpdater: failed to download '$url' to "
+ . "'$zippedOutputPath': " . $ex->getMessage());
+ }
+
+ if ($success !== true)
+ {
+ throw new Exception("Piwik_UserCountry_GeoIPAutoUpdater: failed to download '$url' to "
+ . "'$zippedOutputPath'! (Unknown error)");
+ }
+
+ Piwik::log("Piwik_UserCountry_GeoIPAutoUpdater: successfully downloaded '$url'");
+
+ try
+ {
+ self::unzipDownloadedFile($zippedOutputPath, $unlink = true);
+ }
+ catch (Exception $ex)
+ {
+ throw new Exception("Piwik_UserCountry_GeoIPAutoUpdater: failed to unzip '$zippedOutputPath' after "
+ . "downloading " . "'$url': ".$ex->getMessage());
+ }
+
+ Piwik::log("Piwik_UserCountry_GeoIPAutoUpdater: successfully updated GeoIP database '$url'");
+ }
+
+ /**
+ * Unzips a downloaded GeoIP database. Only unzips .gz & .tar.gz files.
+ *
+ * @param string $path Path to zipped file.
+ * @param bool $unlink Whether to unlink archive or not.
+ */
+ public static function unzipDownloadedFile( $path, $unlink = false )
+ {
+ $parts = explode('.', basename($path));
+ $outputPath = Piwik_UserCountry_LocationProvider_GeoIp::getPathForGeoIpDatabase($parts[0].'.dat');
+
+ // extract file
+ if (substr($path, -7, 7) == '.tar.gz')
+ {
+ // find the .dat file in the tar archive
+ $unzip = Piwik_Unzip::factory('tar.gz', $path);
+ $content = $unzip->listContent();
+
+ if (empty($content))
+ {
+ throw new Exception(Piwik_Translate('UserCountry_CannotListContent',
+ array("'$path'", $unzip->errorInfo())));
+ }
+
+ $datFile = null;
+ foreach ($content as $info)
+ {
+ $archivedPath = $info['filename'];
+ if (basename($archivedPath) === basename($outputPath))
+ {
+ $datFile = $archivedPath;
+ }
+ }
+
+ if ($datFile === null)
+ {
+ throw new Exception(Piwik_Translate('UserCountry_CannotFindGeoIPDatabaseInArchive',
+ array(basename($outputPath), "'$path'")));
+ }
+
+ // extract JUST the .dat file
+ $unzipped = $unzip->extractInString($datFile);
+
+ if (empty($unzipped))
+ {
+ throw new Exception(Piwik_Translate('UserCountry_CannotUnzipDatFile',
+ array("'$path'", $unzip->errorInfo())));
+ }
+
+ // write unzipped to file
+ $fd = fopen($outputPath, 'wb');
+ fwrite($fd, $unzipped);
+ fclose($fd);
+ }
+ else if (substr($path, -3, 3) == '.gz')
+ {
+ $unzip = Piwik_Unzip::factory('gz', $path);
+ $success = $unzip->extract($outputPath);
+
+ if ($success !== true)
+ {
+ throw new Exception(Piwik_Translate('UserCountry_CannotUnzipDatFile',
+ array("'$path'", $unzip->errorInfo())));
+ }
+ }
+ else
+ {
+ $ext = end(explode(basename($path), '.', 2));
+ throw new Exception(Piwik_Translate('UserCountry_UnsupportedArchiveType', "'$ext'"));
+ }
+
+ // delete original archive
+ if ($unlink)
+ {
+ unlink($path);
+ }
+ }
+
+ /**
+ * Creates a ScheduledTask instance based on set option values.
+ *
+ * @return Piwik_ScheduledTask
+ */
+ public static function makeScheduledTask()
+ {
+ $instance = new Piwik_UserCountry_GeoIPAutoUpdater();
+
+ $schedulePeriodStr = self::getSchedulePeriod();
+
+ // created the scheduledtime instance, also, since GeoIP updates are done on tuesdays,
+ // get new DBs on Wednesday
+ switch ($schedulePeriodStr)
+ {
+ case self::SCHEDULE_PERIOD_WEEKLY:
+ $schedulePeriod = new Piwik_ScheduledTime_Weekly();
+ $schedulePeriod->setDay(3);
+ break;
+ case self::SCHEDULE_PERIOD_MONTHLY:
+ default:
+ $schedulePeriod = new Piwik_ScheduledTime_Monthly();
+ $schedulePeriod->setDayOfWeek(3, 0);
+ break;
+ }
+
+ return new Piwik_ScheduledTask($instance, 'update', $schedulePeriod, Piwik_ScheduledTask::LOWEST_PRIORITY);
+ }
+
+ /**
+ * Sets the options used by this class based on query parameter values.
+ *
+ * See setUpdaterOptions for query params used.
+ */
+ public static function setUpdaterOptionsFromUrl()
+ {
+ self::setUpdaterOptions(array(
+ 'loc' => Piwik_Common::getRequestVar('loc_db', false, 'string'),
+ 'isp' => Piwik_Common::getRequestVar('isp_db', false, 'string'),
+ 'org' => Piwik_Common::getRequestVar('org_db', false, 'string'),
+ 'period' => Piwik_Common::getRequestVar('period', false, 'string'),
+ ));
+ }
+
+ /**
+ * Sets the options used by this class based on the elements in $options.
+ *
+ * The following elements of $options are used:
+ * 'loc' - URL for location database.
+ * 'isp' - URL for ISP database.
+ * 'org' - URL for Organization database.
+ * 'period' - 'weekly' or 'monthly'. When to run the updates.
+ *
+ * @param array $options
+ */
+ public static function setUpdaterOptions( $options )
+ {
+ // set url options
+ foreach (self::$urlOptions as $optionKey => $optionName)
+ {
+ if (empty($options[$optionKey]))
+ {
+ continue;
+ }
+
+ Piwik_SetOption($optionName, $url = $options[$optionKey]);
+ }
+
+ // set period option
+ if (!empty($options['period']))
+ {
+ $period = $options['period'];
+ if ($period != self::SCHEDULE_PERIOD_MONTHLY
+ && $period != self::SCHEDULE_PERIOD_WEEKLY)
+ {
+ throw new Exception(Piwik_Translate(
+ 'UserCountry_InvalidGeoIPUpdatePeriod',
+ array("'$period'", "'".self::SCHEDULE_PERIOD_MONTHLY."', '".self::SCHEDULE_PERIOD_WEEKLY."'")
+ ));
+ }
+
+ Piwik_SetOption(self::SCHEDULE_PERIOD_OPTION_NAME, $period);
+ }
+ }
+
+ /**
+ * Returns true if the auto-updater is setup to update at least one type of
+ * database. False if otherwise.
+ *
+ * @return bool
+ */
+ public static function isUpdaterSetup()
+ {
+ if (Piwik_GetOption(self::LOC_URL_OPTION_NAME) !== false
+ || Piwik_GetOption(self::ISP_URL_OPTION_NAME) !== false
+ || Piwik_GetOption(self::ORG_URL_OPTION_NAME) !== false)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieves the URLs used to update various GeoIP database files.
+ *
+ * @return array
+ */
+ public static function getConfiguredUrls()
+ {
+ $result = array();
+ foreach (self::$urlOptions as $key => $optionName)
+ {
+ $result[$key] = Piwik_GetOption($optionName);
+ }
+ return $result;
+ }
+
+ /**
+ * Returns the confiured URL (if any) for a type of database.
+ *
+ * @param string $key 'loc', 'isp' or 'org'
+ * @return string|false
+ */
+ public static function getConfiguredUrl( $key )
+ {
+ return Piwik_GetOption(self::$urlOptions[$key]);
+ }
+
+ /**
+ * Performs a GeoIP database update.
+ */
+ public static function performUpdate()
+ {
+ $instance = new Piwik_UserCountry_GeoIPAutoUpdater();
+ $instance->update();
+ }
+
+ /**
+ * Returns the configured update period, either 'week' or 'month'. Defaults to
+ * 'month'.
+ *
+ * @return string
+ */
+ public static function getSchedulePeriod()
+ {
+ $period = Piwik_GetOption(self::SCHEDULE_PERIOD_OPTION_NAME);
+ if ($period === false)
+ {
+ $period = self::SCHEDULE_PERIOD_MONTHLY;
+ }
+ return $period;
+ }
+
+ /**
+ * Returns an array of strings for GeoIP databases that have update URLs configured, but
+ * are not present in the misc directory. Each string is a key describing the type of
+ * database (ie, 'loc', 'isp' or 'org').
+ *
+ * @return array
+ */
+ public static function getMissingDatabases()
+ {
+ $result = array();
+ foreach (self::getConfiguredUrls() as $key => $url)
+ {
+ if ($url !== false)
+ {
+ // if a database of the type does not exist, but there's a url to update, then
+ // a database is missing
+ $path = Piwik_UserCountry_LocationProvider_GeoIp::getPathToGeoIpDatabase(
+ Piwik_UserCountry_LocationProvider_GeoIp::$dbNames[$key]);
+ if ($path === false)
+ {
+ $result[] = $key;
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Returns the extension of a URL used to update a GeoIP database, if it can be found.
+ */
+ public static function getGeoIPUrlExtension( $url )
+ {
+ // check for &suffix= query param that is special to MaxMind URLs
+ if (preg_match('/suffix=([^&]+)/', $url, $matches))
+ {
+ return $matches[1];
+ }
+
+ // use basename of url
+ $filenameParts = explode('.', basename($url), 2);
+ if (count($filenameParts) > 1)
+ {
+ return end($filenameParts);
+ }
+ else
+ {
+ return reset($filenameParts);
+ }
+ }
+}
diff --git a/plugins/UserCountry/LocationProvider/GeoIp.php b/plugins/UserCountry/LocationProvider/GeoIp.php
index 9c2d49a476..4cb0157169 100755
--- a/plugins/UserCountry/LocationProvider/GeoIp.php
+++ b/plugins/UserCountry/LocationProvider/GeoIp.php
@@ -17,6 +17,9 @@
*/
abstract class Piwik_UserCountry_LocationProvider_GeoIp extends Piwik_UserCountry_LocationProvider
{
+ /* For testing, use: 'http://piwik-team.s3.amazonaws.com/GeoLiteCity.dat.gz' */
+ const GEO_LITE_URL = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz';
+
public static $geoIPDatabaseDir = 'misc';
/**
@@ -176,10 +179,9 @@ abstract class Piwik_UserCountry_LocationProvider_GeoIp extends Piwik_UserCountr
*/
public static function getPathToGeoIpDatabase( $possibleFileNames )
{
- $dir = PIWIK_INCLUDE_PATH.'/'.self::$geoIPDatabaseDir;
foreach ($possibleFileNames as $filename)
{
- $path = $dir.'/'.$filename;
+ $path = self::getPathForGeoIpDatabase($filename);
if (file_exists($path))
{
return $path;
@@ -189,6 +191,17 @@ abstract class Piwik_UserCountry_LocationProvider_GeoIp extends Piwik_UserCountr
}
/**
+ * Returns full path for a GeoIP database managed by Piwik.
+ *
+ * @param string $filename Name of the .dat file.
+ * @return string
+ */
+ public static function getPathForGeoIpDatabase( $filename )
+ {
+ return PIWIK_INCLUDE_PATH.'/'.self::$geoIPDatabaseDir.'/'.$filename;
+ }
+
+ /**
* Returns test IP used by isWorking and expected result.
*
* @return array eg. array('1.2.3.4', array(self::COUNTRY_CODE_KEY => ...))
@@ -206,6 +219,18 @@ abstract class Piwik_UserCountry_LocationProvider_GeoIp extends Piwik_UserCountr
}
return $result;
}
+
+ /**
+ * Returns true if there is a GeoIP database in the 'misc' directory.
+ *
+ * @return bool
+ */
+ public static function isDatabaseInstalled()
+ {
+ return self::getPathToGeoIpDatabase(self::$dbNames['loc'])
+ || self::getPathToGeoIpDatabase(self::$dbNames['isp'])
+ || self::getPathToGeoIpDatabase(self::$dbNames['org']);
+ }
}
diff --git a/plugins/UserCountry/UserCountry.php b/plugins/UserCountry/UserCountry.php
index 91e6feaf05..cefbb66d87 100644
--- a/plugins/UserCountry/UserCountry.php
+++ b/plugins/UserCountry/UserCountry.php
@@ -11,6 +11,11 @@
*/
/**
+ * @see plugins/UserCountry/GeoIPAutoUpdater.php
+ */
+require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/GeoIPAutoUpdater.php';
+
+/**
*
* @package Piwik_UserCountry
*/
@@ -48,8 +53,10 @@ class Piwik_UserCountry extends Piwik_Plugin
'Goals.getReportsWithGoalMetrics' => 'getReportsWithGoalMetrics',
'API.getReportMetadata' => 'getReportMetadata',
'API.getSegmentsMetadata' => 'getSegmentsMetadata',
+ 'AssetManager.getCssFiles' => 'getCssFiles',
'AssetManager.getJsFiles' => 'getJsFiles',
'Tracker.getVisitorLocation' => 'getVisitorLocation',
+ 'TaskScheduler.getScheduledTasks' => 'getScheduledTasks',
);
return $hooks;
}
@@ -57,6 +64,27 @@ class Piwik_UserCountry extends Piwik_Plugin
/**
* @param Piwik_Event_Notification $notification notification object
*/
+ function getScheduledTasks($notification)
+ {
+ $tasks = &$notification->getNotificationObject();
+
+ // add the auto updater task
+ $tasks[] = Piwik_UserCountry_GeoIPAutoUpdater::makeScheduledTask();
+ }
+
+ /**
+ * @param Piwik_Event_Notification $notification notification object
+ */
+ function getCssFiles( $notification )
+ {
+ $cssFiles = &$notification->getNotificationObject();
+
+ $cssFiles[] = "plugins/UserCountry/templates/styles.css";
+ }
+
+ /**
+ * @param Piwik_Event_Notification $notification notification object
+ */
public function getJsFiles( $notification )
{
$jsFiles = &$notification->getNotificationObject();
@@ -123,13 +151,10 @@ class Piwik_UserCountry extends Piwik_Plugin
*/
function addAdminMenu()
{
- if (Piwik::isUserIsSuperUser())
- {
- Piwik_AddAdminMenu('UserCountry_Geolocation',
- array('module' => 'UserCountry', 'action' => 'adminIndex'),
- Piwik::isUserHasSomeAdminAccess(),
- $order = 8);
- }
+ Piwik_AddAdminMenu('UserCountry_Geolocation',
+ array('module' => 'UserCountry', 'action' => 'adminIndex'),
+ Piwik::isUserIsSuperUser(),
+ $order = 8);
}
/**
diff --git a/plugins/UserCountry/templates/admin.js b/plugins/UserCountry/templates/admin.js
index 8d10ca6e5c..1077039cba 100755
--- a/plugins/UserCountry/templates/admin.js
+++ b/plugins/UserCountry/templates/admin.js
@@ -6,8 +6,10 @@
*/
$(document).ready(function() {
+ $('#geoip-download-progress,#geoip-updater-progressbar').progressbar({value: 1});
+
// handle switch current location provider
- $('.current-location-provider').change(function() {
+ $('.location-provider').change(function() {
if (!$(this).is(':checked')) return; // only handle radio buttons that get checked
var parent = $(this).parent(),
@@ -60,4 +62,140 @@ $(document).ready(function() {
return false;
});
+
+ // geoip database wizard
+ var downloadNextChunk = function(action, thisId, progressBarId, cont, extraData, callback)
+ {
+ var data = {
+ module: 'UserCountry',
+ action: action,
+ token_auth: piwik.token_auth,
+ 'continue': cont ? 1 : 0,
+ };
+ for (var k in extraData)
+ {
+ data[k] = extraData[k];
+ }
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(data, 'post');
+ ajaxRequest.setCallback(function(response) {
+ if (!response || response.error)
+ {
+ callback(response);
+ }
+ else
+ {
+ // update progress bar
+ var newProgressVal = Math.ceil((response.current_size / response.expected_file_size) * 100);
+ newProgressVal = Math.min(newProgressVal, 100);
+ $('#'+progressBarId).progressbar('option', 'value', newProgressVal);
+
+ // if incomplete, download next chunk, otherwise, show updater manager
+ if (newProgressVal < 100)
+ {
+ downloadNextChunk(action, thisId, progressBarId, true, extraData, callback);
+ }
+ else
+ {
+ callback(response);
+ }
+ }
+ });
+ ajaxRequest.send(false);
+ };
+
+ $('#start-download-free-geoip').click(function() {
+ $('#geoipdb-screen1').hide("slide", {direction: "left"}, 800, function() {
+ $('#geoipdb-screen2-download').fadeIn(1000);
+
+ // start download of free dbs
+ downloadNextChunk(
+ 'downloadFreeGeoIPDB',
+ 'geoipdb-screen2-download',
+ 'geoip-download-progress',
+ false,
+ {},
+ function(response) {
+ if (response.error)
+ {
+ // on error, show error & stop downloading
+ $('#'+thisId).fadeOut(1000, function() {
+ $('#manage-geoip-dbs').html(response.error);
+ });
+ }
+ else
+ {
+ $('#geoipdb-screen2-download').fadeOut(1000, function() {
+ $('#manage-geoip-dbs').html(response.next_screen);
+ });
+ }
+ }
+ );
+ });
+ });
+
+ $('body').on('click', '#start-automatic-update-geoip', function() {
+ $('#geoipdb-screen1').hide("slide", {direction: "left"}, 800, function () {
+ $('#geoipdb-update-info').fadeIn(1000);
+ });
+ });
+
+ $('body').on('click', '#update-geoip-links', function() {
+ $('#geoipdb-update-info-error').hide();
+
+ var currentDownloading = null,
+ updateGeoIPSuccess = function(response)
+ {
+ if (response && response.error)
+ {
+ $('#geoip-progressbar-container').hide();
+ $('#geoipdb-update-info-error').html(response.error).show();
+ }
+ else if (response && response.to_download)
+ {
+ var continuing = currentDownloading == response.to_download;
+ currentDownloading = response.to_download;
+
+ // show progress bar w/ message
+ $('#geoip-updater-progressbar').progressbar('option', 'value', 1);
+ $('#geoip-updater-progressbar-label').html(response.to_download_label);
+ $('#geoip-progressbar-container').show();
+
+ // start/continue download
+ downloadNextChunk(
+ 'downloadMissingGeoIpDb', 'geoipdb-update-info', 'geoip-updater-progressbar',
+ continuing, {key: response.to_download}, updateGeoIPSuccess);
+ }
+ else
+ {
+ $('#geoipdb-update-info-error').hide();
+ $('#geoip-updater-progressbar-label').html('');
+ $('#geoip-progressbar-container').hide();
+
+ // fade in/out Done message
+ $('#done-updating-updater').fadeIn(1000, function() {
+ setTimeout(function() {
+ $('#done-updating-updater').fadeOut(1000);
+ }, 3000);
+ });
+ }
+ };
+
+ // setup the auto-updater
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams({
+ period: $('#geoip-update-period-cell>input:checked').val()
+ }, 'get');
+ ajaxRequest.addParams({
+ module: 'UserCountry',
+ action: 'updateGeoIPLinks',
+ token_auth: piwik.token_auth,
+ loc_db: $('#geoip-location-db').val(),
+ isp_db: $('#geoip-isp-db').val(),
+ org_db: $('#geoip-org-db').val()
+ }, 'post');
+ ajaxRequest.setCallback(updateGeoIPSuccess);
+ ajaxRequest.send(false);
+ });
});
diff --git a/plugins/UserCountry/templates/adminIndex.tpl b/plugins/UserCountry/templates/adminIndex.tpl
index a4ddcfa9bc..ecf51a265d 100755
--- a/plugins/UserCountry/templates/adminIndex.tpl
+++ b/plugins/UserCountry/templates/adminIndex.tpl
@@ -22,7 +22,7 @@
<p>&nbsp;</p>
{/if}
-<table class="adminTable">
+<table class="adminTable locationProviderTable">
<tr>
<th>{'UserCountry_LocationProvider'|translate}</th>
<th>{'General_Description'|translate}</th>
@@ -32,19 +32,19 @@
<tr>
<td width="140">
<p>
- <input class="current-location-provider" name="current-location-provider" value="{$id}" type="radio" {if $currentProviderId eq $id}checked="checked"{/if} id="provider_input_{$id}" style="cursor:pointer" {if $provider.status neq 1}disabled="disabled"{/if}/>
- <label for="provider_input_{$id}" style="font-size:1.2em">{$provider.title|translate}</label><br/>
+ <input class="location-provider" name="location-provider" value="{$id}" type="radio" {if $currentProviderId eq $id}checked="checked"{/if} id="provider_input_{$id}" {if $provider.status neq 1}disabled="disabled"{/if}/>
+ <label for="provider_input_{$id}">{$provider.title|translate}</label><br/>
<span class='loadingPiwik' style='display:none'><img src='./themes/default/images/loading-blue.gif' /></span>
<span class="ajaxSuccess" style='display:none'>{'General_Done'|translate}</span>
</p>
- <p style="margin-left:.5em">
+ <p class="loc-provider-status">
<strong><em>
{if $provider.status eq 0}
- {'General_NotInstalled'|translate}
+ <span class="is-not-installed">{'General_NotInstalled'|translate}</span>
{elseif $provider.status eq 1}
- <span style="color:green">{'General_Installed'|translate}</span>
+ <span class="is-installed">{'General_Installed'|translate}</span>
{elseif $provider.status eq 2}
- <span style="color:red">{'General_Broken'|translate}</span>
+ <span class="is-broken">{'General_Broken'|translate}</span>
{/if}
</em></strong>
</p>
@@ -93,5 +93,33 @@
</div>
+<h2>{'UserCountry_GeoIPDatabases'|translate}</h2>
+
+{if $showGeoIPUpdateSection}
+<div id="manage-geoip-dbs" style="width:900px">
+
+{if !$geoIPDatabasesInstalled}
+<div id="geoipdb-screen1">
+ <p>{'UserCountry_PiwikNotManagingGeoIPDBs'|translate}</p>
+ <div class="geoipdb-column-1">
+ <p>{'UserCountry_IWantToDownloadFreeGeoIP'|translate}</p>
+ <input type="button" class="submit" value="{'General_GetStarted'|translate}..." id="start-download-free-geoip"/>
+ </div>
+ <div class="geoipdb-column-2">
+ <p>{'UserCountry_IPurchasedGeoIPDBs'|translate}</p>
+ <input type="button" class="submit" value="{'General_GetStarted'|translate}..." id="start-automatic-update-geoip"/>
+ </div>
+</div>
+<div id="geoipdb-screen2-download" style="display:none">
+ <p class='loadingPiwik'><img src='./themes/default/images/loading-blue.gif' />{'UserCountry_DownloadingDb'|translate:'GeoLiteCity.dat'}...</p>
+ <div id="geoip-download-progress"></div>
+</div>
+{/if}
+{include file="UserCountry/templates/updaterSetup.tpl"}
+{else}
+<p style="width:900px" class="form-description">{'UserCountry_CannotSetupGeoIPAutoUpdating'|translate}</p>
+{/if}
+</div>
+
{include file="CoreAdminHome/templates/footer.tpl"}
diff --git a/plugins/UserCountry/templates/styles.css b/plugins/UserCountry/templates/styles.css
new file mode 100755
index 0000000000..4ded571aa5
--- /dev/null
+++ b/plugins/UserCountry/templates/styles.css
@@ -0,0 +1,69 @@
+.locationProviderTable label {
+ font-size: 1.2em;
+}
+
+input.location-provider {
+ cursor:pointer;
+}
+
+span.is-installed {
+ color:green;
+}
+
+span.is-broken {
+ color:red;
+}
+
+.loc-provider-status {
+ margin-left:.5em;
+}
+
+#manage-geoip-dbs {
+ width:900px;
+ height: 20em;
+}
+
+#geoipdb-update-info tr input[type="text"],#geoipdb-screen2-update tr input[type="text"] {
+ width: 90%;
+}
+
+#geoipdb-screen1>div {
+ display: inline-block;
+ vertical-align:top;
+}
+
+#geoipdb-screen1>div>p {
+ font-size: 2em;
+ height: 4em;
+}
+
+.geoipdb-column-1,.geoipdb-column-2 {
+ width:396px;
+}
+.geoipdb-column-1 {
+ margin-right: 50px;
+}
+.geoipdb-column-2 {
+ border-left: solid #999 1px;
+ padding-left: 50px;
+}
+.geoipdb-column-1>p {
+ padding-left: 20px;
+}
+
+.error {
+ font-weight:bold;
+ color:red;
+ padding:4px 8px 4px 8px;
+}
+
+#geoip-updater-progressbar-label {
+ float: left;
+ margin: -24px 24px;
+}
+
+#geoip-progressbar-container,#geoipdb-update-info-error {
+ margin: 22px 24px;
+ display:inline-block;
+}
+
diff --git a/plugins/UserCountry/templates/updaterSetup.tpl b/plugins/UserCountry/templates/updaterSetup.tpl
new file mode 100755
index 0000000000..96d55b5f1a
--- /dev/null
+++ b/plugins/UserCountry/templates/updaterSetup.tpl
@@ -0,0 +1,53 @@
+<div id="geoipdb-update-info" {if !$geoIPDatabasesInstalled}style="display:none"{/if}>
+ <p>{'UserCountry_GeoIPUpdaterInstructions'|translate:'<a href="http://www.maxmind.com/en/download_files?rId=piwik" _target="blank">':'</a>':'<a href="http://www.maxmind.com/?rId=piwik">':'</a>'}</p>
+ {if $geoIPDatabasesInstalled}
+ <p>{'UserCountry_GeoIPUpdaterIntro'|translate}:</p>
+ {/if}
+ <table class="adminTable" style="width:900px">
+ <tr>
+ <th>{'Live_GoalType'|translate}</th>
+ <th>{'Actions_ColumnDownloadURL'|translate}</th>
+ <th></th>
+ </tr>
+ <tr>
+ <td width="140">{'UserCountry_LocationDatabase'|translate}</td>
+ <td><input type="text" id="geoip-location-db" value="{$geoIPLocUrl}"/></td>
+ <td width="164">
+ {capture assign=locationHint}
+ {'UserCountry_LocationDatabaseHint'|translate}
+ {/capture}
+ {$locationHint|inlineHelp}
+ </td>
+ </tr>
+ <tr>
+ <td width="140">{'UserCountry_ISPDatabase'|translate}</td>
+ <td><input type="text" id="geoip-isp-db" value="{$geoIPIspUrl}"/></td>
+ </tr>
+ <tr>
+ <td width="140">{'UserCountry_OrgDatabase'|translate}</td>
+ <td><input type="text" id="geoip-org-db" value="{$geoIPOrgUrl}"/></td>
+ </tr>
+ <tr>
+ <td width="140">{'General_Period'|translate}</td>
+ <td id="geoip-update-period-cell">
+ <input type="radio" name="geoip-update-period" value="month" id="geoip-update-period-month" {if $geoIPUpdatePeriod eq 'month'}checked="checked"{/if}/>
+ <label for="geoip-update-period-month">{'General_Monthly'|translate}</label>
+
+ <input type="radio" name="geoip-update-period" value="week" id="geoip-update-period-week" {if $geoIPUpdatePeriod eq 'week'}checked="checked"{/if}/>
+ <label for="geoip-update-period-week">{'General_Weekly'|translate}</label>
+ </td>
+ <td width="164">&nbsp;</td>
+ </tr>
+ </table>
+ <p style="display:inline-block;vertical-align:top">
+ <input type="button" class="submit" value="{if !$geoIPDatabasesInstalled}{'General_Continue'|translate}{else}{'General_Save'|translate}{/if}" id="update-geoip-links"/>
+ </p>
+ <div style="display:inline-block;width:700px">
+ <span style="display:none" class="ajaxSuccess" id="done-updating-updater">{'General_Done'|translate}!</span>
+ <span id="geoipdb-update-info-error" style="display:none" class="error"></span>
+ <div id="geoip-progressbar-container" style="display:none">
+ <div id="geoip-updater-progressbar"></div>
+ <span id="geoip-updater-progressbar-label"></span>
+ </div>
+ </div>
+</div>
diff --git a/tests/PHPUnit/Core/HttpTest.php b/tests/PHPUnit/Core/HttpTest.php
index 0d2fd90e08..3b739e87b1 100644
--- a/tests/PHPUnit/Core/HttpTest.php
+++ b/tests/PHPUnit/Core/HttpTest.php
@@ -55,4 +55,69 @@ class HttpTest extends PHPUnit_Framework_TestCase
$this->assertFileExists($destinationPath);
$this->assertGreaterThan( 0, filesize($destinationPath) );
}
+
+ /**
+ * @group Core
+ * @group Http
+ * @dataProvider getMethodsToTest
+ */
+ public function testCustomByteRange( $method )
+ {
+ $result = Piwik_Http::sendHttpRequestBy(
+ $method,
+ 'http://piwik.org/',
+ 5,
+ $userAgent = null,
+ $destinationPath = null,
+ $file = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $acceptInvalidSslCertificate = false,
+ $byteRange = array(10, 20),
+ $getExtendedInfo = true
+ );
+
+ if ($method != 'fopen')
+ {
+ $this->assertEquals(206, $result['status']);
+ $this->assertEquals("html>\n<!--[", $result['data']);
+ $this->assertTrue(isset($result['headers']['Content-Range']));
+ $this->assertEquals('bytes 10-20/', substr($result['headers']['Content-Range'], 0, 12));
+ $this->assertEquals('text/html; charset=UTF-8', $result['headers']['Content-Type']);
+ }
+ }
+
+ /**
+ * @group Core
+ * @group Http
+ * @dataProvider getMethodsToTest
+ */
+ public function testHEADOperation( $method )
+ {
+ if ($method == 'fopen')
+ {
+ return; // not supported w/ this method
+ }
+
+ $result = Piwik_Http::sendHttpRequestBy(
+ $method,
+ 'http://piwik.org/',
+ 5,
+ $userAgent = null,
+ $destinationPath = null,
+ $file = null,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $acceptInvalidSslCertificate = false,
+ $byteRange = false,
+ $getExtendedInfo = true,
+ $httpMethod = 'HEAD'
+ );
+
+ $this->assertEquals('', $result['data']);
+ $this->assertEquals(200, $result['status']);
+ $this->assertTrue(isset($result['headers']['Content-Length']), "Content-Length header not set!");
+ $this->assertTrue(is_numeric($result['headers']['Content-Length']), "Content-Length header not numeric!");
+ $this->assertEquals('text/html; charset=UTF-8', $result['headers']['Content-Type']);
+ }
}
diff --git a/tests/PHPUnit/Core/ScheduledTimeTest.php b/tests/PHPUnit/Core/ScheduledTimeTest.php
index 24959b810b..b1b5761a70 100644
--- a/tests/PHPUnit/Core/ScheduledTimeTest.php
+++ b/tests/PHPUnit/Core/ScheduledTimeTest.php
@@ -22,6 +22,8 @@ class ScheduledTimeTest extends PHPUnit_Framework_TestCase
private $_JANUARY_15_1971_09_00_00;
private $_FEBRUARY_01_1971_00_00_00;
private $_FEBRUARY_02_1971_00_00_00;
+ private $_FEBRUARY_03_1971_09_00_00;
+ private $_FEBRUARY_21_1971_09_00_00;
private $_FEBRUARY_28_1971_00_00_00;
public function setUp()
@@ -41,6 +43,8 @@ class ScheduledTimeTest extends PHPUnit_Framework_TestCase
$this->_JANUARY_15_1971_09_00_00 = mktime(9,00,00,1,15,1971);
$this->_FEBRUARY_01_1971_00_00_00 = mktime(0,00,00,2,1,1971);
$this->_FEBRUARY_02_1971_00_00_00 = mktime(0,00,00,2,2,1971);
+ $this->_FEBRUARY_03_1971_09_00_00 = mktime(0,00,00,2,3,1971);
+ $this->_FEBRUARY_21_1971_09_00_00 = mktime(0,00,00,2,21,1971);
$this->_FEBRUARY_28_1971_00_00_00 = mktime(0,00,00,2,28,1971);
}
@@ -611,4 +615,21 @@ class ScheduledTimeTest extends PHPUnit_Framework_TestCase
$mock->setDay(31);
$this->assertEquals($this->_FEBRUARY_28_1971_00_00_00, $mock->getRescheduledTime());
}
+
+ /**
+ * @group Core
+ * @group ScheduledTime
+ */
+ public function testMonthlyDayOfWeek()
+ {
+ $mock = $this->getMock('Piwik_ScheduledTime_Monthly', array('getTime'));
+ $mock->expects($this->any())
+ ->method('getTime')
+ ->will($this->returnValue($this->_JANUARY_15_1971_09_00_00));
+ $mock->setDayOfWeek(3, 0); // first wednesday
+ $this->assertEquals($this->_FEBRUARY_03_1971_09_00_00, $mock->getRescheduledTime());
+
+ $mock->setDayOfWeek(0, 2); // third sunday
+ $this->assertEquals($this->_FEBRUARY_21_1971_09_00_00, $mock->getRescheduledTime());
+ }
}
diff --git a/tests/PHPUnit/Core/Unzip/test.gz b/tests/PHPUnit/Core/Unzip/test.gz
new file mode 100755
index 0000000000..113cbed07f
--- /dev/null
+++ b/tests/PHPUnit/Core/Unzip/test.gz
Binary files differ
diff --git a/tests/PHPUnit/Core/Unzip/test.tar.gz b/tests/PHPUnit/Core/Unzip/test.tar.gz
new file mode 100755
index 0000000000..b9cc91965d
--- /dev/null
+++ b/tests/PHPUnit/Core/Unzip/test.tar.gz
Binary files differ
diff --git a/tests/PHPUnit/Core/UnzipTest.php b/tests/PHPUnit/Core/UnzipTest.php
index 85627cc388..398733b1f5 100644
--- a/tests/PHPUnit/Core/UnzipTest.php
+++ b/tests/PHPUnit/Core/UnzipTest.php
@@ -178,5 +178,48 @@ class UnzipTest extends PHPUnit_Framework_TestCase
$this->assertContains('PCLZIP_ERR_MISSING_FILE', $unzip->errorInfo());
}
-
+ /**
+ * @group Core
+ * @group Unzip
+ */
+ public function testGzipFile()
+ {
+ $extractDir = PIWIK_USER_PATH . '/tmp/latest/';
+ $extractFile = $extractDir.'testgz.txt';
+ $filename = dirname(__FILE__).'/Unzip/test.gz';
+
+ $unzip = new Piwik_Unzip_Gzip($filename);
+ $res = $unzip->extract($extractFile);
+ $this->assertTrue($res);
+
+ $this->assertFileContentsEquals('TESTSTRING', $extractFile);
+ }
+
+ /**
+ * @group Core
+ * @group Unzip
+ */
+ public function testTarGzFile()
+ {
+ $extractDir = PIWIK_USER_PATH.'/tmp/latest/';
+ $filename = dirname(__FILE__).'/Unzip/test.tar.gz';
+
+ $unzip = new Piwik_Unzip_Tar($filename, 'gz');
+ $res = $unzip->extract($extractDir);
+ $this->assertTrue($res);
+
+ $this->assertFileContentsEquals('TESTDATA', $extractDir.'tarout1.txt');
+ $this->assertFileContentsEquals('MORETESTDATA', $extractDir.'tardir/tarout2.txt');
+ }
+
+ private function assertFileContentsEquals( $expectedContent, $path )
+ {
+ $this->assertTrue(file_exists($path));
+
+ $fd = fopen($path, 'rb');
+ $actualContent = fread($fd, filesize($path));
+ fclose($fd);
+
+ $this->assertEquals($expectedContent, $actualContent);
+ }
}
diff --git a/themes/default/common.css b/themes/default/common.css
index 0e39e02f78..c6bf75ad36 100644
--- a/themes/default/common.css
+++ b/themes/default/common.css
@@ -609,6 +609,7 @@ select {
background:#847b6d url(images/dashboard_h_bg_hover.png) repeat-x 0 0;
float:right;
margin:12px 0 10px 0;
+ cursor:pointer;
}
/* on admin screen, Save button aligned on the left */
.admin .submit {