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&action=trackingCodeGenerator</howToSetupUrl> + </row> +</result> \ No newline at end of file