PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.2
5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / plugins / Installation / FormDatabaseSetup.php
matomo / app / plugins / Installation Last commit date
Exception 2 years ago Widgets 2 years ago config 2 years ago stylesheets 2 years ago templates 2 years ago vue 2 years ago Controller.php 1 year ago FormDatabaseSetup.php 1 year ago FormDefaultSettings.php 1 year ago FormFirstWebsiteSetup.php 1 year ago FormSuperUser.php 1 year ago Installation.php 2 years ago Menu.php 2 years ago Onboarding.php 2 years ago ServerFilesGenerator.php 1 year ago View.php 1 year ago
FormDatabaseSetup.php
303 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Plugins\Installation;
10
11 use Exception;
12 use HTML_QuickForm2_DataSource_Array;
13 use HTML_QuickForm2_Factory;
14 use HTML_QuickForm2_Rule;
15 use Piwik\Config;
16 use Piwik\Db;
17 use Piwik\Db\Adapter;
18 use Piwik\DbHelper;
19 use Piwik\Filesystem;
20 use Piwik\Piwik;
21 use Piwik\QuickForm2;
22 use Zend_Db_Adapter_Exception;
23 /**
24 * phpcs:ignoreFile PSR1.Classes.ClassDeclaration.MultipleClasses
25 */
26 class FormDatabaseSetup extends QuickForm2
27 {
28 const MASKED_PASSWORD_VALUE = '**********';
29 function __construct($id = 'databasesetupform', $method = 'post', $attributes = null, $trackSubmit = \false)
30 {
31 parent::__construct($id, $method, $attributes = array('autocomplete' => 'off'), $trackSubmit);
32 }
33 function init()
34 {
35 HTML_QuickForm2_Factory::registerRule('checkValidFilename', 'Piwik\\Plugins\\Installation\\FormDatabaseSetupRuleCheckValidFilename');
36 HTML_QuickForm2_Factory::registerRule('checkValidDbname', 'Piwik\\Plugins\\Installation\\FormDatabaseSetupRuleCheckValidDbname');
37 HTML_QuickForm2_Factory::registerRule('checkUserPrivileges', 'Piwik\\Plugins\\Installation\\RuleCheckUserPrivileges');
38 $availableAdapters = Adapter::getAdapters();
39 $adapters = array();
40 foreach ($availableAdapters as $adapter) {
41 $adapters[$adapter] = $adapter;
42 if (Adapter::isRecommendedAdapter($adapter)) {
43 $adapters[$adapter] .= ' (' . Piwik::translate('General_Recommended') . ')';
44 }
45 }
46 $this->addElement('text', 'host')->setLabel(Piwik::translate('Installation_DatabaseSetupServer'))->addRule('required', Piwik::translate('General_Required', Piwik::translate('Installation_DatabaseSetupServer')));
47 $user = $this->addElement('text', 'username')->setLabel(Piwik::translate('Installation_DatabaseSetupLogin'));
48 $user->addRule('required', Piwik::translate('General_Required', Piwik::translate('Installation_DatabaseSetupLogin')));
49 $requiredPrivileges = \Piwik\Plugins\Installation\RuleCheckUserPrivileges::getRequiredPrivilegesPretty();
50 $user->addRule('checkUserPrivileges', Piwik::translate('Installation_InsufficientPrivilegesMain', $requiredPrivileges . '<br/><br/>') . Piwik::translate('Installation_InsufficientPrivilegesHelp'));
51 $this->addElement('password', 'password')->setLabel(Piwik::translate('General_Password'));
52 $item = $this->addElement('text', 'dbname')->setLabel(Piwik::translate('Installation_DatabaseSetupDatabaseName'));
53 $item->addRule('required', Piwik::translate('General_Required', Piwik::translate('Installation_DatabaseSetupDatabaseName')));
54 $item->addRule('checkValidDbname', Piwik::translate('General_NotValid', Piwik::translate('Installation_DatabaseSetupDatabaseName')));
55 $this->addElement('text', 'tables_prefix')->setLabel(Piwik::translate('Installation_DatabaseSetupTablePrefix'))->addRule('checkValidFilename', Piwik::translate('General_NotValid', Piwik::translate('Installation_DatabaseSetupTablePrefix')));
56 $this->addElement('select', 'adapter')->setLabel(Piwik::translate('Installation_DatabaseSetupAdapter'))->loadOptions($adapters)->addRule('required', Piwik::translate('General_Required', Piwik::translate('Installation_DatabaseSetupAdapter')));
57 $this->addElement('select', 'schema')->setLabel(Piwik::translate('Installation_DatabaseSetupEngine'))->loadOptions(['Mysql' => 'MySQL', 'Mariadb' => 'MariaDB'])->addRule('required', Piwik::translate('General_Required', Piwik::translate('Installation_DatabaseSetupEngine')));
58 $this->addElement('submit', 'submit', array('value' => Piwik::translate('General_Next') . ' »', 'class' => 'btn'));
59 $defaultDatabaseType = Config::getInstance()->database['type'];
60 $this->addElement('hidden', 'type')->setLabel('Database engine');
61 $defaults = array('host' => '127.0.0.1', 'type' => $defaultDatabaseType, 'tables_prefix' => 'matomo_', 'schema' => 'Mysql', 'port' => '3306');
62 $defaultsEnvironment = array('host', 'adapter', 'tables_prefix', 'username', 'schema', 'password', 'dbname');
63 foreach ($defaultsEnvironment as $name) {
64 $envValue = $this->getEnvironmentSetting($name);
65 if (null !== $envValue) {
66 $defaults[$name] = $envValue;
67 }
68 }
69 if (array_key_exists('password', $defaults)) {
70 $defaults['password'] = self::MASKED_PASSWORD_VALUE;
71 // ensure not to show password in UI
72 }
73 // default values
74 $this->addDataSource(new HTML_QuickForm2_DataSource_Array($defaults));
75 }
76 private function getEnvironmentSetting(string $name) : ?string
77 {
78 $envName = 'DATABASE_' . strtoupper($name);
79 // fyi getenv is case insensitive
80 $envNameMatomo = 'MATOMO_' . $envName;
81 if (is_string(getenv($envNameMatomo))) {
82 return getenv($envNameMatomo);
83 } elseif (is_string(getenv($envName))) {
84 return getenv($envName);
85 }
86 return null;
87 }
88 /**
89 * Creates database object based on form data.
90 *
91 * @throws Exception|Zend_Db_Adapter_Exception
92 * @return array The database connection info. Can be passed into Piwik::createDatabaseObject.
93 */
94 public function createDatabaseObject()
95 {
96 $dbname = trim($this->getSubmitValue('dbname'));
97 if (empty($dbname)) {
98 // disallow database object creation w/ no selected database
99 throw new Exception("No database name");
100 }
101 $adapter = $this->getSubmitValue('adapter');
102 $host = $this->getSubmitValue('host');
103 $tables_prefix = $this->getSubmitValue('tables_prefix');
104 $password = $this->getSubmitValue('password');
105 $passwordFromEnv = $this->getEnvironmentSetting('password');
106 if ($password === self::MASKED_PASSWORD_VALUE && null !== $passwordFromEnv) {
107 $password = $passwordFromEnv;
108 }
109 $schema = $this->getSubmitValue('schema');
110 $dbInfos = array('host' => is_null($host) ? $host : trim($host), 'username' => $this->getSubmitValue('username'), 'password' => $password, 'dbname' => $dbname, 'tables_prefix' => is_null($tables_prefix) ? $tables_prefix : trim($tables_prefix), 'adapter' => $adapter, 'port' => Db\Schema::getDefaultPortForSchema($schema), 'schema' => $schema, 'type' => $this->getSubmitValue('type'), 'enable_ssl' => \false);
111 if (($portIndex = strpos($dbInfos['host'], '/')) !== \false) {
112 // unix_socket=/path/sock.n
113 $dbInfos['port'] = substr($dbInfos['host'], $portIndex);
114 $dbInfos['host'] = '';
115 } else {
116 if (($portIndex = strpos($dbInfos['host'], ':')) !== \false) {
117 // host:port
118 $dbInfos['port'] = substr($dbInfos['host'], $portIndex + 1);
119 $dbInfos['host'] = substr($dbInfos['host'], 0, $portIndex);
120 }
121 }
122 try {
123 @Db::createDatabaseObject($dbInfos);
124 } catch (Zend_Db_Adapter_Exception $e) {
125 $db = Adapter::factory($adapter, $dbInfos, $connect = \false);
126 // database not found, we try to create it
127 if ($db->isErrNo($e, '1049')) {
128 $dbInfosConnectOnly = $dbInfos;
129 $dbInfosConnectOnly['dbname'] = null;
130 @Db::createDatabaseObject($dbInfosConnectOnly);
131 @DbHelper::createDatabase($dbInfos['dbname']);
132 // select the newly created database
133 @Db::createDatabaseObject($dbInfos);
134 } else {
135 throw $e;
136 }
137 }
138 return $dbInfos;
139 }
140 }
141 /**
142 * Validation rule that checks that the supplied DB user has enough privileges.
143 *
144 * The following privileges are required for Matomo to run:
145 * - CREATE
146 * - ALTER
147 * - SELECT
148 * - INSERT
149 * - UPDATE
150 * - DELETE
151 * - DROP
152 * - CREATE TEMPORARY TABLES
153 *
154 */
155 class RuleCheckUserPrivileges extends HTML_QuickForm2_Rule
156 {
157 public const TEST_TABLE_NAME = 'piwik_test_table';
158 public const TEST_TEMP_TABLE_NAME = 'piwik_test_table_temp';
159 /**
160 * Checks that the DB user entered in the form has the necessary privileges for Piwik
161 * to run.
162 */
163 public function validateOwner()
164 {
165 // try and create the database object
166 try {
167 $this->createDatabaseObject();
168 } catch (Exception $ex) {
169 if ($this->isAccessDenied($ex)) {
170 return \false;
171 } else {
172 return \true;
173 // if we can't create the database object, skip this validation
174 }
175 }
176 $db = Db::get();
177 try {
178 // try to drop tables before running privilege tests
179 $this->dropExtraTables($db);
180 } catch (Exception $ex) {
181 if ($this->isAccessDenied($ex)) {
182 return \false;
183 } else {
184 throw $ex;
185 }
186 }
187 // check each required privilege by running a query that uses it
188 foreach (self::getRequiredPrivileges() as $privilegeType => $queries) {
189 if (!is_array($queries)) {
190 $queries = array($queries);
191 }
192 foreach ($queries as $sql) {
193 try {
194 if (in_array($privilegeType, array('SELECT'))) {
195 $ret = $db->fetchAll($sql);
196 } else {
197 $ret = $db->exec($sql);
198 }
199 // In case an exception is not thrown check the return
200 if ($ret === -1) {
201 return \false;
202 }
203 } catch (Exception $ex) {
204 if ($this->isAccessDenied($ex)) {
205 return \false;
206 } else {
207 throw new Exception("Test SQL failed to execute: {$sql}\nError: " . $ex->getMessage());
208 }
209 }
210 }
211 }
212 // remove extra tables that were created
213 $this->dropExtraTables($db);
214 return \true;
215 }
216 /**
217 * Returns an array describing the database privileges required for Matomo to run. The
218 * array maps privilege names with one or more SQL queries that can be used to test
219 * if the current user has the privilege.
220 *
221 * NOTE: LOAD DATA INFILE & LOCK TABLES privileges are not **required** so they're
222 * not checked.
223 *
224 * @return array
225 */
226 public static function getRequiredPrivileges()
227 {
228 return array('CREATE' => 'CREATE TABLE ' . self::TEST_TABLE_NAME . ' (
229 id INT AUTO_INCREMENT,
230 value INT,
231 PRIMARY KEY (id),
232 KEY index_value (value)
233 )', 'ALTER' => 'ALTER TABLE ' . self::TEST_TABLE_NAME . '
234 ADD COLUMN other_value INT DEFAULT 0', 'SELECT' => 'SELECT * FROM ' . self::TEST_TABLE_NAME, 'INSERT' => 'INSERT INTO ' . self::TEST_TABLE_NAME . ' (value) VALUES (123)', 'UPDATE' => 'UPDATE ' . self::TEST_TABLE_NAME . ' SET value = 456 WHERE id = 1', 'DELETE' => 'DELETE FROM ' . self::TEST_TABLE_NAME . ' WHERE id = 1', 'DROP' => 'DROP TABLE ' . self::TEST_TABLE_NAME, 'CREATE TEMPORARY TABLES' => 'CREATE TEMPORARY TABLE ' . self::TEST_TEMP_TABLE_NAME . ' (
235 id INT AUTO_INCREMENT,
236 PRIMARY KEY (id)
237 )');
238 }
239 /**
240 * Returns a string description of the database privileges required for Matomo to run.
241 *
242 * @return string
243 */
244 public static function getRequiredPrivilegesPretty()
245 {
246 return implode('<br/>', array_keys(self::getRequiredPrivileges()));
247 }
248 /**
249 * Checks if an exception that was thrown after running a query represents an 'access denied'
250 * error.
251 *
252 * @param Exception $ex The exception to check.
253 * @return bool
254 */
255 private function isAccessDenied($ex)
256 {
257 //NOte: this code is duplicated in Tracker.php error handler
258 return $ex->getCode() == 1044 || $ex->getCode() == 42000;
259 }
260 /**
261 * Creates a database object using the connection information entered in the form.
262 *
263 * @return array
264 */
265 private function createDatabaseObject()
266 {
267 return $this->owner->getContainer()->createDatabaseObject();
268 }
269 /**
270 * Drops the tables created by the privilege checking queries, if they exist.
271 *
272 * @param \Piwik\Db $db The database object to use.
273 */
274 private function dropExtraTables($db)
275 {
276 $db->query('DROP TABLE IF EXISTS ' . self::TEST_TABLE_NAME . ', ' . self::TEST_TEMP_TABLE_NAME);
277 }
278 }
279 /**
280 * Filename check for prefix
281 *
282 */
283 class FormDatabaseSetupRuleCheckValidFilename extends HTML_QuickForm2_Rule
284 {
285 function validateOwner()
286 {
287 $prefix = $this->owner->getValue();
288 return empty($prefix) || Filesystem::isValidFilename($prefix);
289 }
290 }
291 /**
292 * Filename check for DB name
293 *
294 */
295 class FormDatabaseSetupRuleCheckValidDbname extends HTML_QuickForm2_Rule
296 {
297 function validateOwner()
298 {
299 $prefix = $this->owner->getValue();
300 return empty($prefix) || DbHelper::isValidDbname($prefix);
301 }
302 }
303