diff --git a/config/global.ini.php b/config/global.ini.php
index 2793ca1b52fac51337fb078be8f9dade87284dbd..1d013c81b9c4e3f1b7cbbb8d38f8bc06c287aa94 100644
--- a/config/global.ini.php
+++ b/config/global.ini.php
@@ -718,6 +718,7 @@ password = ; Proxy password: optional; if specified, username is mandatory
 Plugins[] = CorePluginsAdmin
 Plugins[] = CoreAdminHome
 Plugins[] = CoreHome
+Plugins[] = WebsiteMeasurable
 Plugins[] = Diagnostics
 Plugins[] = CoreVisualizations
 Plugins[] = Proxy
diff --git a/core/Application/Kernel/PluginList.php b/core/Application/Kernel/PluginList.php
index 66fa64eb4e7138b8da7c7e17d14bc315743d38c3..2c93253385c7e7dcb5382153a1d7c83aedbb4094 100644
--- a/core/Application/Kernel/PluginList.php
+++ b/core/Application/Kernel/PluginList.php
@@ -25,6 +25,27 @@ class PluginList
      */
     private $settings;
 
+    /**
+     * Plugins bundled with core package, disabled by default
+     * @var array
+     */
+    private $corePluginsDisabledByDefault = array(
+        'DBStats',
+        'ExampleCommand',
+        'ExampleSettingsPlugin',
+        'ExampleUI',
+        'ExampleVisualization',
+        'ExamplePluginTemplate',
+        'ExampleTracker',
+        'ExampleReport',
+        'MobileAppMeasurable'
+    );
+
+    // Themes bundled with core package, disabled by default
+    private $coreThemesDisabledByDefault = array(
+        'ExampleTheme'
+    );
+
     public function __construct(GlobalSettingsProvider $settings)
     {
         $this->settings = $settings;
@@ -55,6 +76,16 @@ class PluginList
         return $section['Plugins'];
     }
 
+    /**
+     * Returns the plugins bundled with core package that are disabled by default.
+     *
+     * @return string[]
+     */
+    public function getCorePluginsDisabledByDefault()
+    {
+        return array_merge($this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
+    }
+
     /**
      * Sorts an array of plugins in the order they should be loaded.
      *
@@ -68,6 +99,9 @@ class PluginList
             return $plugins;
         }
 
+        // we need to make sure a possibly disabled plugin will be still loaded before any 3rd party plugin
+        $global = array_merge($global, $this->corePluginsDisabledByDefault);
+
         $global = array_values($global);
         $plugins = array_values($plugins);
 
diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php
index 1e674f2decc83253590ce517dfec160b7c143ae5..8402dc5bd1441a8014c16d49155c7bf288c525f8 100644
--- a/core/Db/Schema/Mysql.php
+++ b/core/Db/Schema/Mysql.php
@@ -102,6 +102,14 @@ class Mysql implements SchemaInterface
                             ) ENGINE=$engine DEFAULT CHARSET=utf8
             ",
 
+            'site_setting'    => "CREATE TABLE {$prefixTables}site_setting (
+                          idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+                          `setting_name` VARCHAR(255) NOT NULL,
+                          `setting_value` LONGTEXT NOT NULL,
+                              PRIMARY KEY(idsite, setting_name)
+                            ) ENGINE=$engine DEFAULT CHARSET=utf8
+            ",
+
             'site_url'    => "CREATE TABLE {$prefixTables}site_url (
                               idsite INTEGER(10) UNSIGNED NOT NULL,
                               url VARCHAR(255) NOT NULL,
diff --git a/core/Measurable/Measurable.php b/core/Measurable/Measurable.php
new file mode 100644
index 0000000000000000000000000000000000000000..d80c1f032330dc707fe5734201f1f4090e4087b0
--- /dev/null
+++ b/core/Measurable/Measurable.php
@@ -0,0 +1,32 @@
+<?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\Measurable;
+
+use Exception;
+use Piwik\Site;
+
+/**
+ * Provides access to individual measurables.
+ */
+class Measurable extends Site
+{
+
+    public function getSettingValue($name)
+    {
+        $settings = new MeasurableSettings($this->id, $this->getType());
+        $setting  = $settings->getSetting($name);
+
+        if (!empty($setting)) {
+            return $setting->getValue(); // Calling `getValue` makes sure we respect read permission of this setting
+        }
+
+        throw new Exception(sprintf('Setting %s does not exist', $name));
+    }
+}
diff --git a/core/Measurable/MeasurableSetting.php b/core/Measurable/MeasurableSetting.php
new file mode 100644
index 0000000000000000000000000000000000000000..91e0970442fb950516ab94a8fdec7b40e721f069
--- /dev/null
+++ b/core/Measurable/MeasurableSetting.php
@@ -0,0 +1,70 @@
+<?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\Measurable;
+
+use Piwik\Piwik;
+
+/**
+ * Describes a Type setting for a website, mobile app, ...
+ *
+ * See {@link \Piwik\Plugin\Settings}.
+ */
+class MeasurableSetting extends \Piwik\Settings\Setting
+{
+    /**
+     * By default the value of the type setting is only readable by users having at least view access to one site
+     *
+     * @var bool
+     * @since 2.14.0
+     */
+    public $readableByCurrentUser = false;
+
+    /**
+     * By default the value of the type setting is only writable by users having at least admin access to one site
+     * @var bool
+     * @internal
+     */
+    public $writableByCurrentUser = false;
+
+    /**
+     * Constructor.
+     *
+     * @param string $name The persisted name of the setting.
+     * @param string $title The display name of the setting.
+     */
+    public function __construct($name, $title)
+    {
+        parent::__construct($name, $title);
+
+        $this->writableByCurrentUser = Piwik::isUserHasSomeAdminAccess();
+        $this->readableByCurrentUser = Piwik::isUserHasSomeViewAccess();
+    }
+
+    /**
+     * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns
+     * writable for the current user it will be visible in the Plugin settings UI.
+     *
+     * @return bool
+     */
+    public function isWritableByCurrentUser()
+    {
+        return $this->writableByCurrentUser;
+    }
+
+    /**
+     * Returns `true` if this setting can be displayed for the current user, `false` if otherwise.
+     *
+     * @return bool
+     */
+    public function isReadableByCurrentUser()
+    {
+        return $this->readableByCurrentUser;
+    }
+}
diff --git a/core/Measurable/MeasurableSettings.php b/core/Measurable/MeasurableSettings.php
new file mode 100644
index 0000000000000000000000000000000000000000..d462f4678f0d07fad8b74e69079b7ef70b98174c
--- /dev/null
+++ b/core/Measurable/MeasurableSettings.php
@@ -0,0 +1,103 @@
+<?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\Measurable;
+
+use Piwik\Db;
+use Piwik\Piwik;
+use Piwik\Plugin\Settings;
+use Piwik\Measurable\Settings\Storage;
+use Piwik\Settings\Setting;
+use Piwik\Measurable\Type;
+
+class MeasurableSettings extends Settings
+{
+
+    /**
+     * @var int
+     */
+    private $idSite = null;
+
+    /**
+     * @var string
+     */
+    private $idType = null;
+
+    /**
+     * @param int $idSite The id of a site. If you want to get settings for a not yet created site just pass an empty value ("0")
+     * @param string $idType If no typeId is given, the type of the site will be used.
+     *
+     * @throws \Exception
+     */
+    public function __construct($idSite, $idType)
+    {
+        $this->idSite = $idSite;
+        $this->idType = $idType;
+        $this->storage = new Storage(Db::get(), $this->idSite);
+        $this->pluginName = 'MeasurableSettings';
+
+        $this->init();
+    }
+
+    protected function init()
+    {
+        $typeManager = new Type\Manager();
+        $type = $typeManager->getType($this->idType);
+        $type->configureMeasurableSettings($this);
+
+        /**
+         * This event is posted when generating settings for a Measurable (website). You can add any Measurable settings
+         * that you wish to be shown in the Measurable manager (websites manager). If you need to add settings only for
+         * eg MobileApp measurables you can use eg `$type->getId() === Piwik\Plugins\MobileAppMeasurable\Type::ID` and
+         * add only settings if the condition is true.
+         *
+         * @since Piwik 2.14.0
+         * @deprecated will be removed in Piwik 3.0.0
+         *
+         * @param MeasurableSettings $this
+         * @param \Piwik\Measurable\Type $type
+         * @param int $idSite
+         */
+        Piwik::postEvent('Measurable.initMeasurableSettings', array($this, $type, $this->idSite));
+    }
+
+    public function addSetting(Setting $setting)
+    {
+        if ($this->idSite && $setting instanceof MeasurableSetting) {
+            $setting->writableByCurrentUser = Piwik::isUserHasAdminAccess($this->idSite);
+        }
+
+        parent::addSetting($setting);
+    }
+
+    public function save()
+    {
+        Piwik::checkUserHasAdminAccess($this->idSite);
+
+        $typeManager = new Type\Manager();
+        $type = $typeManager->getType($this->idType);
+
+        /**
+         * Triggered just before Measurable settings are about to be saved. You can use this event for example
+         * to validate not only one setting but multiple ssetting. For example whether username
+         * and password matches.
+         *
+         * @since Piwik 2.14.0
+         * @deprecated will be removed in Piwik 3.0.0
+         *
+         * @param MeasurableSettings $this
+         * @param \Piwik\Measurable\Type $type
+         * @param int $idSite
+         */
+        Piwik::postEvent('Measurable.beforeSaveSettings', array($this, $type, $this->idSite));
+
+        $this->storage->save();
+    }
+
+}
+
diff --git a/core/Measurable/Settings/Storage.php b/core/Measurable/Settings/Storage.php
new file mode 100644
index 0000000000000000000000000000000000000000..df9748af5e98c7b3f84230b11689bf4e84e2933d
--- /dev/null
+++ b/core/Measurable/Settings/Storage.php
@@ -0,0 +1,104 @@
+<?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\Measurable\Settings;
+
+use Piwik\Db;
+use Piwik\Common;
+use Piwik\Settings\Setting;
+
+/**
+ * Storage for site settings
+ */
+class Storage extends \Piwik\Settings\Storage
+{
+    private $idSite = null;
+
+    /**
+     * @var Db
+     */
+    private $db = null;
+
+    private $toBeDeleted = array();
+
+    public function __construct(Db\AdapterInterface $db, $idSite)
+    {
+        $this->db     = $db;
+        $this->idSite = $idSite;
+    }
+
+    protected function deleteSettingsFromStorage()
+    {
+        $table = $this->getTableName();
+        $sql   = "DELETE FROM $table WHERE `idsite` = ?";
+        $bind  = array($this->idSite);
+
+        $this->db->query($sql, $bind);
+    }
+
+    public function deleteValue(Setting $setting)
+    {
+        $this->toBeDeleted[$setting->getName()] = true;
+        parent::deleteValue($setting);
+    }
+
+    public function setValue(Setting $setting, $value)
+    {
+        $this->toBeDeleted[$setting->getName()] = false; // prevent from deleting this setting, we will create/update it
+        parent::setValue($setting, $value);
+    }
+
+    /**
+     * Saves (persists) the current setting values in the database.
+     */
+    public function save()
+    {
+        $table = $this->getTableName();
+
+        foreach ($this->toBeDeleted as $name => $delete) {
+            if ($delete) {
+                $sql  = "DELETE FROM $table WHERE `idsite` = ? and `setting_name` = ?";
+                $bind = array($this->idSite, $name);
+
+                $this->db->query($sql, $bind);
+            }
+        }
+
+        $this->toBeDeleted = array();
+
+        foreach ($this->settingsValues as $name => $value) {
+            $value = serialize($value);
+
+            $sql  = "INSERT INTO $table (`idsite`, `setting_name`, `setting_value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `setting_value` = ?";
+            $bind = array($this->idSite, $name, $value, $value);
+
+            $this->db->query($sql, $bind);
+        }
+    }
+
+    protected function loadSettings()
+    {
+        $sql  = "SELECT `setting_name`, `setting_value` FROM " . $this->getTableName() . " WHERE idsite = ?";
+        $bind = array($this->idSite);
+
+        $settings =$this->db->fetchAll($sql, $bind);
+
+        $flat = array();
+        foreach ($settings as $setting) {
+            $flat[$setting['setting_name']] = unserialize($setting['setting_value']);
+        }
+
+        return $flat;
+    }
+
+    private function getTableName()
+    {
+        return Common::prefixTable('site_setting');
+    }
+}
diff --git a/core/Measurable/Type.php b/core/Measurable/Type.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9457a660fb618218842e9f5a8319db2e423bbd7
--- /dev/null
+++ b/core/Measurable/Type.php
@@ -0,0 +1,62 @@
+<?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\Measurable;
+
+class Type
+{
+    const ID = '';
+    protected $name = 'General_Measurable';
+    protected $namePlural = 'General_Measurables';
+    protected $description = 'Default measurable type';
+    protected $howToSetupUrl = '';
+
+    public function isType($typeId)
+    {
+        // here we should add some point also check whether id matches any extended ID. Eg if
+        // MetaSites extends Websites, then we expected $metaSite->isType('website') to be true (maybe)
+        return $this->getId() === $typeId;
+    }
+
+    public function getId()
+    {
+        $id = static::ID;
+
+        if (empty($id)) {
+            $message = 'Type %s does not define an ID. Set the ID constant to fix this issue';;
+            throw new \Exception(sprintf($message, get_called_class()));
+        }
+
+        return $id;
+    }
+
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    public function getNamePlural()
+    {
+        return $this->namePlural;
+    }
+
+    public function getHowToSetupUrl()
+    {
+        return $this->howToSetupUrl;
+    }
+
+    public function configureMeasurableSettings(MeasurableSettings $settings)
+    {
+    }
+}
+
diff --git a/core/Measurable/Type/Manager.php b/core/Measurable/Type/Manager.php
new file mode 100644
index 0000000000000000000000000000000000000000..cbd35f9349d61af651ba9bc8fbe0349e9de3f8aa
--- /dev/null
+++ b/core/Measurable/Type/Manager.php
@@ -0,0 +1,39 @@
+<?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\Measurable\Type;
+
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Measurable\Type;
+
+class Manager
+{
+    /**
+     * @return Type[]
+     */
+    public function getAllTypes()
+    {
+        return PluginManager::getInstance()->findComponents('Type', '\\Piwik\\Measurable\\Type');
+    }
+
+    /**
+     * @param string $typeId
+     * @return Type|null
+     */
+    public function getType($typeId)
+    {
+        foreach ($this->getAllTypes() as $type) {
+            if ($type->getId() === $typeId) {
+                return $type;
+            }
+        }
+
+        return new Type();
+    }
+}
+
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 62a9954d39b92385bf9504fc3b6b8def8cdfaa82..25434468d14918f518a3f0a0e66645bdaa7582c4 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -78,28 +78,12 @@ class Manager
         'API',
         'Proxy',
         'LanguagesManager',
+        'WebsiteMeasurable',
 
         // default Piwik theme, always enabled
         self::DEFAULT_THEME,
     );
 
