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

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorBen Burgess <88810029+bx80@users.noreply.github.com>2021-12-22 23:26:51 +0300
committerGitHub <noreply@github.com>2021-12-22 23:26:51 +0300
commitd68dc6dd06606e8a7611c53d8ee63da8afbf99f7 (patch)
treeb0363d254677ec77d42ff75352992405b41aef81 /core
parent10afd46db0e033e67127fb4e50eea986ed5296e6 (diff)
Show a summary of new features (#18065)
* Added "What is new" notification display, populated by a new event * Removed test example event hook * Added support for applying a link attribute to menu items, fixes layout issue for mobile with html menu items * Updated UI test screenshots * Revert accidental edit * Hide the "What's new" icon if there are no new features to show * Changed to use changes.json, track user last viewed, added ui test * Fix UserManager unit tests broken by new ts_changes_viewed user field * Moved getChanges to separate helper class, added unit test, added user view access check * Updated to add new changes table and populate only on plugin update/install * Added missing fixture class, updated UI screenshots * Updated matomo font to add ringing bell and new releases icons * Fix for integration test * Reworked class structure, removed unnecessary angular directive, merged templates, other tidy ups * built vue files * built vue files * Added null user check, missing table exception handling, show plugin name in change title, better handling of missing change fields * Added sample changes file, moved UserChanges db code to changes model, added return type hints, better db error code handling, various other improvements * Revert accidental UI screenshot commit * Fix for incorrect link name parameter in sample changes, switched back to using $db->query for INSERT IGNORE * Integration test fix, UI screenshot updates * Test fix * Added link styling, show CoreHome changes without plugin prefix in title * Update UI test screenshot * Added styles to the popover, added event for filtering changes * Test fix * UI test screenshot updates Co-authored-by: sgiehl <stefan@matomo.org> Co-authored-by: bx80 <bx80@users.noreply.github.com>
Diffstat (limited to 'core')
-rw-r--r--core/Changes/Model.php219
-rw-r--r--core/Changes/UserChanges.php77
-rw-r--r--core/Db/Schema/Mysql.php19
-rw-r--r--core/Menu/MenuAbstract.php15
-rw-r--r--core/Plugin.php22
-rw-r--r--core/Updates/4.7.0-b2.php70
-rw-r--r--core/Version.php2
7 files changed, 417 insertions, 7 deletions
diff --git a/core/Changes/Model.php b/core/Changes/Model.php
new file mode 100644
index 0000000000..a9b46ad7a3
--- /dev/null
+++ b/core/Changes/Model.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Changes;
+
+use Piwik\Exception\Exception;
+use Piwik\Piwik;
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\Tracker\Db\DbException;
+use Piwik\Updater\Migration;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugin\Manager as PluginManager;
+
+/**
+ * Change model class
+ *
+ * Handle all data access operations for changes
+ *
+ */
+class Model
+{
+
+ const NO_CHANGES_EXIST = 0;
+ const CHANGES_EXIST = 1;
+ const NEW_CHANGES_EXIST = 2;
+
+ private $pluginManager;
+
+ /**
+ * @var Db\AdapterInterface
+ */
+ private $db;
+
+ /**
+ * @param Db\AdapterInterface|null $db
+ * @param PluginManager|null $pluginManager
+ */
+ public function __construct(?Db\AdapterInterface $db = null, ?PluginManager $pluginManager = null)
+ {
+ $this->db = ($db ?? Db::get());
+ $this->pluginManager = ($pluginManager ?? PluginManager::getInstance());
+ }
+
+ /**
+ * Add any new changes for a plugin to the changes table
+ *
+ * @param string $pluginName
+ *
+ * @throws \Exception
+ */
+ public function addChanges(string $pluginName): void
+ {
+ if ($this->pluginManager->isValidPluginName($pluginName) && $this->pluginManager->isPluginInFilesystem($pluginName)) {
+
+ $plugin = $this->pluginManager->loadPlugin($pluginName);
+ if (!$plugin) {
+ return;
+ }
+
+ $changes = $plugin->getChanges();
+ foreach ($changes as $change) {
+ $this->addChange($pluginName, $change);
+ }
+ }
+ }
+
+ /**
+ * Remove all changes for a plugin
+ *
+ * @param string $pluginName
+ */
+ public function removeChanges(string $pluginName): void
+ {
+ $table = Common::prefixTable('changes');
+
+ try {
+ $this->db->query("DELETE FROM " . $table . " WHERE plugin_name = ?", [$pluginName]);
+ } catch (\Exception $e) {
+ if (Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_NOT_EXISTS)) {
+ return;
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Add a change item to the database table
+ *
+ * @param string $pluginName
+ * @param array $change
+ */
+ public function addChange(string $pluginName, array $change): void
+ {
+ if(!isset($change['version']) || !isset($change['title']) || !isset($change['description'])) {
+ StaticContainer::get('Psr\Log\LoggerInterface')->warning(
+ "Change item for plugin {plugin} missing version, title or description fields - ignored",
+ ['plugin' => $pluginName]);
+ return;
+ }
+
+ $table = Common::prefixTable('changes');
+
+ $fields = ['created_time', 'plugin_name', 'version', 'title', 'description'];
+ $params = [Date::now()->getDatetime(), $pluginName, $change['version'], $change['title'], $change['description']];
+
+ if (isset($change['link_name']) && isset($change['link'])) {
+ $fields[] = 'link_name';
+ $fields[] = 'link';
+ $params[] = $change['link_name'];
+ $params[] = $change['link'];
+ }
+
+ $insertSql = 'INSERT IGNORE INTO ' . $table . ' ('.implode(',', $fields).')
+ VALUES ('.Common::getSqlStringFieldsArray($params).')';
+
+ try {
+ $this->db->query($insertSql, $params);
+ } catch (\Exception $e) {
+ if (Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_NOT_EXISTS)) {
+ return;
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Check if any changes items exist
+ *
+ * @param int|null $newerThanId Only count new changes as having a key > than this sequential key
+ *
+ * @return int
+ */
+ public function doChangesExist(?int $newerThanId = null): int
+ {
+ $changes = $this->getChangeItems();
+
+ $all = 0;
+ $new = 0;
+ foreach ($changes as $c) {
+ $all++;
+ if ($newerThanId !== null && isset($c['idchange']) && $c['idchange'] > $newerThanId) {
+ $new++;
+ }
+ }
+
+ if ($all === 0) {
+ return self::NO_CHANGES_EXIST;
+ } else if ($all > 0 && $new === 0) {
+ return self::CHANGES_EXIST;
+ } else {
+ return self::NEW_CHANGES_EXIST;
+ }
+ }
+
+ /**
+ * Return an array of change items from the changes table
+ *
+ * @return array
+ * @throws DbException
+ */
+ public function getChangeItems(): array
+ {
+ $showAtLeast = 10; // Always show at least this number of changes
+ $expireOlderThanDays = 90; // Don't show changes that were added to the table more than x days ago
+
+ $table = Common::prefixTable('changes');
+ $selectSql = "SELECT * FROM " . $table . " WHERE title IS NOT NULL ORDER BY idchange DESC";
+
+ try {
+ $changes = $this->db->fetchAll($selectSql);
+ } catch (\Exception $e) {
+ if (Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_NOT_EXISTS)) {
+ return [];
+ }
+ throw $e;
+ }
+
+ // Remove expired changes, only if there are at more than the minimum changes
+ $cutOffDate = Date::now()->subDay($expireOlderThanDays);
+ foreach ($changes as $k => $change) {
+ if (isset($change['idchange'])) {
+ $changes[$k]['idchange'] = (int)$change['idchange'];
+ }
+ if (count($changes) > $showAtLeast && $change['created_time'] < $cutOffDate) {
+ unset($changes[$k]);
+ }
+ }
+
+ /**
+ * Event triggered before changes are displayed
+ *
+ * Can be used to filter out unwanted changes
+ *
+ * **Example**
+ *
+ * Piwik::addAction('Changes.filterChanges', function ($changes) {
+ * foreach ($changes as $k => $c) {
+ * // Hide changes for the CoreHome plugin
+ * if (isset($c['plugin_name']) && $c['plugin_name'] == 'CoreHome') {
+ * unset($changes[$k]);
+ * }
+ * }
+ * });
+ *
+ * @param array &$changes
+ */
+ Piwik::postEvent('Changes.filterChanges', array(&$changes));
+
+ return $changes;
+ }
+
+}
diff --git a/core/Changes/UserChanges.php b/core/Changes/UserChanges.php
new file mode 100644
index 0000000000..5e971c776d
--- /dev/null
+++ b/core/Changes/UserChanges.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Changes;
+
+use Piwik\Db;
+use Piwik\Changes\Model as ChangesModel;
+use Piwik\Plugins\UsersManager\Model as UsersModel;
+
+/**
+ * CoreHome user changes class
+ */
+class UserChanges
+{
+
+ /**
+ * @var Db\AdapterInterface
+ */
+ private $db;
+ private $user;
+
+ /**
+ * @param array $user
+ * @param Db\AdapterInterface|null $db
+ */
+ public function __construct(array $user, ?Db\AdapterInterface $db = null)
+ {
+ $this->db = ($db ?? Db::get());
+ $this->user = $user;
+ }
+
+ /**
+ * Return a value indicating if there are any changes available to show the user
+ *
+ * @return int Changes\Model::NO_CHANGES_EXIST, Changes\Model::CHANGES_EXIST or Changes\Model::NEW_CHANGES_EXIST
+ * @throws \Exception
+ */
+ public function getNewChangesStatus(): int
+ {
+ $idchangeLastViewed = (isset($this->user['idchange_last_viewed']) ? $this->user['idchange_last_viewed'] : null);
+
+ $changesModel = new ChangesModel($this->db);
+ return $changesModel->doChangesExist($idchangeLastViewed);
+ }
+
+ /**
+ * Return an array of changes and update the user's changes last viewed value
+ *
+ * @return array
+ */
+ public function getChanges(): array
+ {
+ $changesModel = new ChangesModel(Db::get());
+ $changes = $changesModel->getChangeItems();
+
+ // Record the time that changes were viewed for the current user
+ $maxId = null;
+ foreach ($changes as $k => $change) {
+ if ($maxId < $change['idchange']) {
+ $maxId = $change['idchange'];
+ }
+ }
+
+ if ($maxId) {
+ $usersModel = new UsersModel();
+ $usersModel->updateUserFields($this->user['login'], ['idchange_last_viewed' => $maxId]);
+ }
+
+ return $changes;
+ }
+
+}
diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php
index 92d2b882fb..09b0b48922 100644
--- a/core/Db/Schema/Mysql.php
+++ b/core/Db/Schema/Mysql.php
@@ -52,6 +52,7 @@ class Mysql implements SchemaInterface
superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0',
date_registered TIMESTAMP NULL,
ts_password_modified TIMESTAMP NULL,
+ idchange_last_viewed TIMESTAMP NULL,
PRIMARY KEY(login)
) ENGINE=$engine DEFAULT CHARSET=$charset
",
@@ -358,6 +359,19 @@ class Mysql implements SchemaInterface
PRIMARY KEY (`key`)
) ENGINE=$engine DEFAULT CHARSET=$charset
",
+ 'changes' => "CREATE TABLE `{$prefixTables}changes` (
+ `idchange` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
+ `created_time` DATETIME NOT NULL,
+ `plugin_name` VARCHAR(255) NOT NULL,
+ `version` VARCHAR(20) NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `description` TEXT NULL,
+ `link_name` VARCHAR(255) NULL,
+ `link` VARCHAR(255) NULL,
+ PRIMARY KEY(`idchange`),
+ UNIQUE KEY unique_plugin_version_title (`plugin_name`, `version`, `title`)
+ ) ENGINE=$engine DEFAULT CHARSET=$charset
+ ",
);
return $tables;
@@ -574,8 +588,9 @@ class Mysql implements SchemaInterface
// note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin
$db = $this->getDb();
$db->query("INSERT IGNORE INTO " . Common::prefixTable("user") . "
- (`login`, `password`, `email`, `twofactor_secret`, `superuser_access`, `date_registered`, `ts_password_modified`)
- VALUES ( 'anonymous', '', 'anonymous@example.org', '', 0, '$now', '$now' );");
+ (`login`, `password`, `email`, `twofactor_secret`, `superuser_access`, `date_registered`, `ts_password_modified`,
+ `idchange_last_viewed`)
+ VALUES ( 'anonymous', '', 'anonymous@example.org', '', 0, '$now', '$now' , NULL);");
$model = new Model();
$model->addTokenAuth('anonymous', 'anonymous', 'anonymous default token', $now);
diff --git a/core/Menu/MenuAbstract.php b/core/Menu/MenuAbstract.php
index 2783d7f48d..5a276dce2e 100644
--- a/core/Menu/MenuAbstract.php
+++ b/core/Menu/MenuAbstract.php
@@ -98,10 +98,11 @@ abstract class MenuAbstract extends Singleton
* @param bool|string $tooltip An optional tooltip to display or false to display the tooltip.
* @param bool|string $icon An icon classname, such as "icon-add". Only supported by admin menu
* @param bool|string $onclick Will execute the on click handler instead of executing the link. Only supported by admin menu.
+ * @param string $attribute Will add this string as a link attribute.
* @since 2.7.0
* @api
*/
- public function addItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false, $icon = false, $onclick = false)
+ public function addItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false, $icon = false, $onclick = false, $attribute = false)
{
// make sure the idSite value used is numeric (hack-y fix for #3426)
if (isset($url['idSite']) && !is_numeric($url['idSite'])) {
@@ -116,7 +117,8 @@ abstract class MenuAbstract extends Singleton
$order,
$tooltip,
$icon,
- $onclick
+ $onclick,
+ $attribute
);
}
@@ -144,7 +146,7 @@ abstract class MenuAbstract extends Singleton
* @param int $order
* @param bool|string $tooltip Tooltip to display.
*/
- private function buildMenuItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false, $icon = false, $onclick = false)
+ private function buildMenuItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false, $icon = false, $onclick = false, $attribute = false)
{
if (!isset($this->menu[$menuName])) {
$this->menu[$menuName] = array(
@@ -158,17 +160,22 @@ abstract class MenuAbstract extends Singleton
$this->menu[$menuName]['_order'] = $order;
$this->menu[$menuName]['_name'] = $menuName;
$this->menu[$menuName]['_tooltip'] = $tooltip;
+ $this->menu[$menuName]['_attribute'] = $attribute;
if (!empty($this->menuIcons[$menuName])) {
$this->menu[$menuName]['_icon'] = $this->menuIcons[$menuName];
} else {
$this->menu[$menuName]['_icon'] = '';
}
+ if (!empty($onclick)) {
+ $this->menu[$menuName]['_onclick'] = $onclick;
+ }
}
if (!empty($subMenuName)) {
$this->menu[$menuName][$subMenuName]['_url'] = $url;
$this->menu[$menuName][$subMenuName]['_order'] = $order;
$this->menu[$menuName][$subMenuName]['_name'] = $subMenuName;
$this->menu[$menuName][$subMenuName]['_tooltip'] = $tooltip;
+ $this->menu[$menuName][$subMenuName]['_attribute'] = $attribute;
$this->menu[$menuName][$subMenuName]['_icon'] = $icon;
$this->menu[$menuName][$subMenuName]['_onclick'] = $onclick;
$this->menu[$menuName]['_hasSubmenu'] = true;
@@ -185,7 +192,7 @@ abstract class MenuAbstract extends Singleton
private function buildMenu()
{
foreach ($this->menuEntries as $menuEntry) {
- $this->buildMenuItem($menuEntry[0], $menuEntry[1], $menuEntry[2], $menuEntry[3], $menuEntry[4], $menuEntry[5], $menuEntry[6]);
+ $this->buildMenuItem($menuEntry[0], $menuEntry[1], $menuEntry[2], $menuEntry[3], $menuEntry[4], $menuEntry[5], $menuEntry[6], $menuEntry[7]);
}
}
diff --git a/core/Plugin.php b/core/Plugin.php
index 109592635a..4e1df7eba9 100644
--- a/core/Plugin.php
+++ b/core/Plugin.php
@@ -648,6 +648,28 @@ class Plugin
}
return $dependency;
}
+
+ /**
+ * Get all changes for this plugin
+ *
+ * @return array Array of changes
+ * [{"title":"abc","description":"xyz","linkName":"def","link":"https://link","version":"1.2.3"}]
+ */
+ public function getChanges()
+ {
+ $file = Manager::getPluginDirectory($this->pluginName).'/changes.json';
+ if (file_exists($file)) {
+ $json = file_get_contents($file);
+ if ($json) {
+ $changes = json_decode($json, true);
+ if ($changes && is_array($changes)) {
+ return array_reverse($changes);
+ }
+ }
+ }
+ return [];
+ }
+
}
}
diff --git a/core/Updates/4.7.0-b2.php b/core/Updates/4.7.0-b2.php
new file mode 100644
index 0000000000..3317067c8d
--- /dev/null
+++ b/core/Updates/4.7.0-b2.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Updates;
+
+use Piwik\Updater;
+use Piwik\Updates as PiwikUpdates;
+use Piwik\Updater\Migration;
+use Piwik\Updater\Migration\Factory as MigrationFactory;
+
+/**
+ * Update for version 4.7.0-b2
+ */
+class Updates_4_7_0_b2 extends PiwikUpdates
+{
+ /**
+ * @var MigrationFactory
+ */
+ private $migration;
+
+ public function __construct(MigrationFactory $factory)
+ {
+ $this->migration = $factory;
+ }
+
+ /**
+ * Here you can define one or multiple SQL statements that should be executed during the update.
+ *
+ * @param Updater $updater
+ *
+ * @return Migration[]
+ */
+ public function getMigrations(Updater $updater)
+ {
+ $migrations = [];
+
+ // add column to track the last change a user viewed the changes list
+ $migrations[] = $this->migration->db->addColumn('user', 'idchange_last_viewed',
+ 'INTEGER UNSIGNED NULL');
+
+ $migrations[] = $this->migration->db->createTable('changes', array(
+ 'idchange' => 'INT(11) NOT NULL AUTO_INCREMENT',
+ 'created_time' => 'DATETIME NOT NULL',
+ 'plugin_name' => 'VARCHAR(255) NOT NULL',
+ 'version' => 'VARCHAR(20) NOT NULL',
+ 'title' => 'VARCHAR(255) NOT NULL',
+ 'description' => 'TEXT NOT NULL',
+ 'link_name' => 'VARCHAR(255) NULL',
+ 'link' => 'VARCHAR(255) NULL',
+ ), $primaryKey = 'idchange');
+
+
+ $migrations[] = $this->migration->db->addUniqueKey('changes', ['plugin_name', 'version', 'title'], 'unique_plugin_version_title');
+
+ return $migrations;
+ }
+
+ public function doUpdate(Updater $updater)
+ {
+ $updater->executeMigrations(__FILE__, $this->getMigrations($updater));
+ }
+
+}
diff --git a/core/Version.php b/core/Version.php
index 013b7f6080..2fc51f589e 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -20,7 +20,7 @@ final class Version
* The current Matomo version.
* @var string
*/
- const VERSION = '4.7.0-b1';
+ const VERSION = '4.7.0-b2';
const MAJOR_VERSION = 4;