diff --git a/core/Config.php b/core/Config.php
index db2e1e5d6a7637fc7c33e7ee9d4617aae50d9634..d6c259881928ad9c1f1a689fefdc3d7fa19c3a65 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -10,6 +10,9 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\Ini\IniReader;
+use Piwik\Ini\IniReadingException;
+use Piwik\Ini\IniWriter;
 
 /**
  * Singleton that provides read & write access to Piwik's INI configuration.
@@ -64,13 +67,22 @@ class Config extends Singleton
     protected $isTest = false;
 
     /**
-     * Constructor
+     * @var IniReader
      */
+    private $iniReader;
+
+    /**
+     * @var IniWriter
+     */
+    private $iniWriter;
+
     public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
     {
         $this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath();
         $this->pathCommon = $pathCommon ?: self::getCommonConfigPath();
         $this->pathLocal = $pathLocal ?: self::getLocalConfigPath();
+        $this->iniReader = new IniReader();
+        $this->iniWriter = new IniWriter();
     }
 
     /**
@@ -317,20 +329,33 @@ class Config extends Singleton
             throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal)));
         }
 
-        $this->configGlobal = _parse_ini_file($this->pathGlobal, true);
-
-        if (empty($this->configGlobal) && $reportError) {
-            throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
+        try {
+            $this->configGlobal = $this->iniReader->readFile($this->pathGlobal);
+        } catch (IniReadingException $e) {
+            if ($reportError) {
+                throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
+            }
         }
 
-        $this->configCommon = _parse_ini_file($this->pathCommon, true);
+        try {
+            if (file_exists($this->pathCommon)) {
+                $this->configCommon = $this->iniReader->readFile($this->pathCommon);
+            } else {
+                $this->configCommon = false;
+            }
+        } catch (IniReadingException $e) {
+            $this->configCommon = false;
+        }
 
         // Check config.ini.php last
         $this->checkLocalConfigFound();
 
-        $this->configLocal = _parse_ini_file($this->pathLocal, true);
-        if (empty($this->configLocal) && $reportError) {
-            throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
+        try {
+            $this->configLocal = $this->iniReader->readFile($this->pathLocal);
+        } catch (IniReadingException $e) {
+            if ($reportError) {
+                throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
+            }
         }
     }
 
@@ -365,8 +390,10 @@ class Config extends Singleton
                 $value = $this->decodeValues($value);
             }
             return $values;
+        } elseif (is_string($values)) {
+            return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
         }
-        return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
+        return $values;
     }
 
     /**
@@ -381,11 +408,9 @@ class Config extends Singleton
             foreach ($values as &$value) {
                 $value = $this->encodeValues($value);
             }
-        } else {
-            if (is_float($values)) {
-                $values = Common::forceDotAsSeparatorForDecimalPoint($values);
-            }
-
+        } elseif (is_float($values)) {
+            $values = Common::forceDotAsSeparatorForDecimalPoint($values);
+        } elseif (is_string($values)) {
             $values = htmlentities($values, ENT_COMPAT, 'UTF-8');
             $values = str_replace('$', '$', $values);
         }
@@ -556,13 +581,12 @@ class Config extends Singleton
     {
         $dirty = false;
 
-        $output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
-        $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
-
         if (!$configCache) {
             return false;
         }
 
+        $configToWrite = array();
+
         // If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values
         if (!empty($configCommon)) {
             $configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon);
@@ -605,38 +629,13 @@ class Config extends Singleton
                 $dirty = true;
             }
 
-            // no point in writing empty sections, so skip if the cached section is empty
-            if (empty($config)) {
-                continue;
-            }
-
-            $output .= "[$section]\n";
-
-            foreach ($config as $name => $value) {
-                $value = $this->encodeValues($value);
-
-                if (is_numeric($name)) {
-                    $name = $section;
-                    $value = array($value);
-                }
-
-                if (is_array($value)) {
-                    foreach ($value as $currentValue) {
-                        $output .= $name . "[] = \"$currentValue\"\n";
-                    }
-                } else {
-                    if (!is_numeric($value)) {
-                        $value = "\"$value\"";
-                    }
-                    $output .= $name . ' = ' . $value . "\n";
-                }
-            }
-
-            $output .= "\n";
+            $configToWrite[$section] = array_map(array($this, 'encodeValues'), $config);
         }
 
         if ($dirty) {
-            return $output;
+            $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
+            $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
+            return $this->iniWriter->writeToString($configToWrite, $header);
         }
         return false;
     }
diff --git a/core/Ini/IniReader.php b/core/Ini/IniReader.php
new file mode 100644
index 0000000000000000000000000000000000000000..e50d79720215fc517bc68b53b82fb217a3d20904
--- /dev/null
+++ b/core/Ini/IniReader.php
@@ -0,0 +1,262 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Ini;
+
+/**
+ * Reads INI configuration.
+ */
+class IniReader
+{
+    /**
+     * Reads a INI configuration file and returns it as an array.
+     *
+     * The array returned is multidimensional, indexed by section names:
+     *
+     * ```
+     * array(
+     *     'Section 1' => array(
+     *         'value1' => 'hello',
+     *         'value2' => 'world',
+     *     ),
+     *     'Section 2' => array(
+     *         'value3' => 'foo',
+     *     )
+     * );
+     * ```
+     *
+     * @param string $filename The file to read.
+     * @throws IniReadingException
+     * @return array
+     */
+    public function readFile($filename)
+    {
+        if (!file_exists($filename) || !is_readable($filename)) {
+            throw new IniReadingException(sprintf("The file %s doesn't exist or is not readable", $filename));
+        }
+
+        $ini = $this->getFileContent($filename);
+
+        if ($ini === false) {
+            throw new IniReadingException(sprintf('Impossible to read the file %s', $filename));
+        }
+
+        return $this->readString($ini);
+    }
+
+    /**
+     * Reads a INI configuration string and returns it as an array.
+     *
+     * The array returned is multidimensional, indexed by section names:
+     *
+     * ```
+     * array(
+     *     'Section 1' => array(
+     *         'value1' => 'hello',
+     *         'value2' => 'world',
+     *     ),
+     *     'Section 2' => array(
+     *         'value3' => 'foo',
+     *     )
+     * );
+     * ```
+     *
+     * @param string $ini String containing INI configuration.
+     * @throws IniReadingException
+     * @return array
+     */
+    public function readString($ini)
+    {
+        if (!function_exists('parse_ini_file')) {
+            return $this->readIni($ini, true);
+        }
+
+        $array = @parse_ini_string($ini, true, INI_SCANNER_RAW);
+
+        if ($array === false) {
+            $e = error_get_last();
+            throw new IniReadingException('Syntax error in INI configuration: ' . $e['message']);
+        }
+
+        $array = $this->decodeValues($array);
+
+        return $array;
+    }
+
+    /**
+     * Reimplementation in case `parse_ini_file()` is disabled.
+     *
+     * @author Andrew Sohn <asohn (at) aircanopy (dot) net>
+     * @author anthon (dot) pang (at) gmail (dot) com
+     *
+     * @param string $ini
+     * @param bool $processSections
+     * @return array
+     */
+    private function readIni($ini, $processSections)
+    {
+        if (is_string($ini)) {
+            $ini = explode("\n", str_replace("\r", "\n", $ini));
+        }
+        if (count($ini) == 0) {
+            return array();
+        }
+
+        $sections = array();
+        $values = array();
+        $result = array();
+        $globals = array();
+        $i = 0;
+        foreach ($ini as $line) {
+            $line = trim($line);
+            $line = str_replace("\t", " ", $line);
+
+            // Comments
+            if (!preg_match('/^[a-zA-Z0-9[]/', $line)) {
+                continue;
+            }
+
+            // Sections
+            if ($line{0} == '[') {
+                $tmp = explode(']', $line);
+                $sections[] = trim(substr($tmp[0], 1));
+                $i++;
+                continue;
+            }
+
+            // Key-value pair
+            list($key, $value) = explode('=', $line, 2);
+            $key = trim($key);
+            $value = trim($value);
+            if (strstr($value, ";")) {
+                $tmp = explode(';', $value);
+                if (count($tmp) == 2) {
+                    if ((($value{0} != '"') && ($value{0} != "'")) ||
+                        preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) ||
+                        preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value)
+                    ) {
+                        $value = $tmp[0];
+                    }
+                } else {
+                    if ($value{0} == '"') {
+                        $value = preg_replace('/^"(.*)".*/', '$1', $value);
+                    } elseif ($value{0} == "'") {
+                        $value = preg_replace("/^'(.*)'.*/", '$1', $value);
+                    } else {
+                        $value = $tmp[0];
+                    }
+                }
+            }
+
+            $value = trim($value);
+            $value = trim($value, "'\"");
+
+            if ($i == 0) {
+                if (substr($key, -2) == '[]') {
+                    $globals[substr($key, 0, -2)][] = $value;
+                } else {
+                    $globals[$key] = $value;
+                }
+            } else {
+                if (substr($key, -2) == '[]') {
+                    $values[$i - 1][substr($key, 0, -2)][] = $value;
+                } else {
+                    $values[$i - 1][$key] = $value;
+                }
+            }
+        }
+
+        for ($j = 0; $j < $i; $j++) {
+            if (isset($values[$j])) {
+                if ($processSections === true) {
+                    $result[$sections[$j]] = $values[$j];
+                } else {
+                    $result[] = $values[$j];
+                }
+            } else {
+                if ($processSections === true) {
+                    $result[$sections[$j]] = array();
+                }
+            }
+        }
+
+        return $result + $globals;
+    }
+
+    /**
+     * @param string $filename
+     * @return bool|string Returns false if failure.
+     */
+    private function getFileContent($filename)
+    {
+        if (function_exists('file_get_contents')) {
+            return file_get_contents($filename);
+        } elseif (function_exists('file')) {
+            $ini = file($filename);
+            if ($ini !== false) {
+                return implode("\n", $ini);
+            }
+        } elseif (function_exists('fopen') && function_exists('fread')) {
+            $handle = fopen($filename, 'r');
+            if (!$handle) {
+                return false;
+            }
+            $ini = fread($handle, filesize($filename));
+            fclose($handle);
+            return $ini;
+        }
+
+        return false;
+    }
+
+    private function decodeValues($config)
+    {
+        foreach ($config as &$section) {
+            foreach ($section as $option => $value) {
+                $section[$option] = $this->decodeValue($value);
+            }
+        }
+        return $config;
+    }
+
+    /**
+     * We have to decode values manually because parse_ini_file() has a poor implementation.
+     *
+     * @param mixed $value
+     * @return mixed
+     */
+    private function decodeValue($value)
+    {
+        if (is_array($value)) {
+            foreach ($value as &$subValue) {
+                $subValue = $this->decodeValue($subValue);
+            }
+            return $value;
+        }
+
+        if (is_numeric($value)) {
+            return $value + 0;
+        }
+
+        switch (strtolower($value)) {
+            case '':
+            case 'null' :
+                return null;
+            case 'true' :
+            case 'yes' :
+            case 'on' :
+                return true;
+            case 'false':
+            case 'no':
+            case 'off':
+                return false;
+        }
+
+        return $value;
+    }
+}
diff --git a/core/Ini/IniReadingException.php b/core/Ini/IniReadingException.php
new file mode 100644
index 0000000000000000000000000000000000000000..227305fd11d9c8f8ca25a5b8f4f5f9a814c11350
--- /dev/null
+++ b/core/Ini/IniReadingException.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Ini;
+
+/**
+ * Exception when reading a INI configuration.
+ */
+class IniReadingException extends \Exception
+{
+}
diff --git a/core/Ini/IniWriter.php b/core/Ini/IniWriter.php
new file mode 100644
index 0000000000000000000000000000000000000000..b18af460f47acfccd98a8dcf143f787ba3fe034c
--- /dev/null
+++ b/core/Ini/IniWriter.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Ini;
+
+/**
+ * Writes INI configuration.
+ */
+class IniWriter
+{
+    /**
+     * Writes an array configuration to a INI file.
+     *
+     * The array provided must be multidimensional, indexed by section names:
+     *
+     * ```
+     * array(
+     *     'Section 1' => array(
+     *         'value1' => 'hello',
+     *         'value2' => 'world',
+     *     ),
+     *     'Section 2' => array(
+     *         'value3' => 'foo',
+     *     )
+     * );
+     * ```
+     *
+     * @param string $filename
+     * @param array $config
+     * @param string $header Optional header to insert at the top of the file.
+     * @throws IniWritingException
+     */
+    public function writeToFile($filename, array $config, $header = '')
+    {
+        $ini = $this->writeToString($config, $header);
+
+        if (!file_put_contents($filename, $ini)) {
+            throw new IniWritingException(sprintf('Impossible to write to file %s', $filename));
+        }
+    }
+
+    /**
+     * Writes an array configuration to a INI string and returns it.
+     *
+     * The array provided must be multidimensional, indexed by section names:
+     *
+     * ```
+     * array(
+     *     'Section 1' => array(
+     *         'value1' => 'hello',
+     *         'value2' => 'world',
+     *     ),
+     *     'Section 2' => array(
+     *         'value3' => 'foo',
+     *     )
+     * );
+     * ```
+     *
+     * @param array $config
+     * @param string $header Optional header to insert at the top of the file.
+     * @return string
+     * @throws IniWritingException
+     */
+    public function writeToString(array $config, $header = '')
+    {
+        $ini = $header;
+
+        $sectionNames = array_keys($config);
+
+        foreach ($sectionNames as $sectionName) {
+            $section = $config[$sectionName];
+
+            // no point in writing empty sections
+            if (empty($section)) {
+                continue;
+            }
+
+            if (! is_array($section)) {
+                throw new IniWritingException(sprintf("Section \"%s\" doesn't contain an array of values", $sectionName));
+            }
+
+            $ini .= "[$sectionName]\n";
+
+            foreach ($section as $option => $value) {
+                if (is_numeric($option)) {
+                    $option = $sectionName;
+                    $value = array($value);
+                }
+
+                if (is_array($value)) {
+                    foreach ($value as $currentValue) {
+                        $ini .= $option . '[] = ' . $this->encodeValue($currentValue) . "\n";
+                    }
+                } else {
+                    $ini .= $option . ' = ' . $this->encodeValue($value) . "\n";
+                }
+            }
+
+            $ini .= "\n";
+        }
+
+        return $ini;
+    }
+
+    private function encodeValue($value)
+    {
+        if (is_bool($value)) {
+            return (int) $value;
+        }
+        if (is_string($value)) {
+            return "\"$value\"";
+        }
+        return $value;
+    }
+}
diff --git a/core/Ini/IniWritingException.php b/core/Ini/IniWritingException.php
new file mode 100644
index 0000000000000000000000000000000000000000..10982fa64d2d1cfaa6b1c8ed0d13261fc988582e
--- /dev/null
+++ b/core/Ini/IniWritingException.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Ini;
+
+/**
+ * Exception when writing a INI configuration.
+ */
+class IniWritingException extends \Exception
+{
+}
diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index 751c4ce1abda7eb3f00edb114d4051d5ec7b6721..d22f3590a52b252d6c94e1c7e025f58824162944 100644
--- a/tests/PHPUnit/Framework/Fixture.php
+++ b/tests/PHPUnit/Framework/Fixture.php
@@ -18,6 +18,7 @@ use Piwik\Date;
 use Piwik\Db;
 use Piwik\DbHelper;
 use Piwik\EventDispatcher;
+use Piwik\Ini\IniReader;
 use Piwik\Log;
 use Piwik\Option;
 use Piwik\Piwik;
@@ -826,7 +827,8 @@ class Fixture extends \PHPUnit_Framework_Assert
 
         $this->log("Dropping database '$dbName'...");
 
-        $config = _parse_ini_file(PIWIK_INCLUDE_PATH . '/config/config.ini.php', true);
+        $iniReader = new IniReader();
+        $config = $iniReader->readFile(PIWIK_INCLUDE_PATH . '/config/config.ini.php');
         $originalDbName = $config['database']['dbname'];
         if ($dbName == $originalDbName
             && $dbName != 'piwik_tests'
diff --git a/tests/PHPUnit/Integration/ReleaseCheckListTest.php b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
index 95aaccb43f667273a2a9c5794e0afdd560f7adf1..189a560c8547ae711b5b8477448a11ba34de9277 100644
--- a/tests/PHPUnit/Integration/ReleaseCheckListTest.php
+++ b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
@@ -11,6 +11,7 @@ namespace Piwik\Tests\Integration;
 use Exception;
 use Piwik\Config;
 use Piwik\Filesystem;
+use Piwik\Ini\IniReader;
 use Piwik\Plugin\Manager;
 use Piwik\Tracker;
 use RecursiveDirectoryIterator;
@@ -26,7 +27,8 @@ class ReleaseCheckListTest extends \PHPUnit_Framework_TestCase
 
     public function setUp()
     {
-        $this->globalConfig = _parse_ini_file(PIWIK_PATH_TEST_TO_ROOT . '/config/global.ini.php', true);
+        $iniReader = new IniReader();
+        $this->globalConfig = $iniReader->readFile(PIWIK_PATH_TEST_TO_ROOT . '/config/global.ini.php');
 
         parent::setUp();
     }
diff --git a/tests/PHPUnit/Unit/ConfigTest.php b/tests/PHPUnit/Unit/ConfigTest.php
index 3543871b1f7ff792a65bc73c986aad3ae8e95a9a..2fe32fec68cbb64a3354de73978b6aa78ad91575 100644
--- a/tests/PHPUnit/Unit/ConfigTest.php
+++ b/tests/PHPUnit/Unit/ConfigTest.php
@@ -238,135 +238,135 @@ class ConfigTest extends PHPUnit_Framework_TestCase
             //   CACHE
             //   --> EXPECTED <--
             array('global only, not cached', array(
-                array(),                                    // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
+                array(),                                  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
                 array(),
                 false,
             )),
 
             array('global only, cached get', array(
-                array(),                                    // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
-                array('General' => array('debug' => '1')),
+                array(),                                  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
+                array('General' => array('debug' => 1)),
                 false,
             )),
 
             array('global only, cached set', array(
-                array(),                                    // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
-                array('General' => array('debug' => '2')),
+                array(),                                  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
+                array('General' => array('debug' => 2)),
                 $header . "[General]\ndebug = 2\n\n",
             )),
 
             array('local copy (same), not cached', array(
-                array('General' => array('debug' => '1')),  // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
+                array('General' => array('debug' => 1)),  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
                 array(),
                 false,
             )),
 
             array('local copy (same), cached get', array(
-                array('General' => array('debug' => '1')),  // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
-                array('General' => array('debug' => '1')),
+                array('General' => array('debug' => 1)),  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
+                array('General' => array('debug' => 1)),
                 false,
             )),
 
             array('local copy (same), cached set', array(
-                array('General' => array('debug' => '1')),  // local
-                array('General' => array('debug' => '1')),  // global
+                array('General' => array('debug' => 1)),  // local
+                array('General' => array('debug' => 1)),  // global
                 array(),                                    // common
-                array('General' => array('debug' => '2')),
+                array('General' => array('debug' => 2)),
                 $header . "[General]\ndebug = 2\n\n",
             )),
 
             array('local copy (different), not cached', array(
-                array('General' => array('debug' => '2')),  // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
+                array('General' => array('debug' => 2)),  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
                 array(),
                 false,
             )),
 
             array('local copy (different), cached get', array(
-                array('General' => array('debug' => '2')),  // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
-                array('General' => array('debug' => '2')),
+                array('General' => array('debug' => 2)),  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
+                array('General' => array('debug' => 2)),
                 false,
             )),
 
             array('local copy (different), cached set', array(
-                array('General' => array('debug' => '2')),  // local
-                array('General' => array('debug' => '1')),  // global
-                array(),                                    // common
-                array('General' => array('debug' => '3')),
+                array('General' => array('debug' => 2)),  // local
+                array('General' => array('debug' => 1)),  // global
+                array(),                                  // common
+                array('General' => array('debug' => 3)),
                 $header . "[General]\ndebug = 3\n\n",
             )),
 
             array('local copy, not cached, new section', array(
-                array('Tracker' => array('anonymize' => '1')),  // local
-                array('General' => array('debug' => '1')),      // global
-                array(),                                        // common
+                array('Tracker' => array('anonymize' => 1)),  // local
+                array('General' => array('debug' => 1)),      // global
+                array(),                                      // common
                 array(),
                 false,
             )),
 
             array('local copy, cached get, new section', array(
-                array('Tracker' => array('anonymize' => '1')),  // local
-                array('General' => array('debug' => '1')),      // global
-                array(),                                        // common
-                array('Tracker' => array('anonymize' => '1')),
+                array('Tracker' => array('anonymize' => 1)),  // local
+                array('General' => array('debug' => 1)),      // global
+                array(),                                      // common
+                array('Tracker' => array('anonymize' => 1)),
                 false,
             )),
 
             array('local copy, cached set local, new section', array(
-                array('Tracker' => array('anonymize' => '1')),  // local
-                array('General' => array('debug' => '1')),      // global
-                array(),                                        // common
-                array('Tracker' => array('anonymize' => '2')),
+                array('Tracker' => array('anonymize' => 1)),  // local
+                array('General' => array('debug' => 1)),      // global
+                array(),                                      // common
+                array('Tracker' => array('anonymize' => 2)),
                 $header . "[Tracker]\nanonymize = 2\n\n",
             )),
 
             array('local copy, cached set global, new section', array(
-                array('Tracker' => array('anonymize' => '1')),  // local
-                array('General' => array('debug' => '1')),      // global
-                array(),                                        // common
-                array('General' => array('debug' => '2')),
+                array('Tracker' => array('anonymize' => 1)),  // local
+                array('General' => array('debug' => 1)),      // global
+                array(),                                      // common
+                array('General' => array('debug' => 2)),
                 $header . "[General]\ndebug = 2\n\n[Tracker]\nanonymize = 1\n\n",
             )),
 
             array('sort, common sections', array(
-                array('Tracker' => array('anonymize' => '1'),   // local
-                      'General' => array('debug' => '1')),
-                array('General' => array('debug' => '0'),       // global
-                      'Tracker' => array('anonymize' => '0')),
-                array(),                                        // common
-                array('Tracker' => array('anonymize' => '2')),
+                array('Tracker' => array('anonymize' => 1),   // local
+                      'General' => array('debug' => 1)),
+                array('General' => array('debug' => 0),       // global
+                      'Tracker' => array('anonymize' => 0)),
+                array(),                                      // common
+                array('Tracker' => array('anonymize' => 2)),
                 $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 2\n\n",
             )),
 
             array('sort, common sections before new section', array(
-                array('Tracker' => array('anonymize' => '1'),   // local
-                      'General' => array('debug' => '1')),
-                array('General' => array('debug' => '0'),       // global
-                      'Tracker' => array('anonymize' => '0')),
-                array(),                                        // common
+                array('Tracker' => array('anonymize' => 1),   // local
+                      'General' => array('debug' => 1)),
+                array('General' => array('debug' => 0),       // global
+                      'Tracker' => array('anonymize' => 0)),
+                array(),                                      // common
                 array('Segment' => array('dimension' => 'foo')),
                 $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 1\n\n[Segment]\ndimension = \"foo\"\n\n",
             )),
 
             array('change back to default', array(
-                array('Tracker' => array('anonymize' => '1')),  // local
-                array('Tracker' => array('anonymize' => '0'),   // global
-                      'General' => array('debug' => '1')),
+                array('Tracker' => array('anonymize' => 1)),  // local
+                array('Tracker' => array('anonymize' => 0),   // global
+                      'General' => array('debug' => 1)),
                 array(),                                        // common
-                array('Tracker' => array('anonymize' => '0')),
+                array('Tracker' => array('anonymize' => 0)),
                 $header
             )),
 
diff --git a/tests/PHPUnit/Unit/Ini/IniReaderTest.php b/tests/PHPUnit/Unit/Ini/IniReaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..190853377c4064fd51caf87b8ebaedc6b2121449
--- /dev/null
+++ b/tests/PHPUnit/Unit/Ini/IniReaderTest.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Ini;
+
+use Piwik\Ini\IniReader;
+
+class IniReaderTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_readString()
+    {
+        $ini = <<<INI
+[Section 1]
+foo = "bar"
+bool_true_1 = 1
+bool_false_1 = 0
+bool_true_2 = true
+bool_false_2 = false
+bool_true_3 = yes
+bool_false_3 = no
+bool_true_4 = on
+bool_false_4 = off
+empty =
+explicit_null = null
+int = 10
+float = 10.3
+array[] = "string"
+array[] = 10.3
+array[] = 1
+array[] = 0
+array[] = true
+array[] = false
+
+[Section 2]
+foo = "bar"
+INI;
+        $expected = array(
+            'Section 1' => array(
+                'foo' => 'bar',
+                'bool_true_1' => 1,
+                'bool_false_1' => 0,
+                'bool_true_2' => true,
+                'bool_false_2' => false,
+                'bool_true_3' => true,
+                'bool_false_3' => false,
+                'bool_true_4' => true,
+                'bool_false_4' => false,
+                'empty' => null,
+                'explicit_null' => null,
+                'int' => 10,
+                'float' => 10.3,
+                'array' => array(
+                    'string',
+                    10.3,
+                    1,
+                    0,
+                    true,
+                    false,
+                ),
+            ),
+            'Section 2' => array(
+                'foo' => 'bar',
+            ),
+        );
+        $reader = new IniReader();
+        $this->assertSame($expected, $reader->readString($ini));
+    }
+
+    public function test_readString_withEmptyString()
+    {
+        $reader = new IniReader();
+        $this->assertSame(array(), $reader->readString(''));
+    }
+
+    /**
+     * @expectedException \Piwik\Ini\IniReadingException
+     * @expectedExceptionMessage Syntax error in INI configuration
+     */
+    public function test_readString_shouldThrowException_ifInvalidIni()
+    {
+        $reader = new IniReader();
+        $reader->readString('[ test = foo');
+    }
+
+    public function test_readString_shouldIgnoreComments()
+    {
+        $expected = array(
+            'Section 1' => array(
+                'foo' => 'bar',
+            ),
+        );
+        $ini = <<<INI
+; <?php exit; ?> DO NOT REMOVE THIS LINE
+[Section 1]
+foo = "bar"
+INI;
+        $reader = new IniReader();
+        $this->assertSame($expected, $reader->readString($ini));
+    }
+}
diff --git a/tests/PHPUnit/Unit/Ini/IniWriterTest.php b/tests/PHPUnit/Unit/Ini/IniWriterTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ff22076d960a496be73ecf6ff9a22fe08a0b71b
--- /dev/null
+++ b/tests/PHPUnit/Unit/Ini/IniWriterTest.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Unit\Ini;
+
+use Piwik\Ini\IniWriter;
+
+class IniWriterTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_writeToString()
+    {
+        $config = array(
+            'Section 1' => array(
+                'foo' => 'bar',
+                'bool_true' => true,
+                'bool_false' => false,
+                'int' => 10,
+                'float' => 10.3,
+                'array' => array(
+                    'string',
+                    10.3,
+                    true,
+                    false,
+                ),
+            ),
+            'Section 2' => array(
+                'foo' => 'bar',
+            ),
+        );
+        $expected = <<<INI
+[Section 1]
+foo = "bar"
+bool_true = 1
+bool_false = 0
+int = 10
+float = 10.3
+array[] = "string"
+array[] = 10.3
+array[] = 1
+array[] = 0
+
+[Section 2]
+foo = "bar"
+
+
+INI;
+        $writer = new IniWriter();
+        $this->assertEquals($expected, $writer->writeToString($config));
+    }
+
+    public function test_writeToString_withEmptyConfig()
+    {
+        $writer = new IniWriter();
+        $this->assertEquals('', $writer->writeToString(array()));
+    }
+
+    /**
+     * @expectedException \Piwik\Ini\IniWritingException
+     * @expectedExceptionMessage Section "Section 1" doesn't contain an array of values
+     */
+    public function test_writeToString_shouldThrowException_withInvalidConfig()
+    {
+        $writer = new IniWriter();
+        $writer->writeToString(array('Section 1' => 123));
+    }
+
+    public function test_writeToString_shouldAddHeader()
+    {
+        $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
+        $config = array(
+            'Section 1' => array(
+                'foo' => 'bar',
+            ),
+        );
+        $expected = <<<INI
+; <?php exit; ?> DO NOT REMOVE THIS LINE
+[Section 1]
+foo = "bar"
+
+
+INI;
+        $writer = new IniWriter();
+        $this->assertEquals($expected, $writer->writeToString($config, $header));
+    }
+}