-    // Plugins bundled with core package, disabled by default
-    protected $corePluginsDisabledByDefault = array(
-        'DBStats',
-        'ExampleCommand',
-        'ExampleSettingsPlugin',
-        'ExampleUI',
-        'ExampleVisualization',
-        'ExamplePluginTemplate',
-        'ExampleTracker',
-        'ExampleReport'
-    );
-
-    // Themes bundled with core package, disabled by default
-    protected $coreThemesDisabledByDefault = array(
-        'ExampleTheme'
-    );
-
     private $trackerPluginsNotToLoad = array();
 
     /**
@@ -194,11 +178,6 @@ class Manager
         return $this->trackerPluginsNotToLoad;
     }
 
-    public function getCorePluginsDisabledByDefault()
-    {
-        return array_merge($this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
-    }
-
     // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker
     const TRACKER_EVENT_PREFIX = 'Tracker.';
 
@@ -668,7 +647,7 @@ class Manager
     public function isPluginBundledWithCore($name)
     {
         return $this->isPluginEnabledByDefault($name)
-        || in_array($name, $this->getCorePluginsDisabledByDefault())
+        || in_array($name, $this->pluginList->getCorePluginsDisabledByDefault())
         || $name == self::DEFAULT_THEME;
     }
 
@@ -888,9 +867,11 @@ class Manager
      */
     public static function getAllPluginsNames()
     {
+        $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList');
+
         $pluginsToLoad = array_merge(
             self::getInstance()->readPluginsDirectory(),
-            self::getInstance()->getCorePluginsDisabledByDefault()
+            $pluginList->getCorePluginsDisabledByDefault()
         );
         $pluginsToLoad = array_values(array_unique($pluginsToLoad));
         return $pluginsToLoad;
diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php
index fa830aad772ae0eac7299987e1d3353b4d2ad391..5ab8583a5a3b7dd08be16b7b7b9760739378205e 100644
--- a/core/Plugin/Report.php
+++ b/core/Plugin/Report.php
@@ -841,6 +841,7 @@ class Report
         $cacheId = CacheId::languageAware('Reports' . md5(implode('', $reports)));
         $cache   = PiwikCache::getTransientCache();
 
+
         if (!$cache->contains($cacheId)) {
             $instances = array();
 
diff --git a/core/Plugin/Settings.php b/core/Plugin/Settings.php
index 1066ea6aff093c2e8b59c91d8e7e6122e60592b7..c26581e4b1ece96fd983f2285964ec8ceb09b0eb 100644
--- a/core/Plugin/Settings.php
+++ b/core/Plugin/Settings.php
@@ -49,12 +49,12 @@ abstract class Settings
     private $settings = array();
 
     private $introduction;
-    private $pluginName;
+    protected $pluginName;
 
     /**
      * @var StorageInterface
      */
-    private $storage;
+    protected $storage;
 
     /**
      * Constructor.
@@ -181,8 +181,8 @@ abstract class Settings
     {
         $name = $setting->getName();
 
-        if (!ctype_alnum($name)) {
-            $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
+        if (!ctype_alnum(str_replace('_', '', $name))) {
+            $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only underscores, alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
             throw new \Exception($msg);
         }
 
diff --git a/core/Settings/Setting.php b/core/Settings/Setting.php
index 51e794214674b43bb5a31e3a135f66ee4004c4ab..bf8947b19674f5507cbaeacf08debdb24f164bd0 100644
--- a/core/Settings/Setting.php
+++ b/core/Settings/Setting.php
@@ -153,7 +153,7 @@ abstract class Setting
      * @var StorageInterface
      */
     private $storage;
-    private $pluginName;
+    protected $pluginName;
 
     /**
      * Constructor.
@@ -266,11 +266,7 @@ abstract class Setting
      */
     public function setValue($value)
     {
-        $this->checkHasEnoughWritePermission();
-
-        if ($this->validate && $this->validate instanceof \Closure) {
-            call_user_func($this->validate, $value, $this);
-        }
+        $this->validateValue($value);
 
         if ($this->transform && $this->transform instanceof \Closure) {
             $value = call_user_func($this->transform, $value, $this);
@@ -281,6 +277,15 @@ abstract class Setting
         return $this->storage->setValue($this, $value);
     }
 
+    private function validateValue($value)
+    {
+        $this->checkHasEnoughWritePermission();
+
+        if ($this->validate && $this->validate instanceof \Closure) {
+            call_user_func($this->validate, $value, $this);
+        }
+    }
+
     /**
      * @throws \Exception
      */
diff --git a/core/Settings/Storage.php b/core/Settings/Storage.php
index 5e3b8fc793279b878416c9fe524e7e985610bf0e..131c01b111e87a15baf4d461a814395004167019 100644
--- a/core/Settings/Storage.php
+++ b/core/Settings/Storage.php
@@ -24,7 +24,7 @@ class Storage implements StorageInterface
      *
      * @var array
      */
-    private $settingsValues = array();
+    protected $settingsValues = array();
 
     // for lazy loading of setting values
     private $settingValuesLoaded = false;
@@ -52,12 +52,17 @@ class Storage implements StorageInterface
      */
     public function deleteAllValues()
     {
-        Option::delete($this->getOptionKey());
+        $this->deleteSettingsFromStorage();
 
         $this->settingsValues = array();
         $this->settingValuesLoaded = false;
     }
 
+    protected function deleteSettingsFromStorage()
+    {
+        Option::delete($this->getOptionKey());
+    }
+
     /**
      * Returns the current value for a setting. If no value is stored, the default value
      * is be returned.
diff --git a/core/Updates/2.14.0-b2.php b/core/Updates/2.14.0-b2.php
new file mode 100644
index 0000000000000000000000000000000000000000..dfa8c3f58fa59c24f41cd4bd3246883fedc6e5f7
--- /dev/null
+++ b/core/Updates/2.14.0-b2.php
@@ -0,0 +1,43 @@
+<?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\Updates;
+
+use Piwik\Common;
+use Piwik\Updater;
+use Piwik\Updates;
+use Piwik\Db;
+
+class Updates_2_14_0_b2 extends Updates
+{
+    public function getMigrationQueries(Updater $updater)
+    {
+        $dbSettings = new Db\Settings();
+        $engine = $dbSettings->getEngine();
+
+        $table = Common::prefixTable('site_setting');
+
+        $sqlarray = array(
+            "DROP TABLE IF EXISTS `$table`" => false,
+            "CREATE TABLE `$table` (
+                  idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+                  `setting_name` VARCHAR(255) NOT NULL,
+                  `setting_value` LONGTEXT NOT NULL,
+                      PRIMARY KEY(idsite, setting_name)
+                    ) ENGINE=$engine DEFAULT CHARSET=utf8" => 1050,
+        );
+
+        return $sqlarray;
+    }
+
+    public function doUpdate(Updater $updater)
+    {
+        $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater));
+    }
+}
diff --git a/lang/en.json b/lang/en.json
index 9a3023d2f11c09440c97d1fba07182de10e0fa53..0742bb04442eb47aedcda57d875bc6201ee237d6 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -299,6 +299,8 @@
         "Price": "Price",
         "ProductConversionRate": "Product Conversion Rate",
         "ProductRevenue": "Product Revenue",
+        "Measurable": "Measurable",
+        "Measurables": "Measurables",
         "PurchasedProducts": "Purchased Products",
         "Quantity": "Quantity",
         "RangeReports": "Custom date ranges",
diff --git a/plugins/API/API.php b/plugins/API/API.php
index 34f0ad5bb54775d2a4af982e10338a4c59a155fe..d66a0b35a2e41df412526fc0f8124a6df2536348 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -27,6 +27,7 @@ use Piwik\Plugins\API\DataTable\MergeDataTables;
 use Piwik\Plugins\CoreAdminHome\CustomLogo;
 use Piwik\Segment\SegmentExpression;
 use Piwik\Translation\Translator;
+use Piwik\Measurable\Type;
 use Piwik\Version;
 
 require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
@@ -94,6 +95,24 @@ class API extends \Piwik\Plugin\API
         return Metrics::getDefaultMetricTranslations();
     }
 
+    public function getAvailableTypes()
+    {
+        $typeManager = new Type\Manager();
+        $types = $typeManager->getAllTypes();
+
+        $available = array();
+        foreach ($types as $type) {
+            $available[] = array(
+                'id' => $type->getId(),
+                'name' => Piwik::translate($type->getName()),
+                'description' => Piwik::translate($type->getDescription()),
+                'howToSetupUrl' => $type->getHowToSetupUrl()
+            );
+        }
+
+        return $available;
+    }
+
     public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true)
     {
         $segments = array();
diff --git a/plugins/CoreAdminHome/templates/pluginSettings.twig b/plugins/CoreAdminHome/templates/pluginSettings.twig
index 592914b06a15d08a0d0a56939eb8b5aacd281b4c..415c174f79f48a9520b45a32ed7a09dc7e2bb04c 100644
--- a/plugins/CoreAdminHome/templates/pluginSettings.twig
+++ b/plugins/CoreAdminHome/templates/pluginSettings.twig
@@ -1,9 +1,11 @@
+
 {% extends mode == 'user' ? "user.twig" : "admin.twig" %}
 
 {% block content %}
 
     {% import 'macros.twig' as piwik %}
     {% import 'ajaxMacros.twig' as ajax %}
+    {% import 'settingsMacros.twig' as settingsMacro %}
 
     {% if mode == 'user' %}
         <h2 piwik-enriched-headline>{{ 'CoreAdminHome_PersonalPluginSettings'|translate }}</h2>
@@ -32,118 +34,9 @@
 
         <div id="pluginSettings" data-pluginname="{{ pluginName|e('html_attr') }}">
 
-        {% for name, setting in pluginSettings.settings %}
-            {% set settingValue = setting.getValue %}
-
-            <div class="form-group">
-
-                {% if setting.introduction %}
-                    <p>{{ setting.introduction }}</p>
-                {% endif %}
-
-                {% if setting.uiControlType != 'checkbox' %}
-                    <label>{{ setting.title }}</label>
-                {% endif %}
-
-                {% if setting.inlineHelp %}
-                    <div class="form-help">
-                        {{ setting.inlineHelp }}
-                        {% if setting.defaultValue and setting.uiControlType != 'checkbox' and setting.uiControlType != 'radio' %}
-                            <br/>
-                            {{ 'General_Default'|translate }}:
-                            {% if setting.defaultValue is iterable %}
-                                {{ setting.defaultValue|join(', ')|truncate(50) }}
-                            {% else %}
-                                {{ setting.defaultValue|truncate(50) }}
-                            {% endif %}
-                        {% endif %}
-                    </div>
-                {% endif %}
-
-                {% if setting.uiControlType == 'select' or setting.uiControlType == 'multiselect' %}
-                    <select
-                        {% for attr, val in setting.uiControlAttributes %}
-                            {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
-                        {% endfor %}
-                        name="{{ setting.getKey|e('html_attr') }}"
-                        {% if setting.uiControlType == 'multiselect' %}multiple{% endif %}>
-
-                        {% for key, value in setting.availableValues %}
-                            <option value='{{ key }}'
-                                    {% if settingValue is iterable and key in settingValue %}
-                                        selected='selected'
-                                    {% elseif settingValue==key %}
-                                        selected='selected'
-                                    {% endif %}>
-                                {{ value }}
-                            </option>
-                        {% endfor %}
-
-                    </select>
-                {% elseif setting.uiControlType == 'textarea' %}
-                    <textarea style="width: 376px; height: 250px;"
-                        {% for attr, val in setting.uiControlAttributes %}
-                            {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
-                        {% endfor %}
-                        name="{{ setting.getKey|e('html_attr') }}"
-                        >
-                        {{- settingValue -}}
-                    </textarea>
-                {% elseif setting.uiControlType == 'radio' %}
-
-                    {% for key, value in setting.availableValues %}
-                        <label class="radio">
-                            <input
-                                id="name-value-{{ loop.index }}"
-                                {% for attr, val in setting.uiControlAttributes %}
-                                    {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
-                                {% endfor %}
-                                {% if settingValue is sameas(key) %}
-                                    checked="checked"
-                                {% endif %}
-                                type="radio"
-                                value="{{ key|e('html_attr') }}"
-                                name="{{ setting.getKey|e('html_attr') }}" />
-
-                            {{ value }}
-                        </label>
-                    {% endfor %}
-
-                {% elseif setting.uiControlType == 'checkbox' %}
-
-                    <label class="checkbox">
-                        <input id="name-value-{{ loop.index }}"
-                            {% for attr, val in setting.uiControlAttributes %}
-                                {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
-                            {% endfor %}
-                            value="1"
-                            {% if settingValue %}
-                                checked="checked"
-                            {% endif %}
-                            type="checkbox"
-                            name="{{ setting.getKey|e('html_attr') }}">
-
-                        {{ setting.title }}
-                    </label>
-
-                {% else %}
-
-                    <input
-                        {% for attr, val in setting.uiControlAttributes %}
-                            {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
-                        {% endfor %}
-                        class="control_{{ setting.uiControlType|e('html_attr') }}"
-                        type="{{ setting.uiControlType|e('html_attr') }}"
-                        name="{{ setting.getKey|e('html_attr') }}"
-                        value="{{ settingValue|e('html_attr') }}">
-
-                {% endif %}
-
-                <span class='form-description'>{{ setting.description }}</span>
-
-            </div>
-
-        {% endfor %}
+            {% for name, setting in pluginSettings.settings %}
+                {{ settingsMacro.singleSetting(setting, loop.index) }}
+            {% endfor %}
 
         </div>
 
diff --git a/plugins/CoreHome/angularjs/common/directives/dialog.js b/plugins/CoreHome/angularjs/common/directives/dialog.js
index e711d3bc29db4b5b0db7689d1ecf3627fab58368..cce88292df55399bfc790f3ff55768b94d1c92ff 100644
--- a/plugins/CoreHome/angularjs/common/directives/dialog.js
+++ b/plugins/CoreHome/angularjs/common/directives/dialog.js
@@ -29,7 +29,9 @@
                 element.css('display', 'none');
 
                 element.on( "dialogclose", function() {
-                    scope.$apply($parse(attrs.piwikDialog).assign(scope, false));
+                    setTimeout(function () {
+                        scope.$apply($parse(attrs.piwikDialog).assign(scope, false));
+                    }, 0);
                 });
 
                 scope.$watch(attrs.piwikDialog, function(newValue, oldValue) {
@@ -37,6 +39,7 @@
                         piwik.helper.modalConfirm(element, {yes: function() {
                             if (attrs.yes) {
                                 scope.$eval(attrs.yes);
+                                setTimeout(function () { scope.$apply(); }, 0);
                             }
                         }});
                     }
diff --git a/plugins/MobileAppMeasurable/MobileAppMeasurable.php b/plugins/MobileAppMeasurable/MobileAppMeasurable.php
new file mode 100644
index 0000000000000000000000000000000000000000..ded345ca3a2a647862b035bb5da83710cc915941
--- /dev/null
+++ b/plugins/MobileAppMeasurable/MobileAppMeasurable.php
@@ -0,0 +1,13 @@
+<?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\Plugins\MobileAppMeasurable;
+
+class MobileAppMeasurable extends \Piwik\Plugin
+{
+}
diff --git a/plugins/MobileAppMeasurable/Type.php b/plugins/MobileAppMeasurable/Type.php
new file mode 100644
index 0000000000000000000000000000000000000000..45aa4eb0b9c3b5a5f31c7ed1f48fcbe942702645
--- /dev/null
+++ b/plugins/MobileAppMeasurable/Type.php
@@ -0,0 +1,35 @@
+<?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\Plugins\MobileAppMeasurable;
+
+use Piwik\Measurable\MeasurableSetting;
+use Piwik\Measurable\MeasurableSettings;
+
+class Type extends \Piwik\Measurable\Type
+{
+    const ID = 'mobileapp';
+    protected $name = 'MobileAppMeasurable_MobileApp';
+    protected $namePlural = 'MobileAppMeasurable_MobileApps';
+    protected $description = 'MobileAppMeasurable_MobileAppDescription';
+    protected $howToSetupUrl = 'http://developer.piwik.org/guides/tracking-api-clients#mobile-sdks';
+
+    public function configureMeasurableSettings(MeasurableSettings $settings)
+    {
+        $appId = new MeasurableSetting('app_id', 'App-ID');
+        $appId->validate = function ($value) {
+            if (strlen($value) > 100) {
+                throw new \Exception('Only 100 characters are allowed');
+            }
+        };
+
+        $settings->addSetting($appId);
+    }
+
+}
+
diff --git a/plugins/MobileAppMeasurable/lang/en.json b/plugins/MobileAppMeasurable/lang/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..de6c59c8d2146667fff94c572747d60c20674fbb
--- /dev/null
+++ b/plugins/MobileAppMeasurable/lang/en.json
@@ -0,0 +1,7 @@
+{
+  "MobileAppMeasurable": {
+    "MobileApp": "Mobile App",
+    "MobileApps": "Mobile Apps",
+    "MobileAppDescription": " A native mobile app for iOS, Android or any other mobile operating system."
+  }
+}
\ No newline at end of file
diff --git a/plugins/MobileAppMeasurable/plugin.json b/plugins/MobileAppMeasurable/plugin.json
new file mode 100644
index 0000000000000000000000000000000000000000..fa99a021bfee1c64290e73798f0ae8716bcc4b11
--- /dev/null
+++ b/plugins/MobileAppMeasurable/plugin.json
@@ -0,0 +1,4 @@
+{
+  "name": "MobileAppMeasurable",
+  "description": "Analytics for Mobile: lets you measure and analyze Mobile Apps with an optimized perspective of your mobile data."
+}
\ No newline at end of file
diff --git a/plugins/Morpheus/templates/settingsMacros.twig b/plugins/Morpheus/templates/settingsMacros.twig
new file mode 100644
index 0000000000000000000000000000000000000000..da9cb43709a4cc05e477384bbf5a0226c433e6ae
--- /dev/null
+++ b/plugins/Morpheus/templates/settingsMacros.twig
@@ -0,0 +1,124 @@
+{% macro singleSetting(setting, index = 0) %}
+
+    {% set settingValue = setting.getValue %}
+
+    <div class="form-group">
+
+        {% if setting.introduction %}
+            <p>{{ setting.introduction }}</p>
+        {% endif %}
+
+        {{ _self.field(setting, index) }}
+
+        <span class='form-description'>{{ setting.description }}</span>
+
+    </div>
+
+{% endmacro %}
+
+{% macro field(setting, index = -1) %}
+
+    {% if index == -1 %}
+        {% set index = setting.getName %}
+    {% endif %}
+
+    {% set settingValue = setting.getValue %}
+
+        {% if setting.uiControlType != 'checkbox' %}
+            <label>{{ setting.title }}</label>
+        {% endif %}
+
+        {% if setting.inlineHelp %}
+            <div class="form-help">
+                {{ setting.inlineHelp }}
+                {% if setting.defaultValue and setting.uiControlType != 'checkbox' and setting.uiControlType != 'radio' %}
+                    <br/>
+                    {{ 'General_Default'|translate }}:
+                    {% if setting.defaultValue is iterable %}
+                        {{ setting.defaultValue|join(', ')|truncate(50) }}
+                    {% else %}
+                        {{ setting.defaultValue|truncate(50) }}
+                    {% endif %}
+                {% endif %}
+            </div>
+        {% endif %}
+
+        {% if setting.uiControlType == 'select' or setting.uiControlType == 'multiselect' %}
+                    <select
+                        {% for attr, val in setting.uiControlAttributes %}
+            {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
+        {% endfor %}
+                        name="{{ setting.getKey|e('html_attr') }}"
+                        {% if setting.uiControlType == 'multiselect' %}multiple{% endif %}>
+
+                        {% for key, value in setting.availableValues %}
+            <option value='{{ key }}'
+                    {% if settingValue is iterable and key in settingValue %}
+                selected='selected'
+            {% elseif settingValue==key %}
+                selected='selected'
+                    {% endif %}>
+                {{ value }}
+            </option>
+        {% endfor %}
+
+                    </select>
+                {% elseif setting.uiControlType == 'textarea' %}
+                    <textarea style="width: 376px; height: 250px;"
+                        {% for attr, val in setting.uiControlAttributes %}
+            {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
+        {% endfor %}
+                        name="{{ setting.getKey|e('html_attr') }}"
+                        >
+                        {{- settingValue -}}
+                    </textarea>
+                {% elseif setting.uiControlType == 'radio' %}
+
+            {% for key, value in setting.availableValues %}
+                <label class="radio">
+                    <input
+                            id="name-value-{{ index }}"
+                    {% for attr, val in setting.uiControlAttributes %}
+                        {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
+                    {% endfor %}
+                    {% if settingValue is sameas(key) %}
+                        checked="checked"
+                    {% endif %}
+                    type="radio"
+                    value="{{ key|e('html_attr') }}"
+                    name="{{ setting.getKey|e('html_attr') }}" />
+
+                    {{ value }}
+                </label>
+            {% endfor %}
+
+        {% elseif setting.uiControlType == 'checkbox' %}
+
+            <label class="checkbox">
+                <input id="name-value-{{ index }}"
+                {% for attr, val in setting.uiControlAttributes %}
+                    {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
+                {% endfor %}
+                value="1"
+                {% if settingValue %}
+                    checked="checked"
+                {% endif %}
+                type="checkbox"
+                name="{{ setting.getKey|e('html_attr') }}">
+
+                {{ setting.title }}
+            </label>
+
+        {% else %}
+
+            <input
+            {% for attr, val in setting.uiControlAttributes %}
+                {{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
+            {% endfor %}
+            class="control_{{ setting.uiControlType|e('html_attr') }}"
+            type="{{ setting.uiControlType|e('html_attr') }}"
+            name="{{ setting.getKey|e('html_attr') }}"
+            value="{{ settingValue|e('html_attr') }}">
+
+        {% endif %}
+{% endmacro %}
diff --git a/plugins/SitesManager/API.php b/plugins/SitesManager/API.php
index 2076fb2914d403e4b768f7d1f13f0482856b22f4..59af2e66a4190169b41bc3bb7ca9ef92622988ed 100644
--- a/plugins/SitesManager/API.php
+++ b/plugins/SitesManager/API.php
@@ -18,6 +18,7 @@ use Piwik\Metrics\Formatter;
 use Piwik\Network\IPUtils;
 use Piwik\Option;
 use Piwik\Piwik;
+use Piwik\Measurable\MeasurableSettings;
 use Piwik\ProxyHttp;
 use Piwik\Scheduler\Scheduler;
 use Piwik\SettingsPiwik;
@@ -26,6 +27,7 @@ use Piwik\Site;
 use Piwik\Tracker;
 use Piwik\Tracker\Cache;
 use Piwik\Tracker\TrackerCodeGenerator;
+use Piwik\Measurable\Type;
 use Piwik\Url;
 use Piwik\UrlHelper;
 
@@ -501,6 +503,7 @@ class API extends \Piwik\Plugin\API
      * @param null|string $excludedUserAgents
      * @param int $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they
      *                              will be removed. If 0, the default global behavior will be used.
+     * @param array|null $settings JSON serialized settings eg {settingName: settingValue, ...}
      * @see getKeepURLFragmentsGlobal.
      * @param string $type The website type, defaults to "website" if not set.
      *
@@ -520,7 +523,8 @@ class API extends \Piwik\Plugin\API
                             $startDate = null,
                             $excludedUserAgents = null,
                             $keepURLFragments = null,
-                            $type = null)
+                            $type = null,
+                            $settings = null)
     {
         Piwik::checkUserHasSuperUserAccess();
 
@@ -549,9 +553,7 @@ class API extends \Piwik\Plugin\API
         $urls = array_slice($urls, 1);
 
         $bind = array('name'     => $siteName,
-                      'main_url' => $url,
-
-        );
+                      'main_url' => $url);
 
         $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps);
         $bind['excluded_parameters']  = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters);
@@ -578,12 +580,21 @@ class API extends \Piwik\Plugin\API
             $bind['group'] = "";
         }
 
+        if (!empty($settings)) {
+            $this->validateMeasurableSettings($bind['type'], $settings);
+        }
+
         $idSite = $this->getModel()->createSite($bind);
 
         $this->insertSiteUrls($idSite, $urls);
 
         // we reload the access list which doesn't yet take in consideration this new website
         Access::getInstance()->reloadAccess();
+
+        if (!empty($settings)) {
+            $this->updateMeasurableSettings($idSite, $settings);
+        }
+
         $this->postUpdateWebsite($idSite);
 
         /**
@@ -596,6 +607,36 @@ class API extends \Piwik\Plugin\API
         return (int) $idSite;
     }
 
+    private function validateMeasurableSettings($idType, $settings)
+    {
+        $measurableSettings = new MeasurableSettings(0, $idType);
+
+        foreach ($measurableSettings->getSettingsForCurrentUser() as $measurableSetting) {
+            $name = $measurableSetting->getName();
+            if (!empty($settings[$name])) {
+                $measurableSetting->setValue($settings[$name]);
+            }
+        }
+    }
+
+    private function updateMeasurableSettings($idSite, $settings)
+    {
+        $idType = Site::getTypeFor($idSite);
+
+        $measurableSettings = new MeasurableSettings($idSite, $idType);
+
+        foreach ($measurableSettings->getSettingsForCurrentUser() as $measurableSetting) {
+            $name = $measurableSetting->getName();
+            if (!empty($settings[$name])) {
+                $measurableSetting->setValue($settings[$name]);
+            }
+            // we do not clear existing settings if the value is missing.
+            // There can be so many settings added by random plugins one would always clear some settings.
+        }
+
+        $measurableSettings->save();
+    }
+
     private function postUpdateWebsite($idSite)
     {
         Site::clearCache();
@@ -1045,6 +1086,7 @@ class API extends \Piwik\Plugin\API
      * @param int|null $keepURLFragments If 1, URL fragments will be kept when tracking. If 2, they
      *                                   will be removed. If 0, the default global behavior will be used.
      * @param string $type The Website type, default value is "website"
+     * @param array|null $settings JSON serialized settings eg {settingName: settingValue, ...}
      * @throws Exception
      * @see getKeepURLFragmentsGlobal. If null, the existing value will
      *                                   not be modified.
@@ -1066,7 +1108,8 @@ class API extends \Piwik\Plugin\API
                                $startDate = null,
                                $excludedUserAgents = null,
                                $keepURLFragments = null,
-                               $type = null)
+                               $type = null,
+                               $settings = null)
     {
         Piwik::checkUserHasAdminAccess($idSite);
 
@@ -1128,10 +1171,21 @@ class API extends \Piwik\Plugin\API
         list($searchKeywordParameters, $searchCategoryParameters) = $this->checkSiteSearchParameters($searchKeywordParameters, $searchCategoryParameters);
         $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters;
         $bind['sitesearch_category_parameters'] = $searchCategoryParameters;
-        $bind['type'] = $this->checkAndReturnType($type);
+
+        if (!is_null($type)) {
+            $bind['type'] = $this->checkAndReturnType($type);
+        }
+
+        if (!empty($settings)) {
+            $this->validateMeasurableSettings(Site::getTypeFor($idSite), $settings);
+        }
 
         $this->getModel()->updateSite($bind, $idSite);
 
+        if (!empty($settings)) {
+            $this->updateMeasurableSettings($idSite, $settings);
+        }
+
         // we now update the main + alias URLs
         $this->getModel()->deleteSiteAliasUrls($idSite);
 
diff --git a/plugins/SitesManager/Controller.php b/plugins/SitesManager/Controller.php
index 33b00235f617637751fa8bb8ca1277bc25a58359..bbf4bd52c4f2ba19462346047a588208e29f0d5a 100644
--- a/plugins/SitesManager/Controller.php
+++ b/plugins/SitesManager/Controller.php
@@ -12,6 +12,8 @@ use Exception;
 use Piwik\API\ResponseBuilder;
 use Piwik\Common;
 use Piwik\Piwik;
+use Piwik\Measurable\MeasurableSetting;
+use Piwik\Measurable\MeasurableSettings;
 use Piwik\SettingsPiwik;
 use Piwik\Site;
 use Piwik\Tracker\TrackerCodeGenerator;
@@ -33,8 +35,29 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
         return $this->renderTemplate('index');
     }
 
-    public function getGlobalSettings() {
+    public function getMeasurableTypeSettings()
+    {
+        $idSite = Common::getRequestVar('idSite', 0, 'int');
+        $idType = Common::getRequestVar('idType', '', 'string');
+
+        if ($idSite >= 1) {
+            Piwik::checkUserHasAdminAccess($idSite);
+        } else if ($idSite === 0) {
+            Piwik::checkUserHasSomeAdminAccess();
+        } else {
+            throw new Exception('Invalid idSite parameter. IdSite has to be zero or higher');
+        }
+
+        $view = new View('@SitesManager/measurable_type_settings');
+
+        $propSettings   = new MeasurableSettings($idSite, $idType);
+        $view->settings = $propSettings->getSettingsForCurrentUser();
 
+        return $view->render();
+    }
+
+    public function getGlobalSettings()
+    {
         Piwik::checkUserHasSomeViewAccess();
 
         $response = new ResponseBuilder(Common::getRequestVar('format'));
diff --git a/plugins/SitesManager/Menu.php b/plugins/SitesManager/Menu.php
index 3f4d1b02adf3f97a871bc04c6b773f3cdc1fc151..43447f14948ddd5b23bb28e031c7fd57e7ccab69 100644
--- a/plugins/SitesManager/Menu.php
+++ b/plugins/SitesManager/Menu.php
@@ -10,15 +10,49 @@ namespace Piwik\Plugins\SitesManager;
 
 use Piwik\Menu\MenuAdmin;
 use Piwik\Piwik;
+use Piwik\Measurable\Type;
 
 class Menu extends \Piwik\Plugin\Menu
 {
+    private $typeManager;
+
+    public function __construct(Type\Manager $typeManager)
+    {
+        $this->typeManager = $typeManager;
+    }
+
     public function configureAdminMenu(MenuAdmin $menu)
     {
         if (Piwik::isUserHasSomeAdminAccess()) {
-            $menu->addManageItem('SitesManager_Sites',
+            $type = $this->getFirstTypeIfOnlyOneIsInUse();
+
+            $menuName = 'General_Measurables';
+            if ($type) {
+                $menuName = $type->getNamePlural();
+            }
+
+            $menu->addManageItem($menuName,
                                  $this->urlForAction('index'),
                                  $order = 1);
         }
     }
+
+    private function getFirstTypeIfOnlyOneIsInUse()
+    {
+        $types = $this->typeManager->getAllTypes();
+
+        if (count($types) === 1) {
+            // only one type is in use, use this one for the wording
+            return reset($types);
+        } else {
+            // multiple types are activated, check whether only one is actually in use
+            $model   = new Model();
+            $typeIds = $model->getUsedTypeIds();
+
+            if (count($typeIds) === 1) {
+                $typeManager = new Type\Manager();
+                return $typeManager->getType(reset($typeIds));
+            }
+        }
+    }
 }
diff --git a/plugins/SitesManager/Model.php b/plugins/SitesManager/Model.php
index 676c5a6f12cd02a029121322b138e069a7fa9050..ed16f79a061f6b2ea5cd6825db7eb8135a6e15dd 100644
--- a/plugins/SitesManager/Model.php
+++ b/plugins/SitesManager/Model.php
@@ -331,6 +331,22 @@ class Model
         Db::query($query, $bind);
     }
 
+    /**
+     * Returns all used type ids (unique)
+     * @return array of used type ids
+     */
+    public function getUsedTypeIds()
+    {
+        $types = array();
+        $rows = $this->getDb()->fetchAll("SELECT DISTINCT `type` as typeid FROM " . $this->table);
+
+        foreach ($rows as $row) {
+            $types[] = $row['typeid'];
+        }
+
+        return $types;
+    }
+
     /**
      * Insert the list of alias URLs for the website.
      * The URLs must not exist already for this website!
diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php
index b4390f32d9d155fec8058595b504be31cb998f28..6baa7ac427de51c87cefc72aaf1a4b4f90c84b18 100644
--- a/plugins/SitesManager/SitesManager.php
+++ b/plugins/SitesManager/SitesManager.php
@@ -10,7 +10,9 @@ namespace Piwik\Plugins\SitesManager;
 
 use Piwik\Common;
 use Piwik\Archive\ArchiveInvalidator;
+use Piwik\Db;
 use Piwik\Plugins\PrivacyManager\PrivacyManager;
+use Piwik\Measurable\Settings\Storage;
 use Piwik\Tracker\Cache;
 use Piwik\Tracker\Model as TrackerModel;
 
@@ -69,6 +71,9 @@ class SitesManager extends \Piwik\Plugin
 
         $archiveInvalidator = new ArchiveInvalidator();
         $archiveInvalidator->forgetRememberedArchivedReportsToInvalidateForSite($idSite);
+
+        $measurableStorage = new Storage(Db::get(), $idSite);
+        $measurableStorage->deleteAllValues();
     }
 
     /**
@@ -88,6 +93,7 @@ class SitesManager extends \Piwik\Plugin
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-helper.service.js";
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-site.service.js";
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/api-core.service.js";
+        $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js";
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/sites-manager-admin-sites-model.js";
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/multiline-field.directive.js";
         $jsFiles[] = "plugins/SitesManager/angularjs/sites-manager/edit-trigger.directive.js";
@@ -326,7 +332,11 @@ class SitesManager extends \Piwik\Plugin
         $translationKeys[] = "SitesManager_SelectDefaultTimezone";
         $translationKeys[] = "SitesManager_DefaultCurrencyForNewWebsites";
         $translationKeys[] = "SitesManager_SelectDefaultCurrency";
+        $translationKeys[] = "SitesManager_AddMeasurable";
         $translationKeys[] = "SitesManager_AddSite";
+        $translationKeys[] = "SitesManager_XManagement";
+        $translationKeys[] = "SitesManager_ChooseMeasurableTypeHeadline";
+        $translationKeys[] = "General_Measurables";
         $translationKeys[] = "Goals_Ecommerce";
         $translationKeys[] = "SitesManager_NotFound";
     }
diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js
index 85ad64716ff7d1fd687ac9a5d85e0549a1835745..21dc868f7ae59c62d920a00c8917d69be5105b8a 100644
--- a/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js
+++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js
@@ -7,9 +7,9 @@
 (function () {
     angular.module('piwikApp').controller('SitesManagerSiteController', SitesManagerSiteController);
 
-    SitesManagerSiteController.$inject = ['$scope', '$filter', 'sitesManagerApiHelper'];
+    SitesManagerSiteController.$inject = ['$scope', '$filter', 'sitesManagerApiHelper', 'sitesManagerTypeModel'];
 
-    function SitesManagerSiteController($scope, $filter, sitesManagerApiHelper) {
+    function SitesManagerSiteController($scope, $filter, sitesManagerApiHelper, sitesManagerTypeModel) {
 
         var translate = $filter('translate');
 
@@ -17,6 +17,16 @@
 
             initModel();
             initActions();
+
+            sitesManagerTypeModel.fetchTypeById($scope.site.type).then(function (type) {
+                if (type) {
+                    $scope.currentType = type;
+                    $scope.howToSetupUrl = type.howToSetupUrl;
+                    $scope.isInternalSetupUrl = '?' === ('' + type.howToSetupUrl).substr(0, 1);
+                } else {
+                    $scope.currentType = {name: $scope.site.type};
+                }
+            });
         };
 
         var initActions = function () {
@@ -77,6 +87,16 @@
                 }, 'GET');
             }
 
+            var settings = $('.typeSettings fieldset').serializeArray();
+
+            var flatSettings = '';
+            if (settings.length) {
+                flatSettings = {};
+                angular.forEach(settings, function (setting) {
+                    flatSettings[setting.name] = setting.value;
+                });
+            }
+
             ajaxHandler.addParams({
                 siteName: $scope.site.name,
                 timezone: $scope.site.timezone,
@@ -87,9 +107,11 @@
                 excludedUserAgents: $scope.site.excluded_user_agents.join(','),
                 keepURLFragments: $scope.site.keep_url_fragment,
                 siteSearch: $scope.site.sitesearch,
+                type: $scope.site.type,
                 searchKeywordParameters: sendSiteSearchKeywordParams ? $scope.site.sitesearch_keyword_parameters.join(',') : null,
                 searchCategoryParameters: sendSearchCategoryParameters ? $scope.site.sitesearch_category_parameters.join(',') : null,
-                urls: $scope.site.alias_urls
+                urls: $scope.site.alias_urls,
+                settings: flatSettings
             }, 'POST');
 
             ajaxHandler.redirectOnSuccess($scope.redirectParams);
diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js
new file mode 100644
index 0000000000000000000000000000000000000000..1168e82483d27d16965db59a7d19e342d29256c4
--- /dev/null
+++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js
@@ -0,0 +1,52 @@
+/**
+ * Model for Sites Manager. Fetches only sites one has at least Admin permission.
+ */
+(function () {
+    angular.module('piwikApp').factory('sitesManagerTypeModel', sitesManagerTypeModel);
+
+    sitesManagerTypeModel.$inject = ['piwikApi'];
+
+    function sitesManagerTypeModel(piwikApi)
+    {
+        var typesPromise = null;
+
+        var model = {
+            typesById: {},
+            fetchTypeById: fetchTypeById,
+            fetchAvailableTypes: fetchAvailableTypes,
+            hasMultipleTypes: hasMultipleTypes
+        };
+
+        return model;
+
+        function hasMultipleTypes(typeId)
+        {
+            return fetchAvailableTypes().then(function (types) {
+                return types && types.length > 1;
+            });
+        }
+
+        function fetchTypeById(typeId)
+        {
+            return fetchAvailableTypes().then(function () {
+                return model.typesById[typeId];
+            });
+        }
+
+        function fetchAvailableTypes()
+        {
+            if (!typesPromise) {
+                typesPromise = piwikApi.fetch({method: 'API.getAvailableTypes'}).then(function (types) {
+
+                    angular.forEach(types, function (type) {
+                        model.typesById[type.id] = type;
+                    });
+
+                    return types;
+                });
+            }
+
+            return typesPromise;
+        }
+    }
+})();
diff --git a/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js b/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js
index c657537ca728a56271dad1d6e004cf6234aca3f5..5c8f820bfc7429bb4728f040633e8b438a730ab4 100644
--- a/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js
+++ b/plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js
@@ -7,9 +7,9 @@
 (function () {
     angular.module('piwikApp').controller('SitesManagerController', SitesManagerController);
 
-    SitesManagerController.$inject = ['$scope', '$filter', 'coreAPI', 'sitesManagerAPI', 'sitesManagerAdminSitesModel', 'piwik', 'sitesManagerApiHelper'];
+    SitesManagerController.$inject = ['$scope', '$filter', 'coreAPI', 'sitesManagerAPI', 'piwikApi', 'sitesManagerAdminSitesModel', 'piwik', 'sitesManagerApiHelper', 'sitesManagerTypeModel'];
 
-    function SitesManagerController($scope, $filter, coreAPI, sitesManagerAPI, adminSites, piwik, sitesManagerApiHelper) {
+    function SitesManagerController($scope, $filter, coreAPI, sitesManagerAPI, piwikApi, adminSites, piwik, sitesManagerApiHelper, sitesManagerTypeModel) {
 
         var translate = $filter('translate');
 
@@ -38,10 +38,28 @@
 
             $scope.cancelEditSite = cancelEditSite;
             $scope.addSite = addSite;
+            $scope.addNewEntity = addNewEntity;
             $scope.saveGlobalSettings = saveGlobalSettings;
 
             $scope.informSiteIsBeingEdited = informSiteIsBeingEdited;
             $scope.lookupCurrentEditSite = lookupCurrentEditSite;
+
+            $scope.closeAddMeasurableDialog = function () {
+                // I couldn't figure out another way to close that jquery dialog
+                var element  = angular.element('[piwik-dialog="$parent.showAddSiteDialog"]');
+                if (element.parents('ui-dialog') && element.dialog('isOpen')) {
+                    element.dialog('close');
+                }
+            }
+        };
+
+        var initAvailableTypes = function () {
+            return sitesManagerTypeModel.fetchAvailableTypes().then(function (types) {
+                $scope.availableTypes = types;
+                $scope.typeForNewEntity = 'website';
+
+                return types;
+            });
         };
 
         var informSiteIsBeingEdited = function() {
@@ -61,6 +79,8 @@
 
             showLoading();
 
+            var availableTypesPromise = initAvailableTypes();
+
             sitesManagerAPI.getGlobalSettings(function(globalSettings) {
 
                 $scope.globalSettings = globalSettings;
@@ -76,7 +96,9 @@
                 initKeepURLFragmentsList();
 
                 adminSites.fetchLimitedSitesWithAdminAccess(function () {
-                    triggerAddSiteIfRequested();
+                    availableTypesPromise.then(function () {
+                        triggerAddSiteIfRequested();
+                    });
                 });
                 sitesManagerAPI.getSitesIdWithAdminAccess(function (siteIds) {
                     if (siteIds && siteIds.length) {
@@ -90,7 +112,7 @@
             var search = String(window.location.search);
 
             if(piwik.helper.getArrayFromQueryString(search).showaddsite == 1)
-                addSite();
+                addNewEntity();
         };
 
         var initEcommerceSelectOptions = function() {
@@ -181,8 +203,23 @@
             };
         };
 
-        var addSite = function() {
-            $scope.adminSites.sites.push({});
+        var addNewEntity = function () {
+            sitesManagerTypeModel.hasMultipleTypes().then(function (hasMultipleTypes) {
+                if (hasMultipleTypes) {
+                    $scope.showAddSiteDialog = true;
+                } else if ($scope.availableTypes.length === 1) {
+                    var type = $scope.availableTypes[0].id;
+                    addSite(type);
+                }
+            });
+        };
+
+        var addSite = function(type) {
+            if (!type) {
+                type = 'website'; // todo shall we really hard code this or trigger an exception or so?
+            }
+
+            $scope.adminSites.sites.unshift({type: type});
         };
 
         var saveGlobalSettings = function() {
diff --git a/plugins/SitesManager/lang/en.json b/plugins/SitesManager/lang/en.json
index 7ee253b445b5ada78e8f8df57e869161b3942d6a..20977db504988f9b8ae4d50ef0ec1290d82fd901 100644
--- a/plugins/SitesManager/lang/en.json
+++ b/plugins/SitesManager/lang/en.json
@@ -1,6 +1,7 @@
 {
     "SitesManager": {
         "AddSite": "Add a new website",
+        "AddMeasurable": "Add a new measurable",
         "AdvancedTimezoneSupportNotFound": "Advanced timezones support was not found in your PHP (supported in PHP>=5.2). You can still choose a manual UTC offset.",
         "AliasUrlHelp": "It is recommended, but not required, to specify the various URLs, one per line, that your visitors use to access this website. Alias URLs for a website will not appear in the Referrers > Websites report. Note that it is not necessary to specify the URLs with and without 'www' as Piwik automatically considers both.",
         "ChangingYourTimezoneWillOnlyAffectDataForward": "Changing your time zone will only affect data going forward, and will not be applied retroactively.",
@@ -74,6 +75,8 @@
         "Urls": "URLs",
         "UTCTimeIs": "UTC time is %s.",
         "WebsitesManagement": "Websites Management",
+        "XManagement": "Manage %s",
+        "ChooseMeasurableTypeHeadline": "What would you like to measure?",
         "YouCurrentlyHaveAccessToNWebsites": "You currently have access to %s websites.",
         "YourCurrentIpAddressIs": "Your current IP address is %s"
     }
diff --git a/plugins/SitesManager/templates/index.html b/plugins/SitesManager/templates/index.html
index 1c5b9c4087f32d37395b7ed0b83a94589047039e..86041b25e4e1713e988fe0198f4a2c6d328c8fd6 100644
--- a/plugins/SitesManager/templates/index.html
+++ b/plugins/SitesManager/templates/index.html
@@ -6,6 +6,8 @@
 
     <div ng-include="'plugins/SitesManager/templates/sites-list/add-site-link.html?cb=' + cacheBuster"></div>
 
+    <div ng-include="'plugins/SitesManager/templates/sites-list/add-entity-dialog.html?cb=' + cacheBuster"></div>
+
     <div ng-include="'plugins/SitesManager/templates/sites-list/sites-list.html?cb=' + cacheBuster"></div>
 
     <div class="bottomButtonBar" ng-include="'plugins/SitesManager/templates/sites-list/add-site-link.html?cb=' + cacheBuster"></div>
diff --git a/plugins/SitesManager/templates/measurable_type_settings.twig b/plugins/SitesManager/templates/measurable_type_settings.twig
new file mode 100644
index 0000000000000000000000000000000000000000..7c1bb624f25976173adac645fc7305c344f23767
--- /dev/null
+++ b/plugins/SitesManager/templates/measurable_type_settings.twig
@@ -0,0 +1,7 @@
+{% import 'settingsMacros.twig' as settingsMacro %}
+
+{% for name, setting in settings %}
+    <fieldset>
+        {{ settingsMacro.singleSetting(setting, loop.index) }}
+    </fieldset>
+{% endfor %}
diff --git a/plugins/SitesManager/templates/sites-list/add-entity-dialog.html b/plugins/SitesManager/templates/sites-list/add-entity-dialog.html
new file mode 100644
index 0000000000000000000000000000000000000000..f1a16796840ccf9cfdd33b7e1c42bfc8b44e712a
--- /dev/null
+++ b/plugins/SitesManager/templates/sites-list/add-entity-dialog.html
@@ -0,0 +1,16 @@
+<div piwik-dialog="$parent.showAddSiteDialog"
+     title="{{ 'SitesManager_ChooseMeasurableTypeHeadline'|translate }}">
+
+    <div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
+        <div class="ui-dialog-buttonset">
+            <button type="button"
+                    ng-repeat="type in availableTypes"
+                    title="{{ type.description }}"
+                    class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only"
+                    ng-click="addSite(type.id);closeAddMeasurableDialog()"
+                    aria-disabled="false">
+                <span class="ui-button-text">{{ type.name }}</span>
+            </button>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/plugins/SitesManager/templates/sites-list/add-site-link.html b/plugins/SitesManager/templates/sites-list/add-site-link.html
index 284e5bf7bff366dcae2acaf292dd534a32c3f998..af0266816bbe80be6ccaf6697fdf72dec03bfed8 100644
--- a/plugins/SitesManager/templates/sites-list/add-site-link.html
+++ b/plugins/SitesManager/templates/sites-list/add-site-link.html
@@ -1,7 +1,9 @@
 <div ng-show="!siteIsBeingEdited" class="sitesButtonBar clearfix">
 
-    <a ng-show="hasSuperUserAccess" class="btn addSite" ng-click="addSite()" tabindex="1">
-        {{ 'SitesManager_AddSite'|translate }}
+    <a ng-show="hasSuperUserAccess && availableTypes"
+       class="btn addSite"
+       ng-click="addNewEntity()" tabindex="1">
+        {{ availableTypes.length > 1 ? ('SitesManager_AddMeasurable'|translate) : ('SitesManager_AddSite'|translate) }}
     </a>
 
     <div class="search" ng-show="adminSites.hasPrev || adminSites.hasNext || adminSites.searchTerm">
diff --git a/plugins/SitesManager/templates/sites-list/site-fields.html b/plugins/SitesManager/templates/sites-list/site-fields.html
index 75c33940b2b3c4a2a350eeec47bce826a89f2f38..cb679f20bb921769ac322a2000ab171c0795c104 100644
--- a/plugins/SitesManager/templates/sites-list/site-fields.html
+++ b/plugins/SitesManager/templates/sites-list/site-fields.html
@@ -6,43 +6,36 @@
             <h4>{{ site.name }}</h4>
             <ul>
                 <li><span class="title">{{ 'General_Id'|translate }}:</span> {{ site.idsite }}</li>
-                <li>
-                    <span class="title">{{ 'SitesManager_Urls'|translate }}</span>:
-                    {{ site.alias_urls.join(', ') }}
-                </li>
+                <li ng-show="availableTypes.length > 1"><span class="title">Type:</span> {{ currentType.name }}</li>
             </ul>
         </div>
         <div class="col-md-3">
             <ul>
                 <li><span class="title">{{ 'SitesManager_Timezone'|translate }}:</span> {{ site.timezone }}</li>
                 <li><span class="title">{{ 'SitesManager_Currency'|translate }}:</span> {{ site.currency }}</li>
-                <li>
-                    <span class="title">{{ 'Actions_SubmenuSitesearch'|translate }}:</span>
-                    <span ng-switch="site.sitesearch">
-                        <span ng-switch-when="1">{{ 'General_Yes'|translate }}</span>
-                        <span ng-switch-default>{{ 'General_No'|translate }}</span>
-                    </span>
+                <li ng-show="site.ecommerce == 1">
+                    <span class="title">{{ 'Goals_Ecommerce'|translate }}: {{ 'General_Yes'|translate }}</span>
+                </li>
+                <li ng-show="site.sitesearch == 1">
+                    <span class="title">{{ 'Actions_SubmenuSitesearch'|translate }}: {{ 'General_Yes'|translate }}</span>
                 </li>
             </ul>
         </div>
         <div class="col-md-4">
             <ul>
                 <li>
-                    <span class="title">{{ 'Goals_Ecommerce'|translate }}:</span>
-                    <span ng-switch="site.ecommerce">
-                        <span ng-switch-default>{{ 'General_No'|translate }}</span>
-                        <span ng-switch-when="1">{{ 'General_Yes'|translate }}</span>
-                    </span>
+                    <span class="title">{{ 'SitesManager_Urls'|translate }}</span>:
+                    {{ site.alias_urls.join(', ') }}
                 </li>
-                <li>
+                <li ng-show="site.excluded_ips.length">
                     <span class="title">{{ 'SitesManager_ExcludedIps'|translate }}:</span>
                     {{ site.excluded_ips.join(', ') }}
                 </li>
-                <li>
+                <li ng-show="site.excluded_parameters.length">
                     <span class="title">{{ 'SitesManager_ExcludedParameters'|translate }}:</span>
                     {{ site.excluded_parameters.join(', ') }}
                 </li>
-                <li ng-if="globalSettings.siteSpecificUserAgentExcludeEnabled">
+                <li ng-if="globalSettings.siteSpecificUserAgentExcludeEnabled && site.excluded_user_agents.length">
                     <span class="title">{{ 'SitesManager_ExcludedUserAgents'|translate }}:</span>
                     {{ site.excluded_user_agents.join(', ') }}
                 </li>
@@ -62,8 +55,8 @@
                         Delete
                     </span>
                 </li>
-                <li ng-show="site.idsite">
-                    <a href="?module=CoreAdminHome&action=trackingCodeGenerator&idSite={{ site.idsite }}&period={{ period }}&date={{ date }}&updated=false">
+                <li ng-show="site.idsite && howToSetupUrl">
+                    <a target="{{ isInternalSetupUrl ? '_self' : '_blank' }}" ng-href="{{ howToSetupUrl }}{{ isInternalSetupUrl ? '&idSite=' + site.idsite + '&period=' + period + '&date=' + date +'&updated=false' : ''}}">
                         {{ 'SitesManager_ShowTrackingTag'|translate }}</a>
                 </li>
             </ul>
@@ -78,6 +71,11 @@
             <input type="text" ng-model="site.name"/>
         </div>
 
+        <div class="form-group typeSettings"
+             ng-include="'?module=SitesManager&action=getMeasurableTypeSettings&idSite=' + site.idsite + '&idType=' + site.type"
+                >
+        </div>
+
         <div class="form-group">
             <label>{{ 'SitesManager_Urls'|translate }}</label>
             <div class="form-help">
@@ -143,4 +141,4 @@
 
     </div>
 
-</div>
+</div>
\ No newline at end of file
diff --git a/plugins/SitesManager/templates/sites-manager-header.html b/plugins/SitesManager/templates/sites-manager-header.html
index 59762777fdfad18df159546fe0c419b24e406452..167b20ed838c5a1d77a1bc62919640b02246a3f1 100644
--- a/plugins/SitesManager/templates/sites-manager-header.html
+++ b/plugins/SitesManager/templates/sites-manager-header.html
@@ -1,8 +1,9 @@
 <h2
+        ng-show="availableTypes"
         piwik-enriched-headline
         help-url="http://piwik.org/docs/manage-websites/"
         feature-name="{{ 'SitesManager_WebsitesManagement'|translate }}">
-    {{ 'SitesManager_WebsitesManagement'|translate }}
+    {{ 'SitesManager_XManagement'|translate:(availableTypes.length > 1 ? ('General_Measurables'|translate) : ('SitesManager_Sites'|translate)) }}
 </h2>
 
 <p>
diff --git a/plugins/SitesManager/tests/Integration/ApiTest.php b/plugins/SitesManager/tests/Integration/ApiTest.php
index 011ff7a344b137994f264dcadbfbbab395834321..63ec493b0c695a83cc855ac4dcab7579daab38e6 100644
--- a/plugins/SitesManager/tests/Integration/ApiTest.php
+++ b/plugins/SitesManager/tests/Integration/ApiTest.php
@@ -9,9 +9,12 @@
 namespace Piwik\Plugins\SitesManager\tests\Integration;
 
 use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\Plugins\MobileAppMeasurable;
 use Piwik\Plugins\SitesManager\API;
 use Piwik\Plugins\SitesManager\Model;
 use Piwik\Plugins\UsersManager\API as APIUsersManager;
+use Piwik\Measurable\Measurable;
 use Piwik\Site;
 use Piwik\Tests\Framework\Mock\FakeAccess;
 use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
@@ -31,6 +34,8 @@ class ApiTest extends IntegrationTestCase
     {
         parent::setUp();
 
+        Plugin\Manager::getInstance()->activatePlugin('MobileAppMeasurable');
+
         // setup the access layer
         FakeAccess::$superUser = true;
     }
@@ -193,6 +198,40 @@ class ApiTest extends IntegrationTestCase
 
     }
 
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage Only 100 characters are allowed
+     */
+    public function testAddSite_ShouldFailAndNotCreatedASiteIfASettingIsInvalid()
+    {
+        try {
+            $type = MobileAppMeasurable\Type::ID;
+            $settings = array('app_id' => str_pad('test', 789, 't'));
+            $this->addSiteWithType($type, $settings);
+        } catch (Exception $e) {
+
+            // make sure no site created
+            $ids = API::getInstance()->getAllSitesId();
+            $this->assertEquals(array(), $ids);
+
+            throw $e;
+        }
+    }
+
+    public function testAddSite_ShouldSavePassedMeasurableSettings_IfSettingsAreValid()
+    {
+        $type = MobileAppMeasurable\Type::ID;
+        $settings = array('app_id' => 'org.piwik.mobile2');
+        $idSite = $this->addSiteWithType($type, $settings);
+
+        $this->assertSame(1, $idSite);
+
+        $measurable = new Measurable($idSite);
+        $appId = $measurable->getSettingValue('app_id');
+
+        $this->assertSame('org.piwik.mobile2', $appId);
+    }
+
     /**
      * adds a site
      * use by several other unit tests
@@ -213,6 +252,42 @@ class ApiTest extends IntegrationTestCase
         return $idsite;
     }
 
+    private function addSiteWithType($type, $settings)
+    {
+        return API::getInstance()->addSite("name", "http://piwik.net/", $ecommerce = 0,
+            $siteSearch = 1, $searchKeywordParameters = null, $searchCategoryParameters = null,
+            $ip = null,
+            $excludedQueryParameters = null,
+            $timezone = null,
+            $currency = null,
+            $group = null,
+            $startDate = null,
+            $excludedUserAgents = null,
+            $keepURLFragments = null,
+            $type, $settings);
+    }
+
+    private function updateSiteSettings($idSite, $newSiteName, $settings)
+    {
+        return API::getInstance()->updateSite($idSite,
+            $newSiteName,
+            $urls = null,
+            $ecommerce = null,
+            $siteSearch = null,
+            $searchKeywordParameters = null,
+            $searchCategoryParameters = null,
+            $excludedIps = null,
+            $excludedQueryParameters = null,
+            $timezone = null,
+            $currency = null,
+            $group = null,
+            $startDate = null,
+            $excludedUserAgents = null,
+            $keepURLFragments = null,
+            $type = null,
+            $settings);
+    }
+
     /**
      * no duplicate -> all the urls are saved
      */
@@ -795,6 +870,42 @@ class ApiTest extends IntegrationTestCase
         $this->assertEquals($newurls, $allUrls);
     }
 
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage Only 100 characters are allowed
+     */
+    public function testUpdateSite_ShouldFailAndNotUpdateSiteIfASettingIsInvalid()
+    {
+        $type  = MobileAppMeasurable\Type::ID;
+        $idSite = $this->addSiteWithType($type, array());
+
+        try {
+            $this->updateSiteSettings($idSite, 'newSiteName', array('app_id' => str_pad('t', 589, 't')));
+
+        } catch (Exception $e) {
+            // verify nothing was updated (not even the name)
+            $measurable = new Measurable($idSite);
+            $this->assertNotEquals('newSiteName', $measurable->getName());
+
+            throw $e;
+        }
+    }
+
+    public function testUpdateSite_ShouldSavePassedMeasurableSettings_IfSettingsAreValid()
+    {
+        $type = MobileAppMeasurable\Type::ID;
+        $idSite = $this->addSiteWithType($type, array());
+
+        $this->assertSame(1, $idSite);
+
+        $this->updateSiteSettings($idSite, 'newSiteName', $settings = array('app_id' => 'org.piwik.mobile2'));
+
+        // verify it was updated
+        $measurable = new Measurable($idSite);
+        $this->assertSame('newSiteName', $measurable->getName());
+        $this->assertSame('org.piwik.mobile2', $measurable->getSettingValue('app_id'));
+    }
+
     /**
      * @expectedException Exception
      * @expectedExceptionMessage SitesManager_ExceptionDeleteSite
diff --git a/plugins/SitesManager/tests/Integration/ModelTest.php b/plugins/SitesManager/tests/Integration/ModelTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..45ebce2be82871c0a42256fdbf06feaf2d52342f
--- /dev/null
+++ b/plugins/SitesManager/tests/Integration/ModelTest.php
@@ -0,0 +1,66 @@
+<?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\Plugins\SitesManager\tests\Integration;
+
+use Piwik\Plugins\SitesManager\Model;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Plugins
+ * @group ModelTest
+ * @group SitesManager
+ */
+class ModelTest extends IntegrationTestCase
+{
+    /**
+     * @var Model
+     */
+    private $model;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->model = new Model();
+    }
+
+    public function test_getUsedTypeIds_shouldReturnNoType_IfNoSitesExist()
+    {
+        $this->assertSame(array(), $this->model->getUsedTypeIds());
+    }
+
+    public function test_getUsedTypeIds_shouldReturnOnlyOneType_IfAllSitesUseSameType()
+    {
+        for ($i = 0; $i < 9; $i++) {
+            $this->createMeasurable('website');
+        }
+
+        $this->assertSame(array('website'), $this->model->getUsedTypeIds());
+    }
+
+    public function test_getUsedTypeIds_shouldReturnAnotherType_IfDifferentOnesAreUsed()
+    {
+        for ($i = 0; $i < 9; $i++) {
+            $this->createMeasurable('website');
+            $this->createMeasurable('universal');
+            $this->createMeasurable('mobileapp');
+        }
+
+        $this->assertSame(array('website', 'universal', 'mobileapp'), $this->model->getUsedTypeIds());
+    }
+
+    private function createMeasurable($type)
+    {
+        Fixture::createWebsite('2015-01-01 00:00:00',
+            $ecommerce = 0, $siteName = false, $siteUrl = false,
+            $siteSearch = 1, $searchKeywordParameters = null,
+            $searchCategoryParameters = null, $timezone = null, $type);
+    }
+}
diff --git a/plugins/WebsiteMeasurable/Type.php b/plugins/WebsiteMeasurable/Type.php
new file mode 100644
index 0000000000000000000000000000000000000000..714b9dd580c5b11bc0258de595a2629dd3b3cec6
--- /dev/null
+++ b/plugins/WebsiteMeasurable/Type.php
@@ -0,0 +1,19 @@
+<?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\Plugins\WebsiteMeasurable;
+
+class Type extends \Piwik\Measurable\Type
+{
+    const ID = 'website';
+    protected $name = 'Referrers_ColumnWebsite'; // we will use new key of WebsiteType_ once we have them
+    protected $namePlural = 'SitesManager_Sites'; // translated into more languages
+    protected $description = 'WebsiteMeasurable_WebsiteDescription';
+    protected $howToSetupUrl = '?module=CoreAdminHome&action=trackingCodeGenerator';
+}
+
diff --git a/plugins/WebsiteMeasurable/WebsiteMeasurable.php b/plugins/WebsiteMeasurable/WebsiteMeasurable.php
new file mode 100644
index 0000000000000000000000000000000000000000..3199a4183088edb9a367a2241e87dcc55bab120d
--- /dev/null
+++ b/plugins/WebsiteMeasurable/WebsiteMeasurable.php
@@ -0,0 +1,13 @@
+<?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\Plugins\WebsiteMeasurable;
+
+class WebsiteMeasurable extends \Piwik\Plugin
+{
+}
diff --git a/plugins/WebsiteMeasurable/lang/en.json b/plugins/WebsiteMeasurable/lang/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..05414e3b1d180a90fafff3ff2ca68e61382d9516
--- /dev/null
+++ b/plugins/WebsiteMeasurable/lang/en.json
@@ -0,0 +1,7 @@
+{
+  "WebsiteMeasurable": {
+    "Website": "Website",
+    "Websites": "Websites",
+    "WebsiteDescription": "A website consists of web pages typically served from a single web domain."
+  }
+}
\ No newline at end of file
diff --git a/plugins/WebsiteMeasurable/plugin.json b/plugins/WebsiteMeasurable/plugin.json
new file mode 100644
index 0000000000000000000000000000000000000000..aa193af98cdccf2dbaf03030b37b2d68b0bb9b89
--- /dev/null
+++ b/plugins/WebsiteMeasurable/plugin.json
@@ -0,0 +1,4 @@
+{
+  "name": "WebsiteMeasurable",
+  "description": "Analytics for the web: lets you measure and analyze Websites."
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index f60a43e0c84f40a2cbf02c8a649345359d85b0d9..f430d1278c94686bf2319540d11d7037c9919d65 100644
--- a/tests/PHPUnit/Framework/Fixture.php
+++ b/tests/PHPUnit/Framework/Fixture.php
@@ -399,11 +399,13 @@ class Fixture extends \PHPUnit_Framework_Assert
      * @param int $siteSearch
      * @param null|string $searchKeywordParameters
      * @param null|string $searchCategoryParameters
+     * @param null|string $timezone
+     * @param null|string $type eg 'website' or 'mobileapp'
      * @return int    idSite of website created
      */
     public static function createWebsite($dateTime, $ecommerce = 0, $siteName = false, $siteUrl = false,
                                          $siteSearch = 1, $searchKeywordParameters = null,
-                                         $searchCategoryParameters = null, $timezone = null)
+                                         $searchCategoryParameters = null, $timezone = null, $type = null)
     {
         if($siteName === false) {
             $siteName = self::DEFAULT_SITE_NAME;
@@ -416,7 +418,12 @@ class Fixture extends \PHPUnit_Framework_Assert
             $ips = null,
             $excludedQueryParameters = null,
             $timezone,
-            $currency = null
+            $currency = null,
+            $group = null,
+            $startDate = null,
+            $excludedUserAgents = null,
+            $keepURLFragments = null,
+            $type
         );
 
         // Manually set the website creation date to a day earlier than the earliest day we record stats for
diff --git a/tests/PHPUnit/Framework/TestingEnvironmentVariables.php b/tests/PHPUnit/Framework/TestingEnvironmentVariables.php
index 78f2ed06f129cba2e3e48655bc1add86c0f4d0ab..51703e26e45d75c3ee90306e26c0cd9ef49d15a7 100644
--- a/tests/PHPUnit/Framework/TestingEnvironmentVariables.php
+++ b/tests/PHPUnit/Framework/TestingEnvironmentVariables.php
@@ -59,9 +59,10 @@ class TestingEnvironmentVariables
     public function getCoreAndSupportedPlugins()
     {
         $settings = new \Piwik\Application\Kernel\GlobalSettingsProvider();
-        $pluginManager = new PluginManager(new \Piwik\Application\Kernel\PluginList($settings));
+        $pluginList = new \Piwik\Application\Kernel\PluginList($settings);
+        $pluginManager = new PluginManager($pluginList);
 
-        $disabledPlugins = $pluginManager->getCorePluginsDisabledByDefault();
+        $disabledPlugins = $pluginList->getCorePluginsDisabledByDefault();
         $disabledPlugins[] = 'LoginHttpAuth';
         $disabledPlugins[] = 'ExampleVisualization';
 
diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0e45286e7dc73413e175306d5d106fc7271aab3
--- /dev/null
+++ b/tests/PHPUnit/Integration/Measurable/MeasurableSettingTest.php
@@ -0,0 +1,82 @@
+<?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\Integration\Measurable;
+
+use Piwik\Measurable\MeasurableSetting;
+use Piwik\Settings\Storage;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class MeasurableSettingTest extends IntegrationTestCase
+{
+    public function setUp()
+    {
+        parent::setUp();
+        FakeAccess::$superUser = true;
+    }
+
+    private function createSetting()
+    {
+        $setting = new MeasurableSetting('name', 'test');
+        $storage = new Storage('test');
+        $setting->setStorage($storage);
+        return $setting;
+    }
+
+    public function test_setValue_getValue_shouldSucceed_IfEnoughPermission()
+    {
+        $setting = $this->createSetting();
+        $setting->setValue('test');
+        $value = $setting->getValue();
+
+        $this->assertSame('test', $value);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage CoreAdminHome_PluginSettingChangeNotAllowed
+     */
+    public function testSetValue_shouldThrowException_IfOnlyViewPermission()
+    {
+        FakeAccess::clearAccess();
+        FakeAccess::setIdSitesView(array(1, 2, 3));
+        $this->createSetting()->setValue('test');
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage CoreAdminHome_PluginSettingChangeNotAllowed
+     */
+    public function testSetValue_shouldThrowException_IfNoPermissionAtAll()
+    {
+        FakeAccess::clearAccess();
+        $this->createSetting()->setValue('test');
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage CoreAdminHome_PluginSettingReadNotAllowed
+     */
+    public function testGetSettingValue_shouldThrowException_IfNoPermissionToRead()
+    {
+        FakeAccess::clearAccess();
+        $this->createSetting()->getValue();
+    }
+
+    public function provideContainerConfig()
+    {
+        return array(
+            'Piwik\Access' => new FakeAccess()
+        );
+    }
+
+}
diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf17fc634e77a41fb2c7d2b566641410b2d158ad
--- /dev/null
+++ b/tests/PHPUnit/Integration/Measurable/MeasurableSettingsTest.php
@@ -0,0 +1,109 @@
+<?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\Integration\Measurable;
+
+use Piwik\Db;
+use Piwik\Plugin;
+use Piwik\Plugins\MobileAppMeasurable\Type as MobileAppType;
+use Piwik\Measurable\MeasurableSetting;
+use Piwik\Measurable\MeasurableSettings;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class MeasurableSettingsTest extends IntegrationTestCase
+{
+    private $idSite = 1;
+
+    /**
+     * @var MeasurableSettings
+     */
+    private $settings;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        FakeAccess::$superUser = true;
+
+        Plugin\Manager::getInstance()->activatePlugin('MobileAppMeasurable');
+
+        if (!Fixture::siteCreated($this->idSite)) {
+            $type = MobileAppType::ID;
+            Fixture::createWebsite('2015-01-01 00:00:00',
+                $ecommerce = 0, $siteName = false, $siteUrl = false,
+                $siteSearch = 1, $searchKeywordParameters = null,
+                $searchCategoryParameters = null, $timezone = null, $type);
+        }
+
+        $this->settings = $this->createSettings();
+    }
+
+    public function test_init_shouldAddSettingsFromType()
+    {
+        $this->assertNotEmpty($this->settings->getSetting('app_id'));
+    }
+
+    public function test_save_shouldActuallyStoreValues()
+    {
+        $this->settings->getSetting('test2')->setValue('value2');
+        $this->settings->getSetting('test3')->setValue('value3');
+
+        $this->assertStoredSettingsValue(null, 'test2');
+        $this->assertStoredSettingsValue(null, 'test3');
+
+        $this->settings->save();
+
+        $this->assertStoredSettingsValue('value2', 'test2');
+        $this->assertStoredSettingsValue('value3', 'test3');
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage checkUserHasAdminAccess
+     */
+    public function test_save_shouldCheckAdminPermissionsForThatSite()
+    {
+        FakeAccess::clearAccess();
+
+        $this->settings->save();
+    }
+
+    private function createSettings()
+    {
+        $settings = new MeasurableSettings($this->idSite, MobileAppType::ID);
+        $settings->addSetting($this->createSetting('test2'));
+        $settings->addSetting($this->createSetting('test3'));
+
+        return $settings;
+    }
+
+    private function createSetting($name)
+    {
+        return new MeasurableSetting($name, $name . ' Name');
+    }
+
+    private function assertStoredSettingsValue($expectedValue, $settingName)
+    {
+        $settings = $this->createSettings();
+        $value    = $settings->getSetting($settingName)->getValue();
+
+        $this->assertSame($expectedValue, $value);
+    }
+
+    public function provideContainerConfig()
+    {
+        return array(
+            'Piwik\Access' => new FakeAccess()
+        );
+    }
+}
diff --git a/tests/PHPUnit/Integration/Measurable/MeasurableTest.php b/tests/PHPUnit/Integration/Measurable/MeasurableTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3da9a5054492b8e4f9b6055e9b0eb9311d33bbf2
--- /dev/null
+++ b/tests/PHPUnit/Integration/Measurable/MeasurableTest.php
@@ -0,0 +1,91 @@
+<?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\Integration\Measurable;
+
+use Piwik\Db;
+use Piwik\Plugins\MobileAppMeasurable\Type as MobileAppType;
+use Piwik\Plugin;
+use Piwik\Measurable\Measurable;
+use Piwik\Measurable\MeasurableSettings;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class MeasurableTest extends IntegrationTestCase
+{
+    private $idSite = 1;
+
+    /**
+     * @var Measurable
+     */
+    private $measurable;
+
+    private $settingName = 'app_id';
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        Plugin\Manager::getInstance()->activatePlugin('MobileAppMeasurable');
+
+        if (!Fixture::siteCreated($this->idSite)) {
+            $type = MobileAppType::ID;
+            Fixture::createWebsite('2015-01-01 00:00:00',
+                $ecommerce = 0, $siteName = false, $siteUrl = false,
+                $siteSearch = 1, $searchKeywordParameters = null,
+                $searchCategoryParameters = null, $timezone = null, $type);
+        }
+
+        $this->measurable = new Measurable($this->idSite);
+    }
+
+    public function testGetSettingValue_shouldReturnValue_IfSettingExistsAndIsReadable()
+    {
+        $setting = new MeasurableSettings($this->idSite, Measurable::getTypeFor($this->idSite));
+        $setting->getSetting($this->settingName)->setValue('mytest');
+
+        $value = $this->measurable->getSettingValue($this->settingName);
+        $this->assertNull($value);
+
+        $setting->save(); // actually save value
+
+        $value = $this->measurable->getSettingValue($this->settingName);
+        $this->assertSame('mytest', $value);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage does not exist
+     */
+    public function testGetSettingValue_shouldThrowException_IfSettingDoesNotExist()
+    {;
+        $this->measurable->getSettingValue('NoTeXisTenT');
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage CoreAdminHome_PluginSettingReadNotAllowed
+     */
+    public function testGetSettingValue_shouldThrowException_IfNoPermissionToRead()
+    {
+        FakeAccess::clearAccess();
+        $this->measurable->getSettingValue('app_id');
+    }
+
+    public function provideContainerConfig()
+    {
+        return array(
+            'Piwik\Access' => new FakeAccess()
+        );
+    }
+
+}
diff --git a/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php b/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9264a51ba349fd06a6e18c968492fc00ecae827b
--- /dev/null
+++ b/tests/PHPUnit/Integration/Measurable/Settings/StorageTest.php
@@ -0,0 +1,188 @@
+<?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\Integration\Measurable\Settings;
+
+use Piwik\Db;
+use Piwik\Measurable\MeasurableSetting;
+use Piwik\Measurable\Settings\Storage;
+use Piwik\Settings\Setting;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class StorageTest extends IntegrationTestCase
+{
+    private $idSite = 1;
+
+    /**
+     * @var Storage
+     */
+    private $storage;
+
+    /**
+     * @var MeasurableSetting
+     */
+    private $setting;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        if (!Fixture::siteCreated($this->idSite)) {
+            Fixture::createWebsite('2015-01-01 00:00:00');
+        }
+
+        $this->storage = $this->createStorage();
+        $this->setting = $this->createSetting('test');
+    }
+
+    private function createStorage($idSite = null)
+    {
+        if (!isset($idSite)) {
+            $idSite = $this->idSite;
+        }
+
+        return new Storage(Db::get(), $idSite);
+    }
+
+    private function createSetting($name)
+    {
+        return new MeasurableSetting($name, $name . ' Name');
+    }
+
+    public function test_getValue_shouldReturnNullByDefault()
+    {
+        $value = $this->storage->getValue($this->setting);
+        $this->assertNull($value);
+    }
+
+    public function test_getValue_shouldReturnADefaultValueIfOneIsSet()
+    {
+        $this->setting->defaultValue = 194.34;
+        $value = $this->storage->getValue($this->setting);
+        $this->assertSame(194.34, $value);
+    }
+
+    public function test_setValue_getValue_shouldSetAndGetActualValue()
+    {
+        $this->storage->setValue($this->setting, 'myRandomVal');
+        $value = $this->storage->getValue($this->setting);
+        $this->assertEquals('myRandomVal', $value);
+    }
+
+    public function test_setValue_shouldNotSaveItInDatabase()
+    {
+        $this->storage->setValue($this->setting, 'myRandomVal');
+
+        // make sure not actually stored
+        $this->assertSettingValue(null, $this->setting);
+    }
+
+    public function test_save_shouldPersistValueInDatabase()
+    {
+        $this->storage->setValue($this->setting, 'myRandomVal');
+        $this->storage->save();
+
+        // make sure actually stored
+        $this->assertSettingValue('myRandomVal', $this->setting);
+    }
+
+    public function test_save_shouldPersistValueForEachSiteInDatabase()
+    {
+        $this->storage->setValue($this->setting, 'myRandomVal');
+        $this->storage->save();
+
+        // make sure actually stored
+        $this->assertSettingValue('myRandomVal', $this->setting);
+
+        $storage = $this->createStorage($idSite = 2);
+        $valueForDifferentSite = $storage->getValue($this->setting);
+        $this->assertNull($valueForDifferentSite);
+    }
+
+    public function test_save_shouldPersistMultipleValues_ContainingInt()
+    {
+        $this->saveMultipleValues();
+
+        $this->assertSettingValue('myRandomVal', $this->setting);
+        $this->assertSettingValue(5, $this->createSetting('test2'));
+        $this->assertSettingValue(array(1, 2, '4'), $this->createSetting('test3'));
+    }
+
+    public function test_deleteAll_ShouldRemoveTheEntireEntry()
+    {
+        $this->saveMultipleValues();
+
+        $this->assertSettingNotEmpty($this->setting);
+        $this->assertSettingNotEmpty($this->createSetting('test2'));
+        $this->assertSettingNotEmpty($this->createSetting('test3'));
+
+        $this->storage->deleteAllValues();
+
+        $this->assertSettingEmpty($this->setting);
+        $this->assertSettingEmpty($this->createSetting('test2'));
+        $this->assertSettingEmpty($this->createSetting('test3'));
+    }
+
+    public function test_deleteValue_ShouldOnlyDeleteOneValue()
+    {
+        $this->saveMultipleValues();
+
+        $this->assertSettingNotEmpty($this->setting);
+        $this->assertSettingNotEmpty($this->createSetting('test2'));
+        $this->assertSettingNotEmpty($this->createSetting('test3'));
+
+        $this->storage->deleteValue($this->createSetting('test2'));
+        $this->storage->save();
+
+        $this->assertSettingEmpty($this->createSetting('test2'));
+
+        $this->assertSettingNotEmpty($this->setting);
+        $this->assertSettingNotEmpty($this->createSetting('test3'));
+    }
+
+    public function test_deleteValue_saveValue_ShouldNotResultInADeletedValue()
+    {
+        $this->saveMultipleValues();
+
+        $this->storage->deleteValue($this->createSetting('test2'));
+        $this->storage->setValue($this->createSetting('test2'), 'PiwikTest');
+        $this->storage->save();
+
+        $this->assertSettingValue('PiwikTest', $this->createSetting('test2'));
+    }
+
+    private function assertSettingValue($expectedValue, $setting)
+    {
+        $value = $this->createStorage()->getValue($setting);
+        $this->assertSame($expectedValue, $value);
+    }
+
+    private function assertSettingNotEmpty(Setting $setting)
+    {
+        $value = $this->createStorage()->getValue($setting);
+        $this->assertNotNull($value);
+    }
+
+    private function assertSettingEmpty(Setting $setting)
+    {
+        $value = $this->createStorage()->getValue($setting);
+        $this->assertNull($value);
+    }
+
+    private function saveMultipleValues()
+    {
+        $this->storage->setValue($this->setting, 'myRandomVal');
+        $this->storage->setValue($this->createSetting('test2'), 5);
+        $this->storage->setValue($this->createSetting('test3'), array(1, 2, '4'));
+        $this->storage->save();
+    }
+}
diff --git a/tests/PHPUnit/Integration/Plugin/SettingsTest.php b/tests/PHPUnit/Integration/Plugin/SettingsTest.php
index a7e76800914cf14ea2787a1f7e8234e8b206cffb..f5ac688d3107045793c1da589f1beccb75a788f2 100644
--- a/tests/PHPUnit/Integration/Plugin/SettingsTest.php
+++ b/tests/PHPUnit/Integration/Plugin/SettingsTest.php
@@ -48,11 +48,11 @@ class SettingsTest extends IntegrationTestCase
 
     /**
      * @expectedException \Exception
-     * @expectedExceptionMessage The setting name "myname_" in plugin "ExampleSettingsPlugin" is not valid. Only alpha and numerical characters are allowed
+     * @expectedExceptionMessage The setting name "myname-" in plugin "ExampleSettingsPlugin" is not valid. Only underscores, alpha and numerical characters are allowed
      */
     public function test_addSetting_shouldThrowException_IfTheSettingNameIsNotValid()
     {
-        $setting = $this->buildUserSetting('myname_', 'mytitle');
+        $setting = $this->buildUserSetting('myname-', 'mytitle');
         $this->settings->addSetting($setting);
     }
 
diff --git a/tests/PHPUnit/Integration/ReleaseCheckListTest.php b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
index 2c92d75c61c67b1748eb22a7204f9e7a89189f66..f147824fb40fd08d52b1039faf2715e6ca603d01 100644
--- a/tests/PHPUnit/Integration/ReleaseCheckListTest.php
+++ b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
@@ -10,6 +10,7 @@ namespace Piwik\Tests\Integration;
 
 use Exception;
 use Piwik\Config;
+use Piwik\Container\StaticContainer;
 use Piwik\Filesystem;
 use Piwik\Ini\IniReader;
 use Piwik\Plugin\Manager;
@@ -235,7 +236,10 @@ class ReleaseCheckListTest extends \PHPUnit_Framework_TestCase
             }
             $manager = Manager::getInstance();
             $isGitSubmodule = $manager->isPluginOfficialAndNotBundledWithCore($pluginName);
-            $disabled = in_array($pluginName, $manager->getCorePluginsDisabledByDefault())  || $isGitSubmodule;
+
+            $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList');
+
+            $disabled = in_array($pluginName, $pluginList->getCorePluginsDisabledByDefault())  || $isGitSubmodule;
 
             $enabled = in_array($pluginName, $pluginsBundledWithPiwik);
 
diff --git a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b4433cb0f29c75dce8880d3566e5395628dbdf6e
--- /dev/null
+++ b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getAvailableTypes.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<id>website</id>
+		<name>Website</name>
+		<description>A website consists of web pages typically served from a single web domain.</description>
+		<howToSetupUrl>?module=CoreAdminHome&amp;action=trackingCodeGenerator</howToSetupUrl>
+	</row>
+</result>
\ No newline at end of file