Update website

This commit is contained in:
Guilhem Lavaux 2025-03-24 09:27:39 +01:00
parent a0b0d3dae7
commit ae7ef6ad45
3151 changed files with 566766 additions and 48 deletions

View file

@ -0,0 +1,698 @@
<?php
declare(strict_types=1);
return [
// Queries
[
'id' => 'Uptime below one day',
'name' => __('Uptime below one day'),
'formula' => 'Uptime',
'test' => 'value < 86400',
'issue' => __('Uptime is less than 1 day, performance tuning may not be accurate.'),
'recommendation' => __(
'To have more accurate averages it is recommended to let the server run for'
. ' longer than a day before running this analyzer'
),
'justification' => __('The uptime is only %s'),
'justification_formula' => 'ADVISOR_timespanFormat(Uptime)',
],
[
'id' => 'Questions below 1,000',
'name' => __('Questions below 1,000'),
'formula' => 'Questions',
'test' => 'value < 1000',
'issue' => __(
'Fewer than 1,000 questions have been run against this server.'
. ' The recommendations may not be accurate.'
),
'recommendation' => __(
'Let the server run for a longer time until it has executed a greater amount of queries.'
),
'justification' => __('Current amount of Questions: %s'),
'justification_formula' => 'Questions',
],
[
'id' => 'Percentage of slow queries',
'name' => __('Percentage of slow queries'),
'precondition' => 'Questions > 0',
'formula' => 'Slow_queries / Questions * 100',
'test' => 'value >= 5',
'issue' => __('There is a lot of slow queries compared to the overall amount of Queries.'),
'recommendation' => __(
'You might want to increase {long_query_time}'
. ' or optimize the queries listed in the slow query log'
),
'justification' => __('The slow query rate should be below 5%%, your value is %s%%.'),
'justification_formula' => 'round(value,2)',
],
[
'id' => 'Slow query rate',
'name' => __('Slow query rate'),
'precondition' => 'Questions > 0',
'formula' => '(Slow_queries / Questions * 100) / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('There is a high percentage of slow queries compared to the server uptime.'),
'recommendation' => __(
'You might want to increase {long_query_time}'
. ' or optimize the queries listed in the slow query log'
),
'justification' => __('You have a slow query rate of %s per hour, you should have less than 1%% per hour.'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Long query time',
'name' => __('Long query time'),
'formula' => 'long_query_time',
'test' => 'value >= 10',
'issue' => __(
'{long_query_time} is set to 10 seconds or more,'
. ' thus only slow queries that take above 10 seconds are logged.'
),
'recommendation' => __(
'It is suggested to set {long_query_time} to a lower value, depending on your environment.'
. ' Usually a value of 1-5 seconds is suggested.'
),
'justification' => __('long_query_time is currently set to %ds.'),
'justification_formula' => 'value',
],
[
'id' => 'Slow query logging',
'name' => __('Slow query logging'),
'precondition' => 'PMA_MYSQL_INT_VERSION < 50600',
'formula' => 'log_slow_queries',
'test' => 'value == \'OFF\'',
'issue' => __('The slow query log is disabled.'),
'recommendation' => __(
'Enable slow query logging by setting {log_slow_queries} to \'ON\'.'
. ' This will help troubleshooting badly performing queries.'
),
'justification' => __('log_slow_queries is set to \'OFF\''),
],
[
'id' => 'Slow query logging',
'name' => __('Slow query logging'),
'precondition' => 'PMA_MYSQL_INT_VERSION >= 50600',
'formula' => 'slow_query_log',
'test' => 'value == \'OFF\'',
'issue' => __('The slow query log is disabled.'),
'recommendation' => __(
'Enable slow query logging by setting {slow_query_log} to \'ON\'.'
. ' This will help troubleshooting badly performing queries.'
),
'justification' => __('slow_query_log is set to \'OFF\''),
],
// Versions
[
'id' => 'Release Series',
'name' => __('Release Series'),
'formula' => 'version',
'test' => 'substr(value,0,2) <= \'5.\' && substr(value,2,1) < 1',
'issue' => __('The MySQL server version less than 5.1.'),
'recommendation' => __(
'You should upgrade, as MySQL 5.1 has improved performance, and MySQL 5.5 even more so.'
),
'justification' => __('Current version: %s'),
'justification_formula' => 'value',
],
[
'id' => 'Minor Version',
'name' => __('Minor Version'),
'precondition' => '! fired(\'Release Series\')',
'formula' => 'version',
'test' => 'substr(value,0,2) <= \'5.\' && substr(value,2,1) <= 1 && substr(value,4,2) < 30',
'issue' => __('Version less than 5.1.30 (the first GA release of 5.1).'),
'recommendation' => __(
'You should upgrade, as recent versions of MySQL 5.1 have improved performance'
. ' and MySQL 5.5 even more so.'
),
'justification' => __('Current version: %s'),
'justification_formula' => 'value',
],
[
'id' => 'Minor Version',
'name' => __('Minor Version'),
'precondition' => '! fired(\'Release Series\')',
'formula' => 'version',
'test' => 'substr(value,0,1) == 5 && substr(value,2,1) == 5 && substr(value,4,2) < 8',
'issue' => __('Version less than 5.5.8 (the first GA release of 5.5).'),
'recommendation' => __('You should upgrade, to a stable version of MySQL 5.5.'),
'justification' => __('Current version: %s'),
'justification_formula' => 'value',
],
[
'id' => 'Distribution',
'name' => __('Distribution'),
'formula' => 'version_comment',
'test' => 'preg_match(\'/source/i\',value)',
'issue' => __('Version is compiled from source, not a MySQL official binary.'),
'recommendation' => __(
'If you did not compile from source, you may be using a package modified by a distribution.'
. ' The MySQL manual only is accurate for official MySQL binaries,'
. ' not any package distributions (such as RedHat, Debian/Ubuntu etc).'
),
'justification' => __('\'source\' found in version_comment'),
],
[
'id' => 'Distribution',
'name' => __('Distribution'),
'formula' => 'version_comment',
'test' => 'preg_match(\'/percona/i\',value)',
'issue' => __('The MySQL manual only is accurate for official MySQL binaries.'),
'recommendation' => __(
'Percona documentation is at <a href="https://www.percona.com/software/documentation/">'
. 'https://www.percona.com/software/documentation/</a>'
),
'justification' => __('\'percona\' found in version_comment'),
],
[
'id' => 'MySQL Architecture',
'name' => __('MySQL Architecture'),
'formula' => 'system_memory',
'test' => 'value > 3072*1024 && !preg_match(\'/64/\',version_compile_machine)'
. ' && !preg_match(\'/64/\',version_compile_os)',
'issue' => __('MySQL is not compiled as a 64-bit package.'),
'recommendation' => __(
'Your memory capacity is above 3 GiB (assuming the Server is on localhost),'
. ' so MySQL might not be able to access all of your memory.'
. ' You might want to consider installing the 64-bit version of MySQL.'
),
'justification' => __('Available memory on this host: %s'),
'justification_formula' => 'ADVISOR_formatByteDown(value*1024, 2, 2)',
],
// Query cache
[
'id' => 'Query caching method',
'name' => __('Query caching method'),
'precondition' => '!fired(\'Query cache disabled\')',
'formula' => 'Questions / Uptime',
'test' => 'value > 100',
'issue' => __('Suboptimal caching method.'),
'recommendation' => __(
'You are using the MySQL Query cache with a fairly high traffic database.'
. ' It might be worth considering to use '
. '<a href="https://dev.mysql.com/doc/refman/5.6/en/ha-memcached.html">memcached</a>'
. ' instead of the MySQL Query cache, especially if you have multiple slaves.'
),
'justification' => __(
'The query cache is enabled and the server receives %d queries per second.'
. ' This rule fires if there is more than 100 queries per second.'
),
'justification_formula' => 'round(value,1)',
],
// Sorts
[
'id' => 'Percentage of sorts that cause temporary tables',
'name' => __('Percentage of sorts that cause temporary tables'),
'precondition' => 'Sort_scan + Sort_range > 0',
'formula' => 'Sort_merge_passes / (Sort_scan + Sort_range) * 100',
'test' => 'value > 10',
'issue' => __('Too many sorts are causing temporary tables.'),
'recommendation' => __(
'Consider increasing {sort_buffer_size} and/or {read_rnd_buffer_size},'
. ' depending on your system memory limits.'
),
'justification' => __('%s%% of all sorts cause temporary tables, this value should be lower than 10%%.'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Rate of sorts that cause temporary tables',
'name' => __('Rate of sorts that cause temporary tables'),
'formula' => 'Sort_merge_passes / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('Too many sorts are causing temporary tables.'),
'recommendation' => __(
'Consider increasing {sort_buffer_size} and/or {read_rnd_buffer_size},'
. ' depending on your system memory limits.'
),
'justification' => __('Temporary tables average: %s, this value should be less than 1 per hour.'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Sort rows',
'name' => __('Sort rows'),
'formula' => 'Sort_rows / Uptime',
'test' => 'value * 60 >= 1',
'issue' => __('There are lots of rows being sorted.'),
'recommendation' => __(
'While there is nothing wrong with a high amount of row sorting, you might want to'
. ' make sure that the queries which require a lot of sorting use indexed columns in'
. ' the ORDER BY clause, as this will result in much faster sorting.'
),
'justification' => __('Sorted rows average: %s'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
// Joins, scans
[
'id' => 'Rate of joins without indexes',
'name' => __('Rate of joins without indexes'),
'formula' => '(Select_range_check + Select_scan + Select_full_join) / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('There are too many joins without indexes.'),
'recommendation' => __(
'This means that joins are doing full table scans. Adding indexes for the columns being'
. ' used in the join conditions will greatly speed up table joins.'
),
'justification' => __('Table joins average: %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Rate of reading first index entry',
'name' => __('Rate of reading first index entry'),
'formula' => 'Handler_read_first / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('The rate of reading the first index entry is high.'),
'recommendation' => __(
'This usually indicates frequent full index scans. Full index scans are faster than'
. ' table scans but require lots of CPU cycles in big tables, if those tables that have or'
. ' had high volumes of UPDATEs and DELETEs, running \'OPTIMIZE TABLE\' might reduce the'
. ' amount of and/or speed up full index scans. Other than that full index scans can'
. ' only be reduced by rewriting queries.'
),
'justification' => __('Index scans average: %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Rate of reading fixed position',
'name' => __('Rate of reading fixed position'),
'formula' => 'Handler_read_rnd / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('The rate of reading data from a fixed position is high.'),
'recommendation' => __(
'This indicates that many queries need to sort results and/or do a full table scan,'
. ' including join queries that do not use indexes. Add indexes where applicable.'
),
'justification' => __('Rate of reading fixed position average: %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Rate of reading next table row',
'name' => __('Rate of reading next table row'),
'formula' => 'Handler_read_rnd_next / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('The rate of reading the next table row is high.'),
'recommendation' => __(
'This indicates that many queries are doing full table scans. Add indexes where applicable.'
),
'justification' => __('Rate of reading next table row: %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
// Temp tables
[
'id' => 'Different tmp_table_size and max_heap_table_size',
'name' => __('Different tmp_table_size and max_heap_table_size'),
'formula' => 'tmp_table_size - max_heap_table_size',
'test' => 'value !=0',
'issue' => __('{tmp_table_size} and {max_heap_table_size} are not the same.'),
'recommendation' => __(
'If you have deliberately changed one of either: The server uses the lower value of either'
. ' to determine the maximum size of in-memory tables. So if you wish to increase the'
. ' in-memory table limit you will have to increase the other value as well.'
),
'justification' => __('Current values are tmp_table_size: %s, max_heap_table_size: %s'),
'justification_formula' => 'ADVISOR_formatByteDown(tmp_table_size, 2, 2),'
. ' ADVISOR_formatByteDown(max_heap_table_size, 2, 2)',
],
[
'id' => 'Percentage of temp tables on disk',
'name' => __('Percentage of temp tables on disk'),
'precondition' => 'Created_tmp_tables + Created_tmp_disk_tables > 0',
'formula' => 'Created_tmp_disk_tables / (Created_tmp_tables + Created_tmp_disk_tables) * 100',
'test' => 'value > 25',
'issue' => __('Many temporary tables are being written to disk instead of being kept in memory.'),
'recommendation' => __(
'Increasing {max_heap_table_size} and {tmp_table_size} might help. However some'
. ' temporary tables are always being written to disk, independent of the value of these variables.'
. ' To eliminate these you will have to rewrite your queries to avoid those conditions'
. ' (Within a temporary table: Presence of a BLOB or TEXT column or presence of a column'
. ' bigger than 512 bytes) as mentioned in the beginning of an <a href="'
. 'https://www.facebook.com/note.php?note_id=10150111255065841&comments'
. '">Article by the Pythian Group</a>'
),
'justification' => __(
'%s%% of all temporary tables are being written to disk, this value should be below 25%%'
),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Temp disk rate',
'name' => __('Temp disk rate'),
'precondition' => '!fired(\'Percentage of temp tables on disk\')',
'formula' => 'Created_tmp_disk_tables / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('Many temporary tables are being written to disk instead of being kept in memory.'),
'recommendation' => __(
'Increasing {max_heap_table_size} and {tmp_table_size} might help. However some'
. ' temporary tables are always being written to disk, independent of the value of these variables.'
. ' To eliminate these you will have to rewrite your queries to avoid those conditions'
. ' (Within a temporary table: Presence of a BLOB or TEXT column or presence of a column'
. ' bigger than 512 bytes) as mentioned in the <a href="'
. 'https://dev.mysql.com/doc/refman/8.0/en/internal-temporary-tables.html'
. '">MySQL Documentation</a>'
),
'justification' => __(
'Rate of temporary tables being written to disk: %s, this value should be less than 1 per hour'
),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
// MyISAM index cache
[
'id' => 'MyISAM key buffer size',
'name' => __('MyISAM key buffer size'),
'formula' => 'key_buffer_size',
'test' => 'value == 0',
'issue' => __('Key buffer is not initialized. No MyISAM indexes will be cached.'),
'recommendation' => __(
'Set {key_buffer_size} depending on the size of your MyISAM indexes. 64M is a good start.'
),
'justification' => __('key_buffer_size is 0'),
],
[
'id' => 'Max % MyISAM key buffer ever used',
/* xgettext:no-php-format */
'name' => __('Max % MyISAM key buffer ever used'),
'precondition' => 'key_buffer_size > 0',
'formula' => 'Key_blocks_used * key_cache_block_size / key_buffer_size * 100',
'test' => 'value < 95',
/* xgettext:no-php-format */
'issue' => __('MyISAM key buffer (index cache) % used is low.'),
'recommendation' => __(
'You may need to decrease the size of {key_buffer_size}, re-examine your tables to see'
. ' if indexes have been removed, or examine queries and expectations'
. ' about what indexes are being used.'
),
'justification' => __('max %% MyISAM key buffer ever used: %s%%, this value should be above 95%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Percentage of MyISAM key buffer used',
'name' => __('Percentage of MyISAM key buffer used'),
// Don't fire if above rule fired - we don't need the same advice twice
'precondition' => 'key_buffer_size > 0 && !fired(\'Max % MyISAM key buffer ever used\')',
'formula' => '( 1 - Key_blocks_unused * key_cache_block_size / key_buffer_size) * 100',
'test' => 'value < 95',
/* xgettext:no-php-format */
'issue' => __('MyISAM key buffer (index cache) % used is low.'),
'recommendation' => __(
'You may need to decrease the size of {key_buffer_size}, re-examine your tables to see'
. ' if indexes have been removed, or examine queries and expectations'
. ' about what indexes are being used.'
),
'justification' => __('%% MyISAM key buffer used: %s%%, this value should be above 95%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Percentage of index reads from memory',
'name' => __('Percentage of index reads from memory'),
'precondition' => 'Key_read_requests > 0',
'formula' => '100 - (Key_reads / Key_read_requests * 100)',
'test' => 'value < 95',
/* xgettext:no-php-format */
'issue' => __('The % of indexes that use the MyISAM key buffer is low.'),
'recommendation' => __('You may need to increase {key_buffer_size}.'),
'justification' => __('Index reads from memory: %s%%, this value should be above 95%%'),
'justification_formula' => 'round(value,1)',
],
// Other caches
[
'id' => 'Rate of table open',
'name' => __('Rate of table open'),
'formula' => 'Opened_tables / Uptime',
'test' => 'value*60*60 > 10',
'issue' => __('The rate of opening tables is high.'),
'recommendation' => __(
'Opening tables requires disk I/O which is costly.'
. ' Increasing {table_open_cache} might avoid this.'
),
'justification' => __('Opened table rate: %s, this value should be less than 10 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Percentage of used open files limit',
'name' => __('Percentage of used open files limit'),
'formula' => 'Open_files / open_files_limit * 100',
'test' => 'value > 85',
'issue' => __(
'The number of open files is approaching the max number of open files.'
. ' You may get a "Too many open files" error.'
),
'recommendation' => __(
'Consider increasing {open_files_limit}, and check the error log when'
. ' restarting after changing {open_files_limit}.'
),
'justification' => __('The number of opened files is at %s%% of the limit. It should be below 85%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Rate of open files',
'name' => __('Rate of open files'),
'formula' => 'Open_files / Uptime',
'test' => 'value * 60 * 60 > 5',
'issue' => __('The rate of opening files is high.'),
'recommendation' => __(
'Consider increasing {open_files_limit}, and check the error log when'
. ' restarting after changing {open_files_limit}.'
),
'justification' => __('Opened files rate: %s, this value should be less than 5 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Immediate table locks %',
/* xgettext:no-php-format */
'name' => __('Immediate table locks %'),
'precondition' => 'Table_locks_waited + Table_locks_immediate > 0',
'formula' => 'Table_locks_immediate / (Table_locks_waited + Table_locks_immediate) * 100',
'test' => 'value < 95',
'issue' => __('Too many table locks were not granted immediately.'),
'recommendation' => __('Optimize queries and/or use InnoDB to reduce lock wait.'),
'justification' => __('Immediate table locks: %s%%, this value should be above 95%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Table lock wait rate',
'name' => __('Table lock wait rate'),
'formula' => 'Table_locks_waited / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('Too many table locks were not granted immediately.'),
'recommendation' => __('Optimize queries and/or use InnoDB to reduce lock wait.'),
'justification' => __('Table lock wait rate: %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Thread cache',
'name' => __('Thread cache'),
'formula' => 'thread_cache_size',
'test' => 'value < 1',
'issue' => __('Thread cache is disabled, resulting in more overhead from new connections to MySQL.'),
'recommendation' => __('Enable the thread cache by setting {thread_cache_size} > 0.'),
'justification' => __('The thread cache is set to 0'),
],
[
'id' => 'Thread cache hit rate %',
/* xgettext:no-php-format */
'name' => __('Thread cache hit rate %'),
'precondition' => 'thread_cache_size > 0',
'formula' => '100 - Threads_created / Connections',
'test' => 'value < 80',
'issue' => __('Thread cache is not efficient.'),
'recommendation' => __('Increase {thread_cache_size}.'),
'justification' => __('Thread cache hitrate: %s%%, this value should be above 80%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Threads that are slow to launch',
'name' => __('Threads that are slow to launch'),
'precondition' => 'slow_launch_time > 0',
'formula' => 'Slow_launch_threads',
'test' => 'value > 0',
'issue' => __('There are too many threads that are slow to launch.'),
'recommendation' => __(
'This generally happens in case of general system overload as it is pretty simple'
. ' operations. You might want to monitor your system load carefully.'
),
'justification' => __('%s thread(s) took longer than %s seconds to start, it should be 0'),
'justification_formula' => 'value, slow_launch_time',
],
[
'id' => 'Slow launch time',
'name' => __('Slow launch time'),
'formula' => 'slow_launch_time',
'test' => 'value > 2',
'issue' => __('Slow_launch_time is above 2s.'),
'recommendation' => __(
'Set {slow_launch_time} to 1s or 2s to correctly count threads that are slow to launch.'
),
'justification' => __('slow_launch_time is set to %s'),
'justification_formula' => 'value',
],
// Connections
[
'id' => 'Percentage of used connections',
'name' => __('Percentage of used connections'),
'formula' => 'Max_used_connections / max_connections * 100',
'test' => 'value > 80',
'issue' => __(
'The maximum amount of used connections is getting close to the value of {max_connections}.'
),
'recommendation' => __(
'Increase {max_connections}, or decrease {wait_timeout} so that connections that do not'
. ' close database handlers properly get killed sooner.'
. ' Make sure the code closes database handlers properly.'
),
'justification' => __('Max_used_connections is at %s%% of max_connections, it should be below 80%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Percentage of aborted connections',
'name' => __('Percentage of aborted connections'),
'formula' => 'Aborted_connects / Connections * 100',
'test' => 'value > 1',
'issue' => __('Too many connections are aborted.'),
'recommendation' => __(
'Connections are usually aborted when they cannot be authorized. <a href="'
. 'https://www.percona.com/blog/2008/08/23/how-to-track-down-the-source-of-aborted_connects/'
. '">This article</a> might help you track down the source.'
),
'justification' => __('%s%% of all connections are aborted. This value should be below 1%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Rate of aborted connections',
'name' => __('Rate of aborted connections'),
'formula' => 'Aborted_connects / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('Too many connections are aborted.'),
'recommendation' => __(
'Connections are usually aborted when they cannot be authorized. <a href="'
. 'https://www.percona.com/blog/2008/08/23/how-to-track-down-the-source-of-aborted_connects/'
. '">This article</a> might help you track down the source.'
),
'justification' => __('Aborted connections rate is at %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
[
'id' => 'Percentage of aborted clients',
'name' => __('Percentage of aborted clients'),
'formula' => 'Aborted_clients / Connections * 100',
'test' => 'value > 2',
'issue' => __('Too many clients are aborted.'),
'recommendation' => __(
'Clients are usually aborted when they did not close their connection to MySQL properly.'
. ' This can be due to network issues or code not closing a database handler properly.'
. ' Check your network and code.'
),
'justification' => __('%s%% of all clients are aborted. This value should be below 2%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Rate of aborted clients',
'name' => __('Rate of aborted clients'),
'formula' => 'Aborted_clients / Uptime',
'test' => 'value * 60 * 60 > 1',
'issue' => __('Too many clients are aborted.'),
'recommendation' => __(
'Clients are usually aborted when they did not close their connection to MySQL properly.'
. ' This can be due to network issues or code not closing a database handler properly.'
. ' Check your network and code.'
),
'justification' => __('Aborted client rate is at %s, this value should be less than 1 per hour'),
'justification_formula' => 'ADVISOR_bytime(value,2)',
],
// InnoDB
[
'id' => 'Is InnoDB disabled?',
'name' => __('Is InnoDB disabled?'),
'precondition' => 'PMA_MYSQL_INT_VERSION < 50600',
'formula' => 'have_innodb',
'test' => 'value != "YES"',
'issue' => __('You do not have InnoDB enabled.'),
'recommendation' => __('InnoDB is usually the better choice for table engines.'),
'justification' => __('have_innodb is set to \'value\''),
],
[
'id' => 'InnoDB log size',
'name' => __('InnoDB log size'),
'precondition' => 'innodb_buffer_pool_size > 0',
'formula' => '(innodb_log_file_size * innodb_log_files_in_group)/ innodb_buffer_pool_size * 100',
'test' => 'value < 20 && innodb_log_file_size / (1024 * 1024) < 256',
'issue' => __(
'The InnoDB log file size is not an appropriate size, in relation to the InnoDB buffer pool.'
),
'recommendation' => __(/* xgettext:no-php-format */
'Especially on a system with a lot of writes to InnoDB tables you should set'
. ' {innodb_log_file_size} to 25% of {innodb_buffer_pool_size}. However the bigger this value,'
. ' the longer the recovery time will be when database crashes, so this value should not be set'
. ' much higher than 256 MiB. Please note however that you cannot simply change the value of'
. ' this variable. You need to shutdown the server, remove the InnoDB log files, set the new'
. ' value in my.cnf, start the server, then check the error logs if everything went fine.'
. ' See also <a href="'
. 'https://mysqldatabaseadministration.blogspot.com/2007/01/increase-innodblogfilesize-proper-way.html'
. '">this blog entry</a>'
),
'justification' => __(
'Your InnoDB log size is at %s%% in relation to the InnoDB buffer pool size,'
. ' it should not be below 20%%'
),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Max InnoDB log size',
'name' => __('Max InnoDB log size'),
'precondition' => 'innodb_buffer_pool_size > 0 && innodb_log_file_size / innodb_buffer_pool_size * 100 < 30',
'formula' => 'innodb_log_file_size / (1024 * 1024)',
'test' => 'value > 256',
'issue' => __('The InnoDB log file size is inadequately large.'),
'recommendation' => __(/* xgettext:no-php-format */
'It is usually sufficient to set {innodb_log_file_size} to 25% of the size of'
. ' {innodb_buffer_pool_size}. A very big {innodb_log_file_size} slows down the recovery'
. ' time after a database crash considerably. See also '
. '<a href="https://www.percona.com/blog/2006/07/03/choosing-proper-innodb_log_file_size/">'
. 'this Article</a>. You need to shutdown the server, remove the InnoDB log files, set the'
. ' new value in my.cnf, start the server, then check the error logs'
. ' if everything went fine. See also <a href="'
. 'https://mysqldatabaseadministration.blogspot.com/2007/01/increase-innodblogfilesize-proper-way.html'
. '">this blog entry</a>'
),
'justification' => __('Your absolute InnoDB log size is %s MiB'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'InnoDB buffer pool size',
'name' => __('InnoDB buffer pool size'),
'precondition' => 'system_memory > 0',
'formula' => 'innodb_buffer_pool_size / system_memory * 100',
'test' => 'value < 60',
'issue' => __('Your InnoDB buffer pool is fairly small.'),
'recommendation' => __(/* xgettext:no-php-format */
'The InnoDB buffer pool has a profound impact on performance for InnoDB tables.'
. ' Assign all your remaining memory to this buffer. For database servers that use solely InnoDB'
. ' as storage engine and have no other services (e.g. a web server) running, you may set this'
. ' as high as 80% of your available memory. If that is not the case, you need to carefully'
. ' assess the memory consumption of your other services and non-InnoDB-Tables and set this'
. ' variable accordingly. If it is set too high, your system will start swapping,'
. ' which decreases performance significantly. See also '
. '<a href="https://www.percona.com/blog/2007/11/03/choosing-innodb_buffer_pool_size/">this article</a>'
),
'justification' => __(
'You are currently using %s%% of your memory for the InnoDB buffer pool.'
. ' This rule fires if you are assigning less than 60%%, however this might be perfectly'
. ' adequate for your system if you don\'t have much InnoDB tables'
. ' or other services running on the same machine.'
),
'justification_formula' => 'value',
],
// Other
[
'id' => 'MyISAM concurrent inserts',
'name' => __('MyISAM concurrent inserts'),
'formula' => 'concurrent_insert',
'test' => 'value === 0 || value === \'NEVER\'',
'issue' => __('Enable {concurrent_insert} by setting it to 1'),
'recommendation' => __(
'Setting {concurrent_insert} to 1 reduces contention between'
. ' readers and writers for a given table. See also '
. '<a href="https://dev.mysql.com/doc/refman/5.5/en/concurrent-inserts.html">MySQL Documentation</a>'
),
'justification' => __('concurrent_insert is set to 0'),
],
];

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
return [
// Query cache
[
'id' => 'Query cache disabled',
'name' => __('Query cache disabled'),
'formula' => 'query_cache_size',
'test' => 'value == 0 || query_cache_type == \'OFF\' || query_cache_type == \'0\'',
'issue' => __('The query cache is not enabled.'),
'recommendation' => __(
'The query cache is known to greatly improve performance if configured correctly. Enable it by'
. ' setting {query_cache_size} to a 2 digit MiB value and setting {query_cache_type} to \'ON\'.'
. ' <b>Note:</b> If you are using memcached, ignore this recommendation.'
),
'justification' => __('query_cache_size is set to 0 or query_cache_type is set to \'OFF\''),
],
[
'id' => 'Query cache efficiency (%)',
/* xgettext:no-php-format */
'name' => __('Query cache efficiency (%)'),
'precondition' => 'Com_select + Qcache_hits > 0 && !fired(\'Query cache disabled\')',
'formula' => 'Qcache_hits / (Com_select + Qcache_hits) * 100',
'test' => 'value < 20',
'issue' => __('Query cache not running efficiently, it has a low hit rate.'),
'recommendation' => __('Consider increasing {query_cache_limit}.'),
'justification' => __('The current query cache hit rate of %s%% is below 20%%'),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Query Cache usage',
'name' => __('Query Cache usage'),
'precondition' => '!fired(\'Query cache disabled\')',
'formula' => '100 - Qcache_free_memory / query_cache_size * 100',
'test' => 'value < 80',
/* xgettext:no-php-format */
'issue' => __('Less than 80% of the query cache is being utilized.'),
'recommendation' => __(
'This might be caused by {query_cache_limit} being too low.'
. ' Flushing the query cache might help as well.'
),
'justification' => __(
'The current ratio of free query cache memory to total query'
. ' cache size is %s%%. It should be above 80%%'
),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Query cache fragmentation',
'name' => __('Query cache fragmentation'),
'precondition' => '!fired(\'Query cache disabled\')',
'formula' => 'Qcache_free_blocks / (Qcache_total_blocks / 2) * 100',
'test' => 'value > 20',
'issue' => __('The query cache is considerably fragmented.'),
'recommendation' => __(
'Severe fragmentation is likely to (further) increase Qcache_lowmem_prunes. This might be'
. ' caused by many Query cache low memory prunes due to {query_cache_size} being too small. For a'
. ' immediate but short lived fix you can flush the query cache (might lock the query cache for a'
. ' long time). Carefully adjusting {query_cache_min_res_unit} to a lower value might help too,'
. ' e.g. you can set it to the average size of your queries in the cache using this formula:'
. ' (query_cache_size - qcache_free_memory) / qcache_queries_in_cache'
),
'justification' => __(
'The cache is currently fragmented by %s%% , with 100%% fragmentation meaning that the query'
. ' cache is an alternating pattern of free and used blocks. This value should be below 20%%.'
),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Query cache low memory prunes',
'name' => __('Query cache low memory prunes'),
'precondition' => 'Qcache_inserts > 0 && !fired(\'Query cache disabled\')',
'formula' => 'Qcache_lowmem_prunes / Qcache_inserts * 100',
'test' => 'value > 0.1',
'issue' => __('Cached queries are removed due to low query cache memory from the query cache.'),
'recommendation' => __(
'You might want to increase {query_cache_size}, however keep in mind that the overhead of'
. ' maintaining the cache is likely to increase with its size, so do this in small increments'
. ' and monitor the results.'
),
'justification' => __(
'The ratio of removed queries to inserted queries is %s%%. The lower this value is,'
. ' the better (This rules firing limit: 0.1%%)'
),
'justification_formula' => 'round(value,1)',
],
[
'id' => 'Query cache max size',
'name' => __('Query cache max size'),
'precondition' => '!fired(\'Query cache disabled\')',
'formula' => 'query_cache_size',
'test' => 'value > 1024 * 1024 * 128',
'issue' => __(
'The query cache size is above 128 MiB. Big query caches may cause significant'
. ' overhead that is required to maintain the cache.'
),
'recommendation' => __(
'Depending on your environment, it might be performance increasing to reduce this value.'
),
'justification' => __('Current query cache size: %s'),
'justification_formula' => 'ADVISOR_formatByteDown(value, 2, 2)',
],
[
'id' => 'Query cache min result size',
'name' => __('Query cache min result size'),
'precondition' => '!fired(\'Query cache disabled\')',
'formula' => 'query_cache_limit',
'test' => 'value == 1024*1024',
'issue' => __('The max size of the result set in the query cache is the default of 1 MiB.'),
'recommendation' => __(
'Changing {query_cache_limit} (usually by increasing) may increase efficiency. This variable'
. ' determines the maximum size a query result may have to be inserted into the query cache.'
. ' If there are many query results above 1 MiB that are well cacheable (many reads, little writes)'
. ' then increasing {query_cache_limit} will increase efficiency. Whereas in the case of many query'
. ' results being above 1 MiB that are not very well cacheable (often invalidated due to table'
. ' updates) increasing {query_cache_limit} might reduce efficiency.'
),
'justification' => __('query_cache_limit is set to 1 MiB'),
],
];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----

View file

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----

View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View file

@ -0,0 +1,16 @@
phpMyAdmin SSL certificates
===========================
This directory contains copy of root certificates used to sign phpmyadmin.net
and reports.phpmyadmin.net websites. It is used to allow operation on systems
where the certificates are missing or wrongly configured (happens on Windows
with wrongly compiled CURL).
Currently included SSL certificates:
* ISRG Root X1
* DST Root CA X3
See https://letsencrypt.org/certificates/ for more info on them.
In case of update, the filenames can be generated using c_rehash tool.

View file

@ -0,0 +1,51 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----

View file

@ -0,0 +1,431 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Server\SysInfo\SysInfo;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Throwable;
use function array_merge;
use function htmlspecialchars;
use function implode;
use function pow;
use function preg_match;
use function preg_replace_callback;
use function round;
use function sprintf;
use function strpos;
use function substr;
use function vsprintf;
/**
* A simple rules engine, that executes the rules in the advisory_rules files.
*/
class Advisor
{
private const GENERIC_RULES_FILE = 'libraries/advisory_rules_generic.php';
private const BEFORE_MYSQL80003_RULES_FILE = 'libraries/advisory_rules_mysql_before80003.php';
/** @var DatabaseInterface */
private $dbi;
/** @var array */
private $variables;
/** @var array */
private $globals;
/** @var array */
private $rules;
/** @var array */
private $runResult;
/** @var ExpressionLanguage */
private $expression;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param ExpressionLanguage $expression ExpressionLanguage object
*/
public function __construct(DatabaseInterface $dbi, ExpressionLanguage $expression)
{
$this->dbi = $dbi;
$this->expression = $expression;
/*
* Register functions for ExpressionLanguage, we intentionally
* do not implement support for compile as we do not use it.
*/
$this->expression->register(
'round',
static function () {
},
/**
* @param array $arguments
* @param float $num
*/
static function ($arguments, $num) {
return round($num);
}
);
$this->expression->register(
'substr',
static function () {
},
/**
* @param array $arguments
* @param string $string
* @param int $start
* @param int $length
*/
static function ($arguments, $string, $start, $length) {
return substr($string, $start, $length);
}
);
$this->expression->register(
'preg_match',
static function () {
},
/**
* @param array $arguments
* @param string $pattern
* @param string $subject
*/
static function ($arguments, $pattern, $subject) {
return preg_match($pattern, $subject);
}
);
$this->expression->register(
'ADVISOR_bytime',
static function () {
},
/**
* @param array $arguments
* @param float $num
* @param int $precision
*/
static function ($arguments, $num, $precision) {
return self::byTime($num, $precision);
}
);
$this->expression->register(
'ADVISOR_timespanFormat',
static function () {
},
/**
* @param array $arguments
* @param string $seconds
*/
static function ($arguments, $seconds) {
return Util::timespanFormat((int) $seconds);
}
);
$this->expression->register(
'ADVISOR_formatByteDown',
static function () {
},
/**
* @param array $arguments
* @param int $value
* @param int $limes
* @param int $comma
*/
static function ($arguments, $value, $limes = 6, $comma = 0) {
return implode(' ', (array) Util::formatByteDown($value, $limes, $comma));
}
);
$this->expression->register(
'fired',
static function () {
},
/**
* @param array $arguments
* @param int $value
*/
function ($arguments, $value) {
if (! isset($this->runResult['fired'])) {
return 0;
}
// Did matching rule fire?
foreach ($this->runResult['fired'] as $rule) {
if ($rule['id'] == $value) {
return '1';
}
}
return '0';
}
);
/* Some global variables for advisor */
$this->globals = [
'PMA_MYSQL_INT_VERSION' => $this->dbi->getVersion(),
];
}
private function setVariables(): void
{
$globalStatus = $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1);
$globalVariables = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1);
$sysInfo = SysInfo::get();
$memory = $sysInfo->memory();
$systemMemory = ['system_memory' => $memory['MemTotal'] ?? 0];
$this->variables = array_merge($globalStatus, $globalVariables, $systemMemory);
}
/**
* @param string|int $variable Variable to set
* @param mixed $value Value to set
*/
public function setVariable($variable, $value): void
{
$this->variables[$variable] = $value;
}
private function setRules(): void
{
$isMariaDB = strpos($this->variables['version'], 'MariaDB') !== false;
$genericRules = include ROOT_PATH . self::GENERIC_RULES_FILE;
if (! $isMariaDB && $this->globals['PMA_MYSQL_INT_VERSION'] >= 80003) {
$this->rules = $genericRules;
return;
}
$extraRules = include ROOT_PATH . self::BEFORE_MYSQL80003_RULES_FILE;
$this->rules = array_merge($genericRules, $extraRules);
}
/**
* @return array
*/
public function getRunResult(): array
{
return $this->runResult;
}
/**
* @return array
*/
public function run(): array
{
$this->setVariables();
$this->setRules();
$this->runRules();
return $this->runResult;
}
/**
* Stores current error in run results.
*
* @param string $description description of an error.
* @param Throwable $exception exception raised
*/
private function storeError(string $description, Throwable $exception): void
{
$this->runResult['errors'][] = $description . ' ' . sprintf(
__('Error when evaluating: %s'),
$exception->getMessage()
);
}
/**
* Executes advisor rules
*/
private function runRules(): void
{
$this->runResult = [
'fired' => [],
'notfired' => [],
'unchecked' => [],
'errors' => [],
];
foreach ($this->rules as $rule) {
$this->variables['value'] = 0;
$precondition = true;
if (isset($rule['precondition'])) {
try {
$precondition = $this->evaluateRuleExpression($rule['precondition']);
} catch (Throwable $e) {
$this->storeError(
sprintf(
__('Failed evaluating precondition for rule \'%s\'.'),
$rule['name']
),
$e
);
continue;
}
}
if (! $precondition) {
$this->addRule('unchecked', $rule);
continue;
}
try {
$value = $this->evaluateRuleExpression($rule['formula']);
} catch (Throwable $e) {
$this->storeError(
sprintf(
__('Failed calculating value for rule \'%s\'.'),
$rule['name']
),
$e
);
continue;
}
$this->variables['value'] = $value;
try {
if ($this->evaluateRuleExpression($rule['test'])) {
$this->addRule('fired', $rule);
} else {
$this->addRule('notfired', $rule);
}
} catch (Throwable $e) {
$this->storeError(
sprintf(
__('Failed running test for rule \'%s\'.'),
$rule['name']
),
$e
);
}
}
}
/**
* Adds a rule to the result list
*
* @param string $type type of rule
* @param array $rule rule itself
*/
public function addRule(string $type, array $rule): void
{
if ($type !== 'notfired' && $type !== 'fired') {
$this->runResult[$type][] = $rule;
return;
}
if (isset($rule['justification_formula'])) {
try {
$params = $this->evaluateRuleExpression('[' . $rule['justification_formula'] . ']');
} catch (Throwable $e) {
$this->storeError(
sprintf(__('Failed formatting string for rule \'%s\'.'), $rule['name']),
$e
);
return;
}
$rule['justification'] = vsprintf($rule['justification'], $params);
}
// Replaces {server_variable} with 'server_variable'
// linking to /server/variables
$rule['recommendation'] = preg_replace_callback(
'/\{([a-z_0-9]+)\}/Ui',
function (array $matches) {
return $this->replaceVariable($matches);
},
$rule['recommendation']
);
$rule['issue'] = preg_replace_callback(
'/\{([a-z_0-9]+)\}/Ui',
function (array $matches) {
return $this->replaceVariable($matches);
},
$rule['issue']
);
// Replaces external Links with Core::linkURL() generated links
$rule['recommendation'] = preg_replace_callback(
'#href=("|\')(https?://[^"\']+)\1#i',
function (array $matches) {
return $this->replaceLinkURL($matches);
},
$rule['recommendation']
);
$this->runResult[$type][] = $rule;
}
/**
* Callback for wrapping links with Core::linkURL
*
* @param array $matches List of matched elements form preg_replace_callback
*
* @return string Replacement value
*/
private function replaceLinkURL(array $matches): string
{
return 'href="' . Core::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"';
}
/**
* Callback for wrapping variable edit links
*
* @param array $matches List of matched elements form preg_replace_callback
*
* @return string Replacement value
*/
private function replaceVariable(array $matches): string
{
return '<a href="' . Url::getFromRoute('/server/variables', ['filter' => $matches[1]])
. '">' . htmlspecialchars($matches[1]) . '</a>';
}
/**
* Runs a code expression, replacing variable names with their respective values
*
* @return mixed result of evaluated expression
*/
private function evaluateRuleExpression(string $expression)
{
return $this->expression->evaluate($expression, array_merge($this->variables, $this->globals));
}
/**
* Formats interval like 10 per hour
*
* @param float $num number to format
* @param int $precision required precision
*
* @return string formatted string
*/
public static function byTime(float $num, int $precision): string
{
if ($num >= 1) { // per second
$per = __('per second');
} elseif ($num * 60 >= 1) { // per minute
$num *= 60;
$per = __('per minute');
} elseif ($num * 60 * 60 >= 1) { // per hour
$num *= 60 * 60;
$per = __('per hour');
} else {
$num *= 24 * 60 * 60;
$per = __('per day');
}
$num = round($num, $precision);
if ($num == 0) {
$num = '<' . pow(10, -$precision);
}
return $num . ' ' . $per;
}
}

View file

@ -0,0 +1,396 @@
<?php
/**
* Handles bookmarking SQL queries
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use const PREG_SET_ORDER;
use function count;
use function is_array;
use function preg_match_all;
use function preg_replace;
use function str_replace;
use function strlen;
/**
* Handles bookmarking SQL queries
*/
class Bookmark
{
/**
* ID of the bookmark
*
* @var int
*/
private $id;
/**
* Database the bookmark belongs to
*
* @var string
*/
private $database;
/**
* The user to whom the bookmark belongs, empty for public bookmarks
*
* @var string
*/
private $currentUser;
/**
* Label of the bookmark
*
* @var string
*/
private $label;
/**
* SQL query that is bookmarked
*
* @var string
*/
private $query;
/** @var DatabaseInterface */
private $dbi;
/**
* Current user
*
* @var string
*/
private $user;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
*/
public function __construct(DatabaseInterface $dbi, string $user)
{
$this->dbi = $dbi;
$this->user = $user;
}
/**
* Returns the ID of the bookmark
*/
public function getId(): int
{
return (int) $this->id;
}
/**
* Returns the database of the bookmark
*/
public function getDatabase(): string
{
return $this->database;
}
/**
* Returns the user whom the bookmark belongs to
*/
public function getUser(): string
{
return $this->currentUser;
}
/**
* Returns the label of the bookmark
*/
public function getLabel(): string
{
return $this->label;
}
/**
* Returns the query
*/
public function getQuery(): string
{
return $this->query;
}
/**
* Adds a bookmark
*
* @return bool whether the INSERT succeeds or not
*
* @access public
*/
public function save(): bool
{
$cfgBookmark = self::getParams($this->user);
if (! is_array($cfgBookmark)) {
return false;
}
$query = 'INSERT INTO ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. ' (id, dbase, user, query, label) VALUES (NULL, '
. "'" . $this->dbi->escapeString($this->database) . "', "
. "'" . $this->dbi->escapeString($this->currentUser) . "', "
. "'" . $this->dbi->escapeString($this->query) . "', "
. "'" . $this->dbi->escapeString($this->label) . "')";
return $this->dbi->query($query, DatabaseInterface::CONNECT_CONTROL);
}
/**
* Deletes a bookmark
*
* @return bool true if successful
*
* @access public
*/
public function delete(): bool
{
$cfgBookmark = self::getParams($this->user);
if (! is_array($cfgBookmark)) {
return false;
}
$query = 'DELETE FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. ' WHERE id = ' . $this->id;
return $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL);
}
/**
* Returns the number of variables in a bookmark
*
* @return int number of variables
*/
public function getVariableCount(): int
{
$matches = [];
preg_match_all('/\[VARIABLE[0-9]*\]/', $this->query, $matches, PREG_SET_ORDER);
return count($matches);
}
/**
* Replace the placeholders in the bookmark query with variables
*
* @param array $variables array of variables
*
* @return string query with variables applied
*/
public function applyVariables(array $variables): string
{
// remove comments that encloses a variable placeholder
$query = (string) preg_replace(
'|/\*(.*\[VARIABLE[0-9]*\].*)\*/|imsU',
'${1}',
$this->query
);
// replace variable placeholders with values
$number_of_variables = $this->getVariableCount();
for ($i = 1; $i <= $number_of_variables; $i++) {
$var = '';
if (! empty($variables[$i])) {
$var = $this->dbi->escapeString($variables[$i]);
}
$query = str_replace('[VARIABLE' . $i . ']', $var, $query);
// backward compatibility
if ($i != 1) {
continue;
}
$query = str_replace('[VARIABLE]', $var, $query);
}
return $query;
}
/**
* Defines the bookmark parameters for the current user
*
* @param string $user Current user
*
* @return array|bool the bookmark parameters for the current user
*
* @access public
*/
public static function getParams(string $user)
{
global $dbi;
static $cfgBookmark = null;
if ($cfgBookmark !== null) {
return $cfgBookmark;
}
$relation = new Relation($dbi);
$cfgRelation = $relation->getRelationsParam();
if ($cfgRelation['bookmarkwork']) {
$cfgBookmark = [
'user' => $user,
'db' => $cfgRelation['db'],
'table' => $cfgRelation['bookmark'],
];
} else {
$cfgBookmark = false;
}
return $cfgBookmark;
}
/**
* Creates a Bookmark object from the parameters
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param array $bkm_fields the properties of the bookmark to add; here,
* $bkm_fields['bkm_sql_query'] is urlencoded
* @param bool $all_users whether to make the bookmark
* available for all users
*
* @return Bookmark|false
*/
public static function createBookmark(
DatabaseInterface $dbi,
string $user,
array $bkm_fields,
bool $all_users = false
) {
if (! (isset($bkm_fields['bkm_sql_query'], $bkm_fields['bkm_label'])
&& strlen($bkm_fields['bkm_sql_query']) > 0
&& strlen($bkm_fields['bkm_label']) > 0)
) {
return false;
}
$bookmark = new Bookmark($dbi, $user);
$bookmark->database = $bkm_fields['bkm_database'];
$bookmark->label = $bkm_fields['bkm_label'];
$bookmark->query = $bkm_fields['bkm_sql_query'];
$bookmark->currentUser = $all_users ? '' : $bkm_fields['bkm_user'];
return $bookmark;
}
/**
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param array $row Resource used to build the bookmark
*/
protected static function createFromRow(
DatabaseInterface $dbi,
string $user,
$row
): Bookmark {
$bookmark = new Bookmark($dbi, $user);
$bookmark->id = $row['id'];
$bookmark->database = $row['dbase'];
$bookmark->currentUser = $row['user'];
$bookmark->label = $row['label'];
$bookmark->query = $row['query'];
return $bookmark;
}
/**
* Gets the list of bookmarks defined for the current database
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param string|false $db the current database name or false
*
* @return Bookmark[] the bookmarks list
*
* @access public
*/
public static function getList(
DatabaseInterface $dbi,
string $user,
$db = false
): array {
$cfgBookmark = self::getParams($user);
if (! is_array($cfgBookmark)) {
return [];
}
$query = 'SELECT * FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. " WHERE ( `user` = ''"
. " OR `user` = '" . $dbi->escapeString($cfgBookmark['user']) . "' )";
if ($db !== false) {
$query .= " AND dbase = '" . $dbi->escapeString($db) . "'";
}
$query .= ' ORDER BY label ASC';
$result = $dbi->fetchResult(
$query,
null,
null,
DatabaseInterface::CONNECT_CONTROL,
DatabaseInterface::QUERY_STORE
);
if (! empty($result)) {
$bookmarks = [];
foreach ($result as $row) {
$bookmarks[] = self::createFromRow($dbi, $user, $row);
}
return $bookmarks;
}
return [];
}
/**
* Retrieve a specific bookmark
*
* @param DatabaseInterface $dbi DatabaseInterface object
* @param string $user Current user
* @param string $db the current database name
* @param int|string $id an identifier of the bookmark to get
* @param string $id_field which field to look up the identifier
* @param bool $action_bookmark_all true: get all bookmarks regardless
* of the owning user
* @param bool $exact_user_match whether to ignore bookmarks with no user
*
* @return Bookmark|null the bookmark
*
* @access public
*/
public static function get(
DatabaseInterface $dbi,
string $user,
string $db,
$id,
string $id_field = 'id',
bool $action_bookmark_all = false,
bool $exact_user_match = false
): ?self {
$cfgBookmark = self::getParams($user);
if (! is_array($cfgBookmark)) {
return null;
}
$query = 'SELECT * FROM ' . Util::backquote($cfgBookmark['db'])
. '.' . Util::backquote($cfgBookmark['table'])
. " WHERE dbase = '" . $dbi->escapeString($db) . "'";
if (! $action_bookmark_all) {
$query .= " AND (user = '"
. $dbi->escapeString($cfgBookmark['user']) . "'";
if (! $exact_user_match) {
$query .= " OR user = ''";
}
$query .= ')';
}
$query .= ' AND ' . Util::backquote($id_field)
. " = '" . $dbi->escapeString((string) $id) . "' LIMIT 1";
$result = $dbi->fetchSingleRow($query, 'ASSOC', DatabaseInterface::CONNECT_CONTROL);
if (! empty($result)) {
return self::createFromRow($dbi, $user, $result);
}
return null;
}
}

View file

@ -0,0 +1,352 @@
<?php
/**
* Contains functions used by browse foreigners
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use function asort;
use function ceil;
use function floor;
use function htmlspecialchars;
use function is_array;
use function mb_strlen;
use function mb_substr;
/**
* PhpMyAdmin\BrowseForeigners class
*/
class BrowseForeigners
{
/** @var int */
private $limitChars;
/** @var int */
private $maxRows;
/** @var int */
private $repeatCells;
/** @var bool */
private $showAll;
/** @var string */
private $themeImage;
/** @var Template */
public $template;
/**
* @param Template $template Template object
*/
public function __construct(Template $template)
{
global $cfg, $PMA_Theme;
$this->template = $template;
$this->limitChars = (int) $cfg['LimitChars'];
$this->maxRows = (int) $cfg['MaxRows'];
$this->repeatCells = (int) $cfg['RepeatCells'];
$this->showAll = (bool) $cfg['ShowAll'];
$this->themeImage = $PMA_Theme->getImgPath();
}
/**
* Function to get html for one relational key
*
* @param int $horizontal_count the current horizontal count
* @param string $header table header
* @param array $keys all the keys
* @param int $indexByKeyname index by keyname
* @param array $descriptions descriptions
* @param int $indexByDescription index by description
* @param string $current_value current value on the edit form
*
* @return array the generated html
*/
private function getHtmlForOneKey(
int $horizontal_count,
string $header,
array $keys,
int $indexByKeyname,
array $descriptions,
int $indexByDescription,
string $current_value
): array {
$horizontal_count++;
$output = '';
// whether the key name corresponds to the selected value in the form
$rightKeynameIsSelected = false;
$leftKeynameIsSelected = false;
if ($this->repeatCells > 0 && $horizontal_count > $this->repeatCells) {
$output .= $header;
$horizontal_count = 0;
}
// key names and descriptions for the left section,
// sorted by key names
$leftKeyname = $keys[$indexByKeyname];
[
$leftDescription,
$leftDescriptionTitle,
] = $this->getDescriptionAndTitle($descriptions[$indexByKeyname]);
// key names and descriptions for the right section,
// sorted by descriptions
$rightKeyname = $keys[$indexByDescription];
[
$rightDescription,
$rightDescriptionTitle,
] = $this->getDescriptionAndTitle($descriptions[$indexByDescription]);
$indexByDescription++;
if (! empty($current_value)) {
$rightKeynameIsSelected = $rightKeyname == $current_value;
$leftKeynameIsSelected = $leftKeyname == $current_value;
}
$output .= '<tr class="noclick">';
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $leftKeyname,
'description' => $leftDescription,
'title' => $leftDescriptionTitle,
'is_selected' => $leftKeynameIsSelected,
'nowrap' => true,
]);
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $leftKeyname,
'description' => $leftDescription,
'title' => $leftDescriptionTitle,
'is_selected' => $leftKeynameIsSelected,
'nowrap' => false,
]);
$output .= '<td width="20%">'
. '<img src="' . $this->themeImage . 'spacer.png" alt=""'
. ' width="1" height="1"></td>';
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $rightKeyname,
'description' => $rightDescription,
'title' => $rightDescriptionTitle,
'is_selected' => $rightKeynameIsSelected,
'nowrap' => false,
]);
$output .= $this->template->render('table/browse_foreigners/column_element', [
'keyname' => $rightKeyname,
'description' => $rightDescription,
'title' => $rightDescriptionTitle,
'is_selected' => $rightKeynameIsSelected,
'nowrap' => true,
]);
$output .= '</tr>';
return [
$output,
$horizontal_count,
$indexByDescription,
];
}
/**
* Function to get html for relational field selection
*
* @param string $db current database
* @param string $table current table
* @param string $field field
* @param array $foreignData foreign column data
* @param string|null $fieldkey field key
* @param string $current_value current columns's value
*/
public function getHtmlForRelationalFieldSelection(
string $db,
string $table,
string $field,
array $foreignData,
?string $fieldkey,
string $current_value
): string {
$gotopage = $this->getHtmlForGotoPage($foreignData);
$foreignShowAll = $this->template->render('table/browse_foreigners/show_all', [
'foreign_data' => $foreignData,
'show_all' => $this->showAll,
'max_rows' => $this->maxRows,
]);
$output = '<form class="ajax" '
. 'id="browse_foreign_form" name="browse_foreign_from" action="'
. Url::getFromRoute('/browse-foreigners')
. '" method="post"><fieldset>'
. Url::getHiddenInputs($db, $table)
. '<input type="hidden" name="field" value="' . htmlspecialchars($field)
. '">'
. '<input type="hidden" name="fieldkey" value="'
. (isset($fieldkey) ? htmlspecialchars($fieldkey) : '') . '">';
if (isset($_POST['rownumber'])) {
$output .= '<input type="hidden" name="rownumber" value="'
. htmlspecialchars((string) $_POST['rownumber']) . '">';
}
$filter_value = (isset($_POST['foreign_filter'])
? htmlspecialchars($_POST['foreign_filter'])
: '');
$output .= '<span class="formelement">'
. '<label for="input_foreign_filter">' . __('Search:') . '</label>'
. '<input type="text" name="foreign_filter" '
. 'id="input_foreign_filter" '
. 'value="' . $filter_value . '" data-old="' . $filter_value . '" '
. '>'
. '<input class="btn btn-primary" type="submit" name="submit_foreign_filter" value="'
. __('Go') . '">'
. '</span>'
. '<span class="formelement">' . $gotopage . '</span>'
. '<span class="formelement">' . $foreignShowAll . '</span>'
. '</fieldset>'
. '</form>';
$output .= '<table class="pma-table" width="100%" id="browse_foreign_table">';
if (! is_array($foreignData['disp_row'])) {
return $output . '</tbody>'
. '</table>';
}
$header = '<tr>
<th>' . __('Keyname') . '</th>
<th>' . __('Description') . '</th>
<td width="20%"></td>
<th>' . __('Description') . '</th>
<th>' . __('Keyname') . '</th>
</tr>';
$output .= '<thead>' . $header . '</thead>' . "\n"
. '<tfoot>' . $header . '</tfoot>' . "\n"
. '<tbody>' . "\n";
$descriptions = [];
$keys = [];
foreach ($foreignData['disp_row'] as $relrow) {
if ($foreignData['foreign_display'] != false) {
$descriptions[] = $relrow[$foreignData['foreign_display']] ?? '';
} else {
$descriptions[] = '';
}
$keys[] = $relrow[$foreignData['foreign_field']];
}
asort($keys);
$horizontal_count = 0;
$indexByDescription = 0;
foreach ($keys as $indexByKeyname => $value) {
[
$html,
$horizontal_count,
$indexByDescription,
] = $this->getHtmlForOneKey(
$horizontal_count,
$header,
$keys,
$indexByKeyname,
$descriptions,
$indexByDescription,
$current_value
);
$output .= $html;
}
$output .= '</tbody>'
. '</table>';
return $output;
}
/**
* Get the description (possibly truncated) and the title
*
* @param string $description the key name's description
*
* @return array the new description and title
*/
private function getDescriptionAndTitle(string $description): array
{
if (mb_strlen($description) <= $this->limitChars) {
$description = htmlspecialchars(
$description
);
$descriptionTitle = '';
} else {
$descriptionTitle = htmlspecialchars(
$description
);
$description = htmlspecialchars(
mb_substr(
$description,
0,
$this->limitChars
)
. '...'
);
}
return [
$description,
$descriptionTitle,
];
}
/**
* Function to get html for the goto page option
*
* @param array|null $foreignData foreign data
*/
private function getHtmlForGotoPage(?array $foreignData): string
{
$gotopage = '';
isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0;
if ($foreignData === null || ! is_array($foreignData['disp_row'])) {
return $gotopage;
}
$pageNow = (int) floor($pos / $this->maxRows) + 1;
$nbTotalPage = (int) ceil($foreignData['the_total'] / $this->maxRows);
if ($foreignData['the_total'] > $this->maxRows) {
$gotopage = Util::pageselector(
'pos',
$this->maxRows,
$pageNow,
$nbTotalPage,
200,
5,
5,
20,
10,
__('Page number:')
);
}
return $gotopage;
}
/**
* Function to get foreign limit
*
* @param string|null $foreignShowAll foreign navigation
*/
public function getForeignLimit(?string $foreignShowAll): ?string
{
if (isset($foreignShowAll) && $foreignShowAll == __('Show all')) {
return null;
}
isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0;
return 'LIMIT ' . $pos . ', ' . $this->maxRows . ' ';
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* MySQL charset metadata and manipulations
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use const SORT_STRING;
use function array_keys;
use function count;
use function explode;
use function is_string;
use function ksort;
/**
* Class used to manage MySQL charsets
*/
class Charsets
{
/**
* MySQL charsets map
*
* @var array
*/
public static $mysqlCharsetMap = [
'big5' => 'big5',
'cp-866' => 'cp866',
'euc-jp' => 'ujis',
'euc-kr' => 'euckr',
'gb2312' => 'gb2312',
'gbk' => 'gbk',
'iso-8859-1' => 'latin1',
'iso-8859-2' => 'latin2',
'iso-8859-7' => 'greek',
'iso-8859-8' => 'hebrew',
'iso-8859-8-i' => 'hebrew',
'iso-8859-9' => 'latin5',
'iso-8859-13' => 'latin7',
'iso-8859-15' => 'latin1',
'koi8-r' => 'koi8r',
'shift_jis' => 'sjis',
'tis-620' => 'tis620',
'utf-8' => 'utf8',
'windows-1250' => 'cp1250',
'windows-1251' => 'cp1251',
'windows-1252' => 'latin1',
'windows-1256' => 'cp1256',
'windows-1257' => 'cp1257',
];
/**
* The charset for the server
*
* @var Charset|null
*/
private static $serverCharset = null;
/** @var array<string, Charset> */
private static $charsets = [];
/** @var array<string, array<string, Collation>> */
private static $collations = [];
/**
* Loads charset data from the server
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
private static function loadCharsets(DatabaseInterface $dbi, bool $disableIs): void
{
/* Data already loaded */
if (count(self::$charsets) > 0) {
return;
}
if ($disableIs) {
$sql = 'SHOW CHARACTER SET';
} else {
$sql = 'SELECT `CHARACTER_SET_NAME` AS `Charset`,'
. ' `DEFAULT_COLLATE_NAME` AS `Default collation`,'
. ' `DESCRIPTION` AS `Description`,'
. ' `MAXLEN` AS `Maxlen`'
. ' FROM `information_schema`.`CHARACTER_SETS`';
}
$res = $dbi->query($sql);
self::$charsets = [];
while ($row = $dbi->fetchAssoc($res)) {
self::$charsets[$row['Charset']] = Charset::fromServer($row);
}
$dbi->freeResult($res);
ksort(self::$charsets, SORT_STRING);
}
/**
* Loads collation data from the server
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
private static function loadCollations(DatabaseInterface $dbi, bool $disableIs): void
{
/* Data already loaded */
if (count(self::$collations) > 0) {
return;
}
if ($disableIs) {
$sql = 'SHOW COLLATION';
} else {
$sql = 'SELECT `COLLATION_NAME` AS `Collation`,'
. ' `CHARACTER_SET_NAME` AS `Charset`,'
. ' `ID` AS `Id`,'
. ' `IS_DEFAULT` AS `Default`,'
. ' `IS_COMPILED` AS `Compiled`,'
. ' `SORTLEN` AS `Sortlen`'
. ' FROM `information_schema`.`COLLATIONS`';
}
$res = $dbi->query($sql);
self::$collations = [];
while ($row = $dbi->fetchAssoc($res)) {
self::$collations[$row['Charset']][$row['Collation']] = Collation::fromServer($row);
}
$dbi->freeResult($res);
foreach (array_keys(self::$collations) as $charset) {
ksort(self::$collations[$charset], SORT_STRING);
}
}
/**
* Get current server charset
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*/
public static function getServerCharset(DatabaseInterface $dbi, bool $disableIs): Charset
{
if (self::$serverCharset !== null) {
return self::$serverCharset;
}
self::loadCharsets($dbi, $disableIs);
$serverCharset = $dbi->getVariable('character_set_server');
if (! is_string($serverCharset)) {// MySQL 5.7.8 fallback, issue #15614
$serverCharset = $dbi->fetchValue('SELECT @@character_set_server;');
}
self::$serverCharset = self::$charsets[$serverCharset] ?? null;
// MySQL 8.0.11+ fallback, issue #16931
if (self::$serverCharset === null && $serverCharset === 'utf8mb3') {
// See: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-charset
// The utf8mb3 character set will be replaced by utf8mb4 in a future MySQL version.
// The utf8 character set is currently an alias for utf8mb3,
// but will at that point become a reference to utf8mb4.
// To avoid ambiguity about the meaning of utf8,
// consider specifying utf8mb4 explicitly for character set references instead of utf8.
// Warning: #3719 'utf8' is currently an alias for the character set UTF8MB3 [...]
return self::$charsets['utf8'];
}
if (self::$serverCharset === null) {// Fallback in case nothing is found
return Charset::fromServer(
[
'Charset' => __('Unknown'),
'Description' => __('Unknown'),
]
);
}
return self::$serverCharset;
}
/**
* Get all server charsets
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*
* @return array
*/
public static function getCharsets(DatabaseInterface $dbi, bool $disableIs): array
{
self::loadCharsets($dbi, $disableIs);
return self::$charsets;
}
/**
* Get all server collations
*
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
*
* @return array
*/
public static function getCollations(DatabaseInterface $dbi, bool $disableIs): array
{
self::loadCollations($dbi, $disableIs);
return self::$collations;
}
/**
* @param DatabaseInterface $dbi DatabaseInterface instance
* @param bool $disableIs Disable use of INFORMATION_SCHEMA
* @param string|null $name Collation name
*/
public static function findCollationByName(DatabaseInterface $dbi, bool $disableIs, ?string $name): ?Collation
{
$pieces = explode('_', (string) $name);
if ($pieces === false || ! isset($pieces[0])) {
return null;
}
$charset = $pieces[0];
$collations = self::getCollations($dbi, $disableIs);
return $collations[$charset][$name] ?? null;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* Value object class for a character set
*/
declare(strict_types=1);
namespace PhpMyAdmin\Charsets;
/**
* Value object class for a character set
*/
final class Charset
{
/**
* The character set name
*
* @var string
*/
private $name;
/**
* A description of the character set
*
* @var string
*/
private $description;
/**
* The default collation for the character set
*
* @var string
*/
private $defaultCollation;
/**
* The maximum number of bytes required to store one character
*
* @var int
*/
private $maxLength;
/**
* @param string $name Charset name
* @param string $description Description
* @param string $defaultCollation Default collation
* @param int $maxLength Maximum length
*/
private function __construct(
string $name,
string $description,
string $defaultCollation,
int $maxLength
) {
$this->name = $name;
$this->description = $description;
$this->defaultCollation = $defaultCollation;
$this->maxLength = $maxLength;
}
/**
* @param array $state State obtained from the database server
*
* @return Charset
*/
public static function fromServer(array $state): self
{
return new self(
$state['Charset'] ?? '',
$state['Description'] ?? '',
$state['Default collation'] ?? '',
(int) ($state['Maxlen'] ?? 0)
);
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getDefaultCollation(): string
{
return $this->defaultCollation;
}
public function getMaxLength(): int
{
return $this->maxLength;
}
}

View file

@ -0,0 +1,587 @@
<?php
/**
* Value object class for a collation
*/
declare(strict_types=1);
namespace PhpMyAdmin\Charsets;
use function count;
use function explode;
use function implode;
/**
* Value object class for a collation
*/
final class Collation
{
/**
* The collation name
*
* @var string
*/
private $name;
/**
* A description of the collation
*
* @var string
*/
private $description;
/**
* The name of the character set with which the collation is associated
*
* @var string
*/
private $charset;
/**
* The collation ID
*
* @var int
*/
private $id;
/**
* Whether the collation is the default for its character set
*
* @var bool
*/
private $isDefault;
/**
* Whether the character set is compiled into the server
*
* @var bool
*/
private $isCompiled;
/**
* Used for determining the memory used to sort strings in this collation
*
* @var int
*/
private $sortLength;
/**
* The collation pad attribute
*
* @var string
*/
private $padAttribute;
/**
* @param string $name Collation name
* @param string $charset Related charset
* @param int $id Collation ID
* @param bool $isDefault Whether is the default
* @param bool $isCompiled Whether the charset is compiled
* @param int $sortLength Sort length
* @param string $padAttribute Pad attribute
*/
private function __construct(
string $name,
string $charset,
int $id,
bool $isDefault,
bool $isCompiled,
int $sortLength,
string $padAttribute
) {
$this->name = $name;
$this->charset = $charset;
$this->id = $id;
$this->isDefault = $isDefault;
$this->isCompiled = $isCompiled;
$this->sortLength = $sortLength;
$this->padAttribute = $padAttribute;
$this->description = $this->buildDescription();
}
/**
* @param array $state State obtained from the database server
*/
public static function fromServer(array $state): self
{
return new self(
$state['Collation'] ?? '',
$state['Charset'] ?? '',
(int) ($state['Id'] ?? 0),
isset($state['Default']) && ($state['Default'] === 'Yes' || $state['Default'] === '1'),
isset($state['Compiled']) && ($state['Compiled'] === 'Yes' || $state['Compiled'] === '1'),
(int) ($state['Sortlen'] ?? 0),
$state['Pad_attribute'] ?? ''
);
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getCharset(): string
{
return $this->charset;
}
public function getId(): int
{
return $this->id;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function isCompiled(): bool
{
return $this->isCompiled;
}
public function getSortLength(): int
{
return $this->sortLength;
}
public function getPadAttribute(): string
{
return $this->padAttribute;
}
/**
* Returns description for given collation
*
* @return string collation description
*/
private function buildDescription(): string
{
$parts = explode('_', $this->getName());
$name = __('Unknown');
$variant = null;
$suffixes = [];
$unicode = false;
$unknown = false;
$level = 0;
foreach ($parts as $part) {
if ($level === 0) {
/* Next will be language */
$level = 1;
/* First should be charset */
$this->getNameForLevel0(
$unicode, // By reference
$unknown, // Same
$part,
$name, // By reference
$variant// Same
);
continue;
}
if ($level === 1) {
/* Next will be variant unless changed later */
$level = 4;
/* Locale name or code */
$found = true;
$this->getNameForLevel1(
$unicode,
$unknown,
$part,
$name, // By reference, will be changed
$level, // Also
$found// Same
);
if ($found) {
continue;
}
// Not parsed token, fall to next level
}
if ($level === 2) {
/* Next will be variant */
$level = 4;
/* Germal variant */
if ($part === 'pb') {
$name = _pgettext('Collation', 'German (phone book order)');
continue;
}
$name = _pgettext('Collation', 'German (dictionary order)');
// Not parsed token, fall to next level
}
if ($level === 3) {
/* Next will be variant */
$level = 4;
/* Spanish variant */
if ($part === 'trad') {
$name = _pgettext('Collation', 'Spanish (traditional)');
continue;
}
$name = _pgettext('Collation', 'Spanish (modern)');
// Not parsed token, fall to next level
}
if ($level === 4) {
/* Next will be suffix */
$level = 5;
/* Variant */
$found = true;
$variantFound = $this->getVariant($part);
if ($variantFound === null) {
$found = false;
} else {
$variant = $variantFound;
}
if ($found) {
continue;
}
// Not parsed token, fall to next level
}
if ($level < 5) {
continue;
}
/* Suffixes */
$suffixes = $this->addSuffixes($suffixes, $part);
}
return $this->buildName($name, $variant, $suffixes);
}
private function buildName(string $result, ?string $variant, array $suffixes): string
{
if ($variant !== null) {
$result .= ' (' . $variant . ')';
}
if (count($suffixes) > 0) {
$result .= ', ' . implode(', ', $suffixes);
}
return $result;
}
private function getVariant(string $part): ?string
{
switch ($part) {
case '0900':
return 'UCA 9.0.0';
case '520':
return 'UCA 5.2.0';
case 'mysql561':
return 'MySQL 5.6.1';
case 'mysql500':
return 'MySQL 5.0.0';
default:
return null;
}
}
private function addSuffixes(array $suffixes, string $part): array
{
switch ($part) {
case 'ci':
$suffixes[] = _pgettext('Collation variant', 'case-insensitive');
break;
case 'cs':
$suffixes[] = _pgettext('Collation variant', 'case-sensitive');
break;
case 'ai':
$suffixes[] = _pgettext('Collation variant', 'accent-insensitive');
break;
case 'as':
$suffixes[] = _pgettext('Collation variant', 'accent-sensitive');
break;
case 'ks':
$suffixes[] = _pgettext('Collation variant', 'kana-sensitive');
break;
case 'w2':
case 'l2':
$suffixes[] = _pgettext('Collation variant', 'multi-level');
break;
case 'bin':
$suffixes[] = _pgettext('Collation variant', 'binary');
break;
case 'nopad':
$suffixes[] = _pgettext('Collation variant', 'no-pad');
break;
}
return $suffixes;
}
private function getNameForLevel0(
bool &$unicode,
bool &$unknown,
string $part,
?string &$name,
?string &$variant
): void {
switch ($part) {
case 'binary':
$name = _pgettext('Collation', 'Binary');
break;
// Unicode charsets
case 'utf8mb4':
$variant = 'UCA 4.0.0';
// Fall through to other unicode
case 'ucs2':
case 'utf8':
case 'utf16':
case 'utf16le':
case 'utf16be':
case 'utf32':
$name = _pgettext('Collation', 'Unicode');
$unicode = true;
break;
// West European charsets
case 'ascii':
case 'cp850':
case 'dec8':
case 'hp8':
case 'latin1':
case 'macroman':
$name = _pgettext('Collation', 'West European');
break;
// Central European charsets
case 'cp1250':
case 'cp852':
case 'latin2':
case 'macce':
$name = _pgettext('Collation', 'Central European');
break;
// Russian charsets
case 'cp866':
case 'koi8r':
$name = _pgettext('Collation', 'Russian');
break;
// Chinese charsets
case 'gb2312':
case 'gbk':
$name = _pgettext('Collation', 'Simplified Chinese');
break;
case 'big5':
$name = _pgettext('Collation', 'Traditional Chinese');
break;
case 'gb18030':
$name = _pgettext('Collation', 'Chinese');
$unicode = true;
break;
// Japanese charsets
case 'sjis':
case 'ujis':
case 'cp932':
case 'eucjpms':
$name = _pgettext('Collation', 'Japanese');
break;
// Baltic charsets
case 'cp1257':
case 'latin7':
$name = _pgettext('Collation', 'Baltic');
break;
// Other
case 'armscii8':
case 'armscii':
$name = _pgettext('Collation', 'Armenian');
break;
case 'cp1251':
$name = _pgettext('Collation', 'Cyrillic');
break;
case 'cp1256':
$name = _pgettext('Collation', 'Arabic');
break;
case 'euckr':
$name = _pgettext('Collation', 'Korean');
break;
case 'hebrew':
$name = _pgettext('Collation', 'Hebrew');
break;
case 'geostd8':
$name = _pgettext('Collation', 'Georgian');
break;
case 'greek':
$name = _pgettext('Collation', 'Greek');
break;
case 'keybcs2':
$name = _pgettext('Collation', 'Czech-Slovak');
break;
case 'koi8u':
$name = _pgettext('Collation', 'Ukrainian');
break;
case 'latin5':
$name = _pgettext('Collation', 'Turkish');
break;
case 'swe7':
$name = _pgettext('Collation', 'Swedish');
break;
case 'tis620':
$name = _pgettext('Collation', 'Thai');
break;
default:
$name = _pgettext('Collation', 'Unknown');
$unknown = true;
break;
}
}
private function getNameForLevel1(
bool $unicode,
bool $unknown,
string $part,
?string &$name,
int &$level,
bool &$found
): void {
switch ($part) {
case 'general':
break;
case 'bulgarian':
case 'bg':
$name = _pgettext('Collation', 'Bulgarian');
break;
case 'chinese':
case 'cn':
case 'zh':
if ($unicode) {
$name = _pgettext('Collation', 'Chinese');
}
break;
case 'croatian':
case 'hr':
$name = _pgettext('Collation', 'Croatian');
break;
case 'czech':
case 'cs':
$name = _pgettext('Collation', 'Czech');
break;
case 'danish':
case 'da':
$name = _pgettext('Collation', 'Danish');
break;
case 'english':
case 'en':
$name = _pgettext('Collation', 'English');
break;
case 'esperanto':
case 'eo':
$name = _pgettext('Collation', 'Esperanto');
break;
case 'estonian':
case 'et':
$name = _pgettext('Collation', 'Estonian');
break;
case 'german1':
$name = _pgettext('Collation', 'German (dictionary order)');
break;
case 'german2':
$name = _pgettext('Collation', 'German (phone book order)');
break;
case 'german':
case 'de':
/* Name is set later */
$level = 2;
break;
case 'hungarian':
case 'hu':
$name = _pgettext('Collation', 'Hungarian');
break;
case 'icelandic':
case 'is':
$name = _pgettext('Collation', 'Icelandic');
break;
case 'japanese':
case 'ja':
$name = _pgettext('Collation', 'Japanese');
break;
case 'la':
$name = _pgettext('Collation', 'Classical Latin');
break;
case 'latvian':
case 'lv':
$name = _pgettext('Collation', 'Latvian');
break;
case 'lithuanian':
case 'lt':
$name = _pgettext('Collation', 'Lithuanian');
break;
case 'korean':
case 'ko':
$name = _pgettext('Collation', 'Korean');
break;
case 'myanmar':
case 'my':
$name = _pgettext('Collation', 'Burmese');
break;
case 'persian':
$name = _pgettext('Collation', 'Persian');
break;
case 'polish':
case 'pl':
$name = _pgettext('Collation', 'Polish');
break;
case 'roman':
$name = _pgettext('Collation', 'West European');
break;
case 'romanian':
case 'ro':
$name = _pgettext('Collation', 'Romanian');
break;
case 'ru':
$name = _pgettext('Collation', 'Russian');
break;
case 'si':
case 'sinhala':
$name = _pgettext('Collation', 'Sinhalese');
break;
case 'slovak':
case 'sk':
$name = _pgettext('Collation', 'Slovak');
break;
case 'slovenian':
case 'sl':
$name = _pgettext('Collation', 'Slovenian');
break;
case 'spanish':
$name = _pgettext('Collation', 'Spanish (modern)');
break;
case 'es':
/* Name is set later */
$level = 3;
break;
case 'spanish2':
$name = _pgettext('Collation', 'Spanish (traditional)');
break;
case 'swedish':
case 'sv':
$name = _pgettext('Collation', 'Swedish');
break;
case 'thai':
case 'th':
$name = _pgettext('Collation', 'Thai');
break;
case 'turkish':
case 'tr':
$name = _pgettext('Collation', 'Turkish');
break;
case 'ukrainian':
case 'uk':
$name = _pgettext('Collation', 'Ukrainian');
break;
case 'vietnamese':
case 'vi':
$name = _pgettext('Collation', 'Vietnamese');
break;
case 'unicode':
if ($unknown) {
$name = _pgettext('Collation', 'Unicode');
}
break;
default:
$found = false;
}
}
}

View file

@ -0,0 +1,370 @@
<?php
/**
* Get user's global privileges and some db-specific privileges
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\Utils\SessionCache;
use function mb_strpos;
use function mb_substr;
use function preg_match;
use function preg_replace;
use function strpos;
/**
* PhpMyAdmin\CheckUserPrivileges class
*/
class CheckUserPrivileges
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param DatabaseInterface $dbi DatabaseInterface object
*/
public function __construct(DatabaseInterface $dbi)
{
$this->dbi = $dbi;
}
/**
* Extracts details from a result row of a SHOW GRANT query
*
* @param string $row grant row
*
* @return array
*/
public function getItemsFromShowGrantsRow(string $row): array
{
$db_name_offset = mb_strpos($row, ' ON ') + 4;
$tblname_end_offset = mb_strpos($row, ' TO ');
$tblname_start_offset = false;
$__tblname_start_offset = mb_strpos($row, '`.', $db_name_offset);
if ($__tblname_start_offset && $__tblname_start_offset < $tblname_end_offset) {
$tblname_start_offset = $__tblname_start_offset + 1;
}
if ($tblname_start_offset === false) {
$tblname_start_offset = mb_strpos($row, '.', $db_name_offset);
}
$show_grants_dbname = mb_substr(
$row,
$db_name_offset,
$tblname_start_offset - $db_name_offset
);
$show_grants_dbname = Util::unQuote($show_grants_dbname, '`');
$show_grants_str = mb_substr(
$row,
6,
mb_strpos($row, ' ON ') - 6
);
$show_grants_tblname = mb_substr(
$row,
$tblname_start_offset + 1,
$tblname_end_offset - $tblname_start_offset - 1
);
$show_grants_tblname = Util::unQuote($show_grants_tblname, '`');
return [
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname,
];
}
/**
* Check if user has required privileges for
* performing 'Adjust privileges' operations
*
* @param string $show_grants_str string containing grants for user
* @param string $show_grants_dbname name of db extracted from grant string
* @param string $show_grants_tblname name of table extracted from grant string
*/
public function checkRequiredPrivilegesForAdjust(
string $show_grants_str,
string $show_grants_dbname,
string $show_grants_tblname
): void {
// '... ALL PRIVILEGES ON *.* ...' OR '... ALL PRIVILEGES ON `mysql`.* ..'
// OR
// SELECT, INSERT, UPDATE, DELETE .... ON *.* OR `mysql`.*
if ($show_grants_str !== 'ALL'
&& $show_grants_str !== 'ALL PRIVILEGES'
&& (mb_strpos(
$show_grants_str,
'SELECT, INSERT, UPDATE, DELETE'
) === false)
) {
return;
}
if ($show_grants_dbname === '*'
&& $show_grants_tblname === '*'
) {
$GLOBALS['col_priv'] = true;
$GLOBALS['db_priv'] = true;
$GLOBALS['proc_priv'] = true;
$GLOBALS['table_priv'] = true;
if ($show_grants_str === 'ALL PRIVILEGES'
|| $show_grants_str === 'ALL'
) {
$GLOBALS['is_reload_priv'] = true;
}
}
// check for specific tables in `mysql` db
// Ex. '... ALL PRIVILEGES on `mysql`.`columns_priv` .. '
if ($show_grants_dbname !== 'mysql') {
return;
}
switch ($show_grants_tblname) {
case 'columns_priv':
$GLOBALS['col_priv'] = true;
break;
case 'db':
$GLOBALS['db_priv'] = true;
break;
case 'procs_priv':
$GLOBALS['proc_priv'] = true;
break;
case 'tables_priv':
$GLOBALS['table_priv'] = true;
break;
case '*':
$GLOBALS['col_priv'] = true;
$GLOBALS['db_priv'] = true;
$GLOBALS['proc_priv'] = true;
$GLOBALS['table_priv'] = true;
break;
default:
}
}
/**
* sets privilege information extracted from SHOW GRANTS result
*
* Detection for some CREATE privilege.
*
* Since MySQL 4.1.2, we can easily detect current user's grants using $userlink
* (no control user needed) and we don't have to try any other method for
* detection
*
* @todo fix to get really all privileges, not only explicitly defined for this user
* from MySQL manual: (https://dev.mysql.com/doc/refman/5.0/en/show-grants.html)
* SHOW GRANTS displays only the privileges granted explicitly to the named
* account. Other privileges might be available to the account, but they are not
* displayed. For example, if an anonymous account exists, the named account
* might be able to use its privileges, but SHOW GRANTS will not display them.
*/
private function analyseShowGrant(): void
{
if (SessionCache::has('is_create_db_priv')) {
$GLOBALS['is_create_db_priv'] = SessionCache::get(
'is_create_db_priv'
);
$GLOBALS['is_reload_priv'] = SessionCache::get(
'is_reload_priv'
);
$GLOBALS['db_to_create'] = SessionCache::get(
'db_to_create'
);
$GLOBALS['dbs_where_create_table_allowed'] = SessionCache::get(
'dbs_where_create_table_allowed'
);
$GLOBALS['dbs_to_test'] = SessionCache::get(
'dbs_to_test'
);
$GLOBALS['db_priv'] = SessionCache::get(
'db_priv'
);
$GLOBALS['col_priv'] = SessionCache::get(
'col_priv'
);
$GLOBALS['table_priv'] = SessionCache::get(
'table_priv'
);
$GLOBALS['proc_priv'] = SessionCache::get(
'proc_priv'
);
return;
}
// defaults
$GLOBALS['is_create_db_priv'] = false;
$GLOBALS['is_reload_priv'] = false;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'] = [];
$GLOBALS['dbs_to_test'] = Utilities::getSystemSchemas();
$GLOBALS['proc_priv'] = false;
$GLOBALS['db_priv'] = false;
$GLOBALS['col_priv'] = false;
$GLOBALS['table_priv'] = false;
$rs_usr = $this->dbi->tryQuery('SHOW GRANTS');
if (! $rs_usr) {
return;
}
$re0 = '(^|(\\\\\\\\)+|[^\\\\])'; // non-escaped wildcards
$re1 = '(^|[^\\\\])(\\\)+'; // escaped wildcards
while ($row = $this->dbi->fetchRow($rs_usr)) {
[
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname,
] = $this->getItemsFromShowGrantsRow($row[0]);
if ($show_grants_dbname === '*') {
if ($show_grants_str !== 'USAGE') {
$GLOBALS['dbs_to_test'] = false;
}
} elseif ($GLOBALS['dbs_to_test'] !== false) {
$GLOBALS['dbs_to_test'][] = $show_grants_dbname;
}
if (mb_strpos($show_grants_str, 'RELOAD') !== false) {
$GLOBALS['is_reload_priv'] = true;
}
// check for the required privileges for adjust
$this->checkRequiredPrivilegesForAdjust(
$show_grants_str,
$show_grants_dbname,
$show_grants_tblname
);
/**
* @todo if we find CREATE VIEW but not CREATE, do not offer
* the create database dialog box
*/
if ($show_grants_str !== 'ALL'
&& $show_grants_str !== 'ALL PRIVILEGES'
&& $show_grants_str !== 'CREATE'
&& strpos($show_grants_str, 'CREATE,') === false
) {
continue;
}
if ($show_grants_dbname === '*') {
// a global CREATE privilege
$GLOBALS['is_create_db_priv'] = true;
$GLOBALS['is_reload_priv'] = true;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'][] = '*';
// @todo we should not break here, cause GRANT ALL *.*
// could be revoked by a later rule like GRANT SELECT ON db.*
break;
}
// this array may contain wildcards
$GLOBALS['dbs_where_create_table_allowed'][] = $show_grants_dbname;
$dbname_to_test = Util::backquote($show_grants_dbname);
if ($GLOBALS['is_create_db_priv']) {
// no need for any more tests if we already know this
continue;
}
// does this db exist?
if ((! preg_match('/' . $re0 . '%|_/', $show_grants_dbname)
|| preg_match('/\\\\%|\\\\_/', $show_grants_dbname))
&& ($this->dbi->tryQuery(
'USE ' . preg_replace(
'/' . $re1 . '(%|_)/',
'\\1\\3',
$dbname_to_test
)
)
|| mb_substr((string) $this->dbi->getError(), 1, 4) == 1044)
) {
continue;
}
/**
* Do not handle the underscore wildcard
* (this case must be rare anyway)
*/
$GLOBALS['db_to_create'] = preg_replace(
'/' . $re0 . '%/',
'\\1',
$show_grants_dbname
);
$GLOBALS['db_to_create'] = preg_replace(
'/' . $re1 . '(%|_)/',
'\\1\\3',
$GLOBALS['db_to_create']
);
$GLOBALS['is_create_db_priv'] = true;
/**
* @todo collect $GLOBALS['db_to_create'] into an array,
* to display a drop-down in the "Create database" dialog
*/
// we don't break, we want all possible databases
//break;
}
$this->dbi->freeResult($rs_usr);
// must also cacheUnset() them in
// PhpMyAdmin\Plugins\Auth\AuthenticationCookie
SessionCache::set('is_create_db_priv', $GLOBALS['is_create_db_priv']);
SessionCache::set('is_reload_priv', $GLOBALS['is_reload_priv']);
SessionCache::set('db_to_create', $GLOBALS['db_to_create']);
SessionCache::set(
'dbs_where_create_table_allowed',
$GLOBALS['dbs_where_create_table_allowed']
);
SessionCache::set('dbs_to_test', $GLOBALS['dbs_to_test']);
SessionCache::set('proc_priv', $GLOBALS['proc_priv']);
SessionCache::set('table_priv', $GLOBALS['table_priv']);
SessionCache::set('col_priv', $GLOBALS['col_priv']);
SessionCache::set('db_priv', $GLOBALS['db_priv']);
}
/**
* Get user's global privileges and some db-specific privileges
*/
public function getPrivileges(): void
{
$username = '';
$current = $this->dbi->getCurrentUserAndHost();
if (! empty($current)) {
[$username] = $current;
}
// If MySQL is started with --skip-grant-tables
if ($username === '') {
$GLOBALS['is_create_db_priv'] = true;
$GLOBALS['is_reload_priv'] = true;
$GLOBALS['db_to_create'] = '';
$GLOBALS['dbs_where_create_table_allowed'] = ['*'];
$GLOBALS['dbs_to_test'] = false;
$GLOBALS['db_priv'] = true;
$GLOBALS['col_priv'] = true;
$GLOBALS['table_priv'] = true;
$GLOBALS['proc_priv'] = true;
} else {
$this->analyseShowGrant();
}
}
}

View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Routing;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
use PhpMyAdmin\Twig\CoreExtension;
use PhpMyAdmin\Twig\I18nExtension;
use PhpMyAdmin\Twig\MessageExtension;
use PhpMyAdmin\Twig\PluginsExtension;
use PhpMyAdmin\Twig\RelationExtension;
use PhpMyAdmin\Twig\SanitizeExtension;
use PhpMyAdmin\Twig\TableExtension;
use PhpMyAdmin\Twig\TrackerExtension;
use PhpMyAdmin\Twig\TransformationsExtension;
use PhpMyAdmin\Twig\UrlExtension;
use PhpMyAdmin\Twig\UtilExtension;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Twig\Cache\CacheInterface;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use function fclose;
use function fopen;
use function fwrite;
use function json_encode;
use function str_replace;
use function strpos;
use function is_file;
use function sprintf;
final class CacheWarmupCommand extends Command
{
/** @var string */
protected static $defaultName = 'cache:warmup';
protected function configure(): void
{
$this->setDescription('Warms up the Twig templates cache');
$this->addOption('twig', null, null, 'Warm up twig templates cache.');
$this->addOption('routing', null, null, 'Warm up routing cache.');
$this->setHelp('The <info>%command.name%</info> command warms up the cache of the Twig templates.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('twig') === true && $input->getOption('routing') === true) {
$output->writeln('Please specify --twig or --routing');
return 1;
}
if ($input->getOption('twig') === true) {
return $this->warmUpTwigCache($output);
}
if ($input->getOption('routing') === true) {
return $this->warmUpRoutingCache($output);
}
$output->writeln('Warming up all caches.', OutputInterface::VERBOSITY_VERBOSE);
$twigCode = $this->warmUptwigCache($output);
if ($twigCode !== 0) {
$output->writeln('Twig cache generation had an error.');
return $twigCode;
}
$routingCode = $this->warmUpTwigCache($output);
if ($routingCode !== 0) {
$output->writeln('Routing cache generation had an error.');
return $twigCode;
}
$output->writeln('Warm up of all caches done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
private function warmUpRoutingCache(OutputInterface $output): int
{
$output->writeln('Warming up the routing cache', OutputInterface::VERBOSITY_VERBOSE);
Routing::getDispatcher();
if (is_file(Routing::ROUTES_CACHE_FILE)) {
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
$output->writeln(
sprintf(
'Warm up did not work, the folder "%s" is probably not writable.',
CACHE_DIR
),
OutputInterface::VERBOSITY_NORMAL
);
return 1;
}
private function warmUpTwigCache(OutputInterface $output): int
{
global $cfg, $PMA_Config, $dbi;
$output->writeln('Warming up the twig cache', OutputInterface::VERBOSITY_VERBOSE);
$cfg['environment'] = 'production';
$PMA_Config = new Config(CONFIG_FILE);
$PMA_Config->set('environment', $cfg['environment']);
$dbi = new DatabaseInterface(new DbiDummy());
$tplDir = ROOT_PATH . 'templates';
$tmpDir = ROOT_PATH . 'twig-templates';
$loader = new FilesystemLoader($tplDir);
$twig = new Environment($loader, [
'auto_reload' => true,
'cache' => $tmpDir,
]);
$twig->setExtensions([
new CoreExtension(),
new I18nExtension(),
new MessageExtension(),
new PluginsExtension(),
new RelationExtension(),
new SanitizeExtension(),
new TableExtension(),
new TrackerExtension(),
new TransformationsExtension(),
new UrlExtension(),
new UtilExtension(),
]);
/** @var CacheInterface $twigCache */
$twigCache = $twig->getCache(false);
$output->writeln('Searching for files...', OutputInterface::VERBOSITY_VERY_VERBOSE);
$replacements = [];
$templates = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tplDir),
RecursiveIteratorIterator::LEAVES_ONLY
);
$output->writeln('Warming templates', OutputInterface::VERBOSITY_VERY_VERBOSE);
foreach ($templates as $file) {
// Skip test files
if (strpos($file->getPathname(), '/test/') !== false) {
continue;
}
// force compilation
if (! $file->isFile() || $file->getExtension() !== 'twig') {
continue;
}
$name = str_replace($tplDir . '/', '', $file->getPathname());
$output->writeln('Loading: ' . $name, OutputInterface::VERBOSITY_DEBUG);
if (Environment::MAJOR_VERSION === 3) {
$template = $twig->loadTemplate($twig->getTemplateClass($name), $name);
} else {// @phpstan-ignore-line Twig 2
$template = $twig->loadTemplate($name);// @phpstan-ignore-line Twig 2
}
// Generate line map
$cacheFilename = $twigCache->generateKey($name, $twig->getTemplateClass($name));
$template_file = 'templates/' . $name;
$cache_file = str_replace($tmpDir, 'twig-templates', $cacheFilename);
$replacements[$cache_file] = [$template_file, $template->getDebugInfo()];
}
$output->writeln('Writing replacements...', OutputInterface::VERBOSITY_VERY_VERBOSE);
// Store replacements in JSON
$handle = fopen($tmpDir . '/replace.json', 'w');
if ($handle === false) {
return 1;
}
fwrite($handle, (string) json_encode($replacements));
fclose($handle);
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return 0;
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use RangeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function file_put_contents;
use function preg_match;
use function sprintf;
final class SetVersionCommand extends Command
{
/** @var string */
protected static $defaultName = 'set-version';
/** @var string */
private static $generatedClassTemplate = <<<'PHP'
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
/**
* This class is generated by scripts/console.
*
* @see \PhpMyAdmin\Command\SetVersionCommand
*/
final class Version
{
// The VERSION_SUFFIX constant is defined at libraries/vendor_config.php
public const VERSION = '%1$u.%2$u.%3$u%4$s' . VERSION_SUFFIX;
public const SERIES = '%1$u.%2$u';
public const MAJOR = %1$u;
public const MINOR = %2$u;
public const PATCH = %3$u;
public const ID = %1$u%2$02u%3$02u;
public const PRE_RELEASE_NAME = '%5$s';
public const IS_DEV = %6$s;
}
PHP;
protected function configure(): void
{
$this->setDescription('Sets the version number');
$this->setHelp('This command generates the PhpMyAdmin\Version class based on the version number provided.');
$this->addArgument('version', InputArgument::REQUIRED, 'The version number');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $version */
$version = $input->getArgument('version');
$generatedClass = $this->getGeneratedClass($version);
if (! $this->writeGeneratedClassFile($generatedClass)) {
// failure
return 1;
}
$output->writeln('PhpMyAdmin\Version class successfully generated!');
// success
return 0;
}
private function getGeneratedClass(string $version): string
{
// Do not allow any major below 5
$return = preg_match('/^([5-9]+)\.(\d{1,2})\.(\d{1,2})(-([a-z0-9]+))?$/', $version, $matches);
if ($return === false || $return === 0) {
throw new RangeException('The version number is in the wrong format: ' . $version);
}
return sprintf(
self::$generatedClassTemplate,
$matches[1],
$matches[2],
$matches[3],
$matches[4] ?? '',
$matches[5] ?? '',
($matches[5] ?? '') === 'dev' ? 'true' : 'false'
);
}
private function writeGeneratedClassFile(string $generatedClass): bool
{
$result = file_put_contents(
ROOT_PATH . 'libraries/classes/Version.php',
$generatedClass
);
return $result !== false;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,577 @@
<?php
/**
* Config file management
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use function array_diff;
use function array_flip;
use function array_keys;
use function array_walk;
use function count;
use function is_array;
use function preg_replace;
/**
* Config file management class.
* Stores its data in $_SESSION
*/
class ConfigFile
{
/**
* Stores default PMA config from config.default.php
*
* @var array
*/
private $defaultCfg;
/**
* Stores allowed values for non-standard fields
*
* @var array
*/
private $cfgDb;
/**
* Stores original PMA config, not modified by user preferences
*
* @var array|null
*/
private $baseCfg;
/**
* Whether we are currently working in PMA Setup context
*
* @var bool
*/
private $isInSetup;
/**
* Keys which will be always written to config file
*
* @var array
*/
private $persistKeys = [];
/**
* Changes keys while updating config in {@link updateWithGlobalConfig()}
* or reading by {@link getConfig()} or {@link getConfigArray()}
*
* @var array
*/
private $cfgUpdateReadMapping = [];
/**
* Key filter for {@link set()}
*
* @var array|null
*/
private $setFilter;
/**
* Instance id (key in $_SESSION array, separate for each server -
* ConfigFile{server id})
*
* @var string
*/
private $id;
/**
* Result for {@link flattenArray()}
*
* @var array|null
*/
private $flattenArrayResult;
/**
* @param array|null $baseConfig base configuration read from
* {@link PhpMyAdmin\Config::$base_config},
* use only when not in PMA Setup
*/
public function __construct($baseConfig = null)
{
// load default config values
$cfg = &$this->defaultCfg;
include ROOT_PATH . 'libraries/config.default.php';
// load additional config information
$this->cfgDb = include ROOT_PATH . 'libraries/config.values.php';
// apply default values overrides
if (count($this->cfgDb['_overrides'])) {
foreach ($this->cfgDb['_overrides'] as $path => $value) {
Core::arrayWrite($path, $cfg, $value);
}
}
$this->baseCfg = $baseConfig;
$this->isInSetup = $baseConfig === null;
$this->id = 'ConfigFile' . $GLOBALS['server'];
if (isset($_SESSION[$this->id])) {
return;
}
$_SESSION[$this->id] = [];
}
/**
* Sets names of config options which will be placed in config file even if
* they are set to their default values (use only full paths)
*
* @param array $keys the names of the config options
*
* @return void
*/
public function setPersistKeys(array $keys)
{
// checking key presence is much faster than searching so move values
// to keys
$this->persistKeys = array_flip($keys);
}
/**
* Returns flipped array set by {@link setPersistKeys()}
*
* @return array
*/
public function getPersistKeysMap()
{
return $this->persistKeys;
}
/**
* By default ConfigFile allows setting of all configuration keys, use
* this method to set up a filter on {@link set()} method
*
* @param array|null $keys array of allowed keys or null to remove filter
*
* @return void
*/
public function setAllowedKeys($keys)
{
if ($keys === null) {
$this->setFilter = null;
return;
}
// checking key presence is much faster than searching so move values
// to keys
$this->setFilter = array_flip($keys);
}
/**
* Sets path mapping for updating config in
* {@link updateWithGlobalConfig()} or reading
* by {@link getConfig()} or {@link getConfigArray()}
*
* @param array $mapping Contains the mapping of "Server/config options"
* to "Server/1/config options"
*
* @return void
*/
public function setCfgUpdateReadMapping(array $mapping)
{
$this->cfgUpdateReadMapping = $mapping;
}
/**
* Resets configuration data
*
* @return void
*/
public function resetConfigData()
{
$_SESSION[$this->id] = [];
}
/**
* Sets configuration data (overrides old data)
*
* @param array $cfg Configuration options
*
* @return void
*/
public function setConfigData(array $cfg)
{
$_SESSION[$this->id] = $cfg;
}
/**
* Sets config value
*
* @param string $path Path
* @param mixed $value Value
* @param string $canonicalPath Canonical path
*
* @return void
*/
public function set($path, $value, $canonicalPath = null)
{
if ($canonicalPath === null) {
$canonicalPath = $this->getCanonicalPath($path);
}
if ($this->setFilter !== null
&& ! isset($this->setFilter[$canonicalPath])
) {
return;
}
// if the path isn't protected it may be removed
if (isset($this->persistKeys[$canonicalPath])) {
Core::arrayWrite($path, $_SESSION[$this->id], $value);
return;
}
$defaultValue = $this->getDefault($canonicalPath);
$removePath = $value === $defaultValue;
if ($this->isInSetup) {
// remove if it has a default value or is empty
$removePath = $removePath
|| (empty($value) && empty($defaultValue));
} else {
// get original config values not overwritten by user
// preferences to allow for overwriting options set in
// config.inc.php with default values
$instanceDefaultValue = Core::arrayRead(
$canonicalPath,
$this->baseCfg
);
// remove if it has a default value and base config (config.inc.php)
// uses default value
$removePath = $removePath
&& ($instanceDefaultValue === $defaultValue);
}
if ($removePath) {
Core::arrayRemove($path, $_SESSION[$this->id]);
return;
}
Core::arrayWrite($path, $_SESSION[$this->id], $value);
}
/**
* Flattens multidimensional array, changes indices to paths
* (eg. 'key/subkey').
* Used as array_walk() callback.
*
* @param mixed $value Value
* @param mixed $key Key
* @param mixed $prefix Prefix
*
* @return void
*/
private function flattenArray($value, $key, $prefix)
{
// no recursion for numeric arrays
if (is_array($value) && ! isset($value[0])) {
$prefix .= $key . '/';
array_walk(
$value,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
$prefix
);
} else {
$this->flattenArrayResult[$prefix . $key] = $value;
}
}
/**
* Returns default config in a flattened array
*
* @return array
*/
public function getFlatDefaultConfig()
{
$this->flattenArrayResult = [];
array_walk(
$this->defaultCfg,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$flatConfig = $this->flattenArrayResult;
$this->flattenArrayResult = null;
return $flatConfig;
}
/**
* Updates config with values read from given array
* (config will contain differences to defaults from config.defaults.php).
*
* @param array $cfg Configuration
*
* @return void
*/
public function updateWithGlobalConfig(array $cfg)
{
// load config array and flatten it
$this->flattenArrayResult = [];
array_walk(
$cfg,
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$flatConfig = $this->flattenArrayResult;
$this->flattenArrayResult = null;
// save values map for translating a few user preferences paths,
// should be complemented by code reading from generated config
// to perform inverse mapping
foreach ($flatConfig as $path => $value) {
if (isset($this->cfgUpdateReadMapping[$path])) {
$path = $this->cfgUpdateReadMapping[$path];
}
$this->set($path, $value, $path);
}
}
/**
* Returns config value or $default if it's not set
*
* @param string $path Path of config file
* @param mixed $default Default values
*
* @return mixed
*/
public function get($path, $default = null)
{
return Core::arrayRead($path, $_SESSION[$this->id], $default);
}
/**
* Returns default config value or $default it it's not set ie. it doesn't
* exist in config.default.php ($cfg) and config.values.php
* ($_cfg_db['_overrides'])
*
* @param string $canonicalPath Canonical path
* @param mixed $default Default value
*
* @return mixed
*/
public function getDefault($canonicalPath, $default = null)
{
return Core::arrayRead($canonicalPath, $this->defaultCfg, $default);
}
/**
* Returns config value, if it's not set uses the default one; returns
* $default if the path isn't set and doesn't contain a default value
*
* @param string $path Path
* @param mixed $default Default value
*
* @return mixed
*/
public function getValue($path, $default = null)
{
$v = Core::arrayRead($path, $_SESSION[$this->id], null);
if ($v !== null) {
return $v;
}
$path = $this->getCanonicalPath($path);
return $this->getDefault($path, $default);
}
/**
* Returns canonical path
*
* @param string $path Path
*
* @return string
*/
public function getCanonicalPath($path)
{
return preg_replace('#^Servers/([\d]+)/#', 'Servers/1/', $path);
}
/**
* Returns config database entry for $path
*
* @param string $path path of the variable in config db
* @param mixed $default default value
*
* @return mixed
*/
public function getDbEntry($path, $default = null)
{
return Core::arrayRead($path, $this->cfgDb, $default);
}
/**
* Returns server count
*
* @return int
*/
public function getServerCount()
{
return isset($_SESSION[$this->id]['Servers'])
? count($_SESSION[$this->id]['Servers'])
: 0;
}
/**
* Returns server list
*
* @return array|null
*/
public function getServers()
{
return $_SESSION[$this->id]['Servers'] ?? null;
}
/**
* Returns DSN of given server
*
* @param int $server server index
*
* @return string
*/
public function getServerDSN($server)
{
if (! isset($_SESSION[$this->id]['Servers'][$server])) {
return '';
}
$path = 'Servers/' . $server;
$dsn = 'mysqli://';
if ($this->getValue($path . '/auth_type') === 'config') {
$dsn .= $this->getValue($path . '/user');
if (! empty($this->getValue($path . '/password'))) {
$dsn .= ':***';
}
$dsn .= '@';
}
if ($this->getValue($path . '/host') !== 'localhost') {
$dsn .= $this->getValue($path . '/host');
$port = $this->getValue($path . '/port');
if ($port) {
$dsn .= ':' . $port;
}
} else {
$dsn .= $this->getValue($path . '/socket');
}
return $dsn;
}
/**
* Returns server name
*
* @param int $id server index
*
* @return string
*/
public function getServerName($id)
{
if (! isset($_SESSION[$this->id]['Servers'][$id])) {
return '';
}
$verbose = $this->get('Servers/' . $id . '/verbose');
if (! empty($verbose)) {
return $verbose;
}
$host = $this->get('Servers/' . $id . '/host');
return empty($host) ? 'localhost' : $host;
}
/**
* Removes server
*
* @param int $server server index
*
* @return void
*/
public function removeServer($server)
{
if (! isset($_SESSION[$this->id]['Servers'][$server])) {
return;
}
$lastServer = $this->getServerCount();
for ($i = $server; $i < $lastServer; $i++) {
$_SESSION[$this->id]['Servers'][$i]
= $_SESSION[$this->id]['Servers'][$i + 1];
}
unset($_SESSION[$this->id]['Servers'][$lastServer]);
if (! isset($_SESSION[$this->id]['ServerDefault'])
|| $_SESSION[$this->id]['ServerDefault'] != $lastServer
) {
return;
}
unset($_SESSION[$this->id]['ServerDefault']);
}
/**
* Returns configuration array (full, multidimensional format)
*
* @return array
*/
public function getConfig()
{
$c = $_SESSION[$this->id];
foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
// if the key $c exists in $map_to
if (Core::arrayRead($mapTo, $c) === null) {
continue;
}
Core::arrayWrite($mapTo, $c, Core::arrayRead($mapFrom, $c));
Core::arrayRemove($mapFrom, $c);
}
return $c;
}
/**
* Returns configuration array (flat format)
*
* @return array
*/
public function getConfigArray()
{
$this->flattenArrayResult = [];
array_walk(
$_SESSION[$this->id],
function ($value, $key, $prefix) {
$this->flattenArray($value, $key, $prefix);
},
''
);
$c = $this->flattenArrayResult;
$this->flattenArrayResult = null;
$persistKeys = array_diff(
array_keys($this->persistKeys),
array_keys($c)
);
foreach ($persistKeys as $k) {
$c[$k] = $this->getDefault($this->getCanonicalPath($k));
}
foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
if (! isset($c[$mapFrom])) {
continue;
}
$c[$mapTo] = $c[$mapFrom];
unset($c[$mapFrom]);
}
return $c;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,310 @@
<?php
/**
* Form handling code.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use const E_USER_ERROR;
use function array_combine;
use function array_shift;
use function array_walk;
use function count;
use function gettype;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function ltrim;
use function mb_strpos;
use function mb_strrpos;
use function mb_substr;
use function str_replace;
use function trigger_error;
/**
* Base class for forms, loads default configuration options, checks allowed
* values etc.
*/
class Form
{
/**
* Form name
*
* @var string
*/
public $name;
/**
* Arbitrary index, doesn't affect class' behavior
*
* @var int
*/
public $index;
/**
* Form fields (paths), filled by {@link readFormPaths()}, indexed by field name
*
* @var array
*/
public $fields;
/**
* Stores default values for some fields (eg. pmadb tables)
*
* @var array
*/
public $default;
/**
* Caches field types, indexed by field names
*
* @var array
*/
private $fieldsTypes;
/**
* ConfigFile instance
*
* @var ConfigFile
*/
private $configFile;
/**
* A counter for the number of groups
*
* @var int
*/
private static $groupCounter = 0;
/**
* Reads default config values
*
* @param string $formName Form name
* @param array $form Form data
* @param ConfigFile $cf Config file instance
* @param int $index arbitrary index, stored in Form::$index
*/
public function __construct(
$formName,
array $form,
ConfigFile $cf,
$index = null
) {
$this->index = $index;
$this->configFile = $cf;
$this->loadForm($formName, $form);
}
/**
* Returns type of given option
*
* @param string $optionName path or field name
*
* @return string|null one of: boolean, integer, double, string, select, array
*/
public function getOptionType($optionName)
{
$key = ltrim(
mb_substr(
$optionName,
(int) mb_strrpos($optionName, '/')
),
'/'
);
return $this->fieldsTypes[$key] ?? null;
}
/**
* Returns allowed values for select fields
*
* @param string $optionPath Option path
*
* @return array
*/
public function getOptionValueList($optionPath)
{
$value = $this->configFile->getDbEntry($optionPath);
if ($value === null) {
trigger_error($optionPath . ' - select options not defined', E_USER_ERROR);
return [];
}
if (! is_array($value)) {
trigger_error($optionPath . ' - not a static value list', E_USER_ERROR);
return [];
}
// convert array('#', 'a', 'b') to array('a', 'b')
if (isset($value[0]) && $value[0] === '#') {
// remove first element ('#')
array_shift($value);
// $value has keys and value names, return it
return $value;
}
// convert value list array('a', 'b') to array('a' => 'a', 'b' => 'b')
$hasStringKeys = false;
$keys = [];
for ($i = 0, $nb = count($value); $i < $nb; $i++) {
if (! isset($value[$i])) {
$hasStringKeys = true;
break;
}
$keys[] = is_bool($value[$i]) ? (int) $value[$i] : $value[$i];
}
if (! $hasStringKeys) {
$value = array_combine($keys, $value);
}
// $value has keys and value names, return it
return $value;
}
/**
* array_walk callback function, reads path of form fields from
* array (see docs for \PhpMyAdmin\Config\Forms\BaseForm::getForms)
*
* @param mixed $value Value
* @param mixed $key Key
* @param mixed $prefix Prefix
*
* @return void
*/
private function readFormPathsCallback($value, $key, $prefix)
{
if (is_array($value)) {
$prefix .= $key . '/';
array_walk(
$value,
function ($value, $key, $prefix) {
$this->readFormPathsCallback($value, $key, $prefix);
},
$prefix
);
return;
}
if (! is_int($key)) {
$this->default[$prefix . $key] = $value;
$value = $key;
}
// add unique id to group ends
if ($value === ':group:end') {
$value .= ':' . self::$groupCounter++;
}
$this->fields[] = $prefix . $value;
}
/**
* Reset the group counter, function for testing purposes
*/
public static function resetGroupCounter(): void
{
self::$groupCounter = 0;
}
/**
* Reads form paths to {@link $fields}
*
* @param array $form Form
*
* @return void
*/
protected function readFormPaths(array $form)
{
// flatten form fields' paths and save them to $fields
$this->fields = [];
array_walk(
$form,
function ($value, $key, $prefix) {
$this->readFormPathsCallback($value, $key, $prefix);
},
''
);
// $this->fields is an array of the form: [0..n] => 'field path'
// change numeric indexes to contain field names (last part of the path)
$paths = $this->fields;
$this->fields = [];
foreach ($paths as $path) {
$key = ltrim(
mb_substr($path, (int) mb_strrpos($path, '/')),
'/'
);
$this->fields[$key] = $path;
}
// now $this->fields is an array of the form: 'field name' => 'field path'
}
/**
* Reads fields' types to $this->fieldsTypes
*
* @return void
*/
protected function readTypes()
{
$cf = $this->configFile;
foreach ($this->fields as $name => $path) {
if (mb_strpos((string) $name, ':group:') === 0) {
$this->fieldsTypes[$name] = 'group';
continue;
}
$v = $cf->getDbEntry($path);
if ($v !== null) {
$type = is_array($v) ? 'select' : $v;
} else {
$type = gettype($cf->getDefault($path));
}
$this->fieldsTypes[$name] = $type;
}
}
/**
* Remove slashes from group names
*
* @see issue #15836
*
* @param array $form The form data
*
* @return array
*/
protected function cleanGroupPaths(array $form): array
{
foreach ($form as &$name) {
if (! is_string($name)) {
continue;
}
if (mb_strpos($name, ':group:') !== 0) {
continue;
}
$name = str_replace('/', '-', $name);
}
return $form;
}
/**
* Reads form settings and prepares class to work with given subset of
* config file
*
* @param string $formName Form name
* @param array $form Form
*
* @return void
*/
public function loadForm($formName, array $form)
{
$this->name = $formName;
$form = $this->cleanGroupPaths($form);
$this->readFormPaths($form);
$this->readTypes();
}
}

View file

@ -0,0 +1,967 @@
<?php
/**
* Form management class, displays and processes forms
*
* Explanation of used terms:
* o work_path - original field path, eg. Servers/4/verbose
* o system_path - work_path modified so that it points to the first server,
* eg. Servers/1/verbose
* o translated_path - work_path modified for HTML field name, a path with
* slashes changed to hyphens, eg. Servers-4-verbose
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config\Forms\User\UserFormList;
use PhpMyAdmin\Html\MySQLDocumentation;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Util;
use const E_USER_WARNING;
use function array_flip;
use function array_keys;
use function array_search;
use function count;
use function explode;
use function function_exists;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_numeric;
use function mb_substr;
use function preg_match;
use function settype;
use function sprintf;
use function str_replace;
use function trigger_error;
use function trim;
/**
* Form management class, displays and processes forms
*/
class FormDisplay
{
/**
* ConfigFile instance
*
* @var ConfigFile
*/
private $configFile;
/**
* Form list
*
* @var Form[]
*/
private $forms = [];
/**
* Stores validation errors, indexed by paths
* [ Form_name ] is an array of form errors
* [path] is a string storing error associated with single field
*
* @var array
*/
private $errors = [];
/**
* Paths changed so that they can be used as HTML ids, indexed by paths
*
* @var array
*/
private $translatedPaths = [];
/**
* Server paths change indexes so we define maps from current server
* path to the first one, indexed by work path
*
* @var array
*/
private $systemPaths = [];
/**
* Language strings which will be sent to Messages JS variable
* Will be looked up in $GLOBALS: str{value} or strSetup{value}
*
* @var array
*/
private $jsLangStrings = [];
/**
* Tells whether forms have been validated
*
* @var bool
*/
private $isValidated = true;
/**
* Dictionary with user preferences keys
*
* @var array|null
*/
private $userprefsKeys;
/**
* Dictionary with disallowed user preferences keys
*
* @var array
*/
private $userprefsDisallow;
/** @var FormDisplayTemplate */
private $formDisplayTemplate;
/**
* @param ConfigFile $cf Config file instance
*/
public function __construct(ConfigFile $cf)
{
$this->formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']);
$this->jsLangStrings = [
'error_nan_p' => __('Not a positive number!'),
'error_nan_nneg' => __('Not a non-negative number!'),
'error_incorrect_port' => __('Not a valid port number!'),
'error_invalid_value' => __('Incorrect value!'),
'error_value_lte' => __('Value must be less than or equal to %s!'),
];
$this->configFile = $cf;
// initialize validators
Validator::getValidators($this->configFile);
}
/**
* Returns {@link ConfigFile} associated with this instance
*
* @return ConfigFile
*/
public function getConfigFile()
{
return $this->configFile;
}
/**
* Registers form in form manager
*
* @param string $formName Form name
* @param array $form Form data
* @param int $serverId 0 if new server, validation; >= 1 if editing a server
*
* @return void
*/
public function registerForm($formName, array $form, $serverId = null)
{
$this->forms[$formName] = new Form(
$formName,
$form,
$this->configFile,
$serverId
);
$this->isValidated = false;
foreach ($this->forms[$formName]->fields as $path) {
$workPath = $serverId === null
? $path
: str_replace('Servers/1/', 'Servers/' . $serverId . '/', $path);
$this->systemPaths[$workPath] = $path;
$this->translatedPaths[$workPath] = str_replace('/', '-', $workPath);
}
}
/**
* Processes forms, returns true on successful save
*
* @param bool $allowPartialSave allows for partial form saving
* on failed validation
* @param bool $checkFormSubmit whether check for $_POST['submit_save']
*
* @return bool whether processing was successful
*/
public function process($allowPartialSave = true, $checkFormSubmit = true)
{
if ($checkFormSubmit && ! isset($_POST['submit_save'])) {
return false;
}
// save forms
if (count($this->forms) > 0) {
return $this->save(array_keys($this->forms), $allowPartialSave);
}
return false;
}
/**
* Runs validation for all registered forms
*
* @return void
*/
private function validate()
{
if ($this->isValidated) {
return;
}
$paths = [];
$values = [];
foreach ($this->forms as $form) {
/** @var Form $form */
$paths[] = $form->name;
// collect values and paths
foreach ($form->fields as $path) {
$workPath = array_search($path, $this->systemPaths);
$values[$path] = $this->configFile->getValue($workPath);
$paths[] = $path;
}
}
// run validation
$errors = Validator::validate(
$this->configFile,
$paths,
$values,
false
);
// change error keys from canonical paths to work paths
if (is_array($errors) && count($errors) > 0) {
$this->errors = [];
foreach ($errors as $path => $errorList) {
$workPath = array_search($path, $this->systemPaths);
// field error
if (! $workPath) {
// form error, fix path
$workPath = $path;
}
$this->errors[$workPath] = $errorList;
}
}
$this->isValidated = true;
}
/**
* Outputs HTML for the forms under the menu tab
*
* @param bool $showRestoreDefault whether to show "restore default"
* button besides the input field
* @param array $jsDefault stores JavaScript code
* to be displayed
* @param array $js will be updated with javascript code
* @param bool $showButtons whether show submit and reset button
*
* @return string
*/
private function displayForms(
$showRestoreDefault,
array &$jsDefault,
array &$js,
$showButtons
) {
$htmlOutput = '';
$validators = Validator::getValidators($this->configFile);
foreach ($this->forms as $form) {
/** @var Form $form */
$formErrors = $this->errors[$form->name] ?? null;
$htmlOutput .= $this->formDisplayTemplate->displayFieldsetTop(
Descriptions::get('Form_' . $form->name),
Descriptions::get('Form_' . $form->name, 'desc'),
$formErrors,
['id' => $form->name]
);
foreach ($form->fields as $field => $path) {
$workPath = array_search($path, $this->systemPaths);
$translatedPath = $this->translatedPaths[$workPath];
// always true/false for user preferences display
// otherwise null
$userPrefsAllow = isset($this->userprefsKeys[$path])
? ! isset($this->userprefsDisallow[$path])
: null;
// display input
$htmlOutput .= $this->displayFieldInput(
$form,
$field,
$path,
$workPath,
$translatedPath,
$showRestoreDefault,
$userPrefsAllow,
$jsDefault
);
// register JS validators for this field
if (! isset($validators[$path])) {
continue;
}
$this->formDisplayTemplate->addJsValidate($translatedPath, $validators[$path], $js);
}
$htmlOutput .= $this->formDisplayTemplate->displayFieldsetBottom($showButtons);
}
return $htmlOutput;
}
/**
* Outputs HTML for forms
*
* @param bool $tabbedForm if true, use a form with tabs
* @param bool $showRestoreDefault whether show "restore default" button
* besides the input field
* @param bool $showButtons whether show submit and reset button
* @param string $formAction action attribute for the form
* @param array|null $hiddenFields array of form hidden fields (key: field
* name)
*
* @return string HTML for forms
*/
public function getDisplay(
$tabbedForm = false,
$showRestoreDefault = false,
$showButtons = true,
$formAction = null,
$hiddenFields = null
) {
static $jsLangSent = false;
$htmlOutput = '';
$js = [];
$jsDefault = [];
$htmlOutput .= $this->formDisplayTemplate->displayFormTop($formAction, 'post', $hiddenFields);
if ($tabbedForm) {
$tabs = [];
foreach ($this->forms as $form) {
$tabs[$form->name] = Descriptions::get('Form_' . $form->name);
}
$htmlOutput .= $this->formDisplayTemplate->displayTabsTop($tabs);
}
// validate only when we aren't displaying a "new server" form
$isNewServer = false;
foreach ($this->forms as $form) {
/** @var Form $form */
if ($form->index === 0) {
$isNewServer = true;
break;
}
}
if (! $isNewServer) {
$this->validate();
}
// user preferences
$this->loadUserprefsInfo();
// display forms
$htmlOutput .= $this->displayForms(
$showRestoreDefault,
$jsDefault,
$js,
$showButtons
);
if ($tabbedForm) {
$htmlOutput .= $this->formDisplayTemplate->displayTabsBottom();
}
$htmlOutput .= $this->formDisplayTemplate->displayFormBottom();
// if not already done, send strings used for validation to JavaScript
if (! $jsLangSent) {
$jsLangSent = true;
$jsLang = [];
foreach ($this->jsLangStrings as $strName => $strValue) {
$jsLang[] = "'" . $strName . "': '" . Sanitize::jsFormat($strValue, false) . '\'';
}
$js[] = "$.extend(Messages, {\n\t"
. implode(",\n\t", $jsLang) . '})';
}
$js[] = "$.extend(defaultValues, {\n\t"
. implode(",\n\t", $jsDefault) . '})';
return $htmlOutput . $this->formDisplayTemplate->displayJavascript($js);
}
/**
* Prepares data for input field display and outputs HTML code
*
* @param Form $form Form object
* @param string $field field name as it appears in $form
* @param string $systemPath field path, eg. Servers/1/verbose
* @param string $workPath work path, eg. Servers/4/verbose
* @param string $translatedPath work path changed so that it can be
* used as XHTML id
* @param bool $showRestoreDefault whether show "restore default" button
* besides the input field
* @param bool|null $userPrefsAllow whether user preferences are enabled
* for this field (null - no support,
* true/false - enabled/disabled)
* @param array $jsDefault array which stores JavaScript code
* to be displayed
*
* @return string|null HTML for input field
*/
private function displayFieldInput(
Form $form,
$field,
$systemPath,
$workPath,
$translatedPath,
$showRestoreDefault,
$userPrefsAllow,
array &$jsDefault
) {
$name = Descriptions::get($systemPath);
$description = Descriptions::get($systemPath, 'desc');
$value = $this->configFile->get($workPath);
$valueDefault = $this->configFile->getDefault($systemPath);
$valueIsDefault = false;
if ($value === null || $value === $valueDefault) {
$value = $valueDefault;
$valueIsDefault = true;
}
$opts = [
'doc' => $this->getDocLink($systemPath),
'show_restore_default' => $showRestoreDefault,
'userprefs_allow' => $userPrefsAllow,
'userprefs_comment' => Descriptions::get($systemPath, 'cmt'),
];
if (isset($form->default[$systemPath])) {
$opts['setvalue'] = (string) $form->default[$systemPath];
}
if (isset($this->errors[$workPath])) {
$opts['errors'] = $this->errors[$workPath];
}
$type = '';
switch ($form->getOptionType($field)) {
case 'string':
$type = 'text';
break;
case 'short_string':
$type = 'short_text';
break;
case 'double':
case 'integer':
$type = 'number_text';
break;
case 'boolean':
$type = 'checkbox';
break;
case 'select':
$type = 'select';
$opts['values'] = $form->getOptionValueList($form->fields[$field]);
break;
case 'array':
$type = 'list';
$value = (array) $value;
$valueDefault = (array) $valueDefault;
break;
case 'group':
// :group:end is changed to :group:end:{unique id} in Form class
$htmlOutput = '';
if (mb_substr($field, 7, 4) !== 'end:') {
$htmlOutput .= $this->formDisplayTemplate->displayGroupHeader(
mb_substr($field, 7)
);
} else {
$this->formDisplayTemplate->displayGroupFooter();
}
return $htmlOutput;
case 'NULL':
trigger_error('Field ' . $systemPath . ' has no type', E_USER_WARNING);
return null;
}
// detect password fields
if ($type === 'text'
&& (mb_substr($translatedPath, -9) === '-password'
|| mb_substr($translatedPath, -4) === 'pass'
|| mb_substr($translatedPath, -4) === 'Pass')
) {
$type = 'password';
}
// TrustedProxies requires changes before displaying
if ($systemPath === 'TrustedProxies') {
foreach ($value as $ip => &$v) {
if (preg_match('/^-\d+$/', $ip)) {
continue;
}
$v = $ip . ': ' . $v;
}
}
$this->setComments($systemPath, $opts);
// send default value to form's JS
$jsLine = '\'' . $translatedPath . '\': ';
switch ($type) {
case 'text':
case 'short_text':
case 'number_text':
case 'password':
$jsLine .= '\'' . Sanitize::escapeJsString($valueDefault) . '\'';
break;
case 'checkbox':
$jsLine .= $valueDefault ? 'true' : 'false';
break;
case 'select':
$valueDefaultJs = is_bool($valueDefault)
? (int) $valueDefault
: $valueDefault;
$jsLine .= '[\'' . Sanitize::escapeJsString($valueDefaultJs) . '\']';
break;
case 'list':
$val = $valueDefault;
if (isset($val['wrapper_params'])) {
unset($val['wrapper_params']);
}
$jsLine .= '\'' . Sanitize::escapeJsString(implode("\n", $val))
. '\'';
break;
}
$jsDefault[] = $jsLine;
return $this->formDisplayTemplate->displayInput(
$translatedPath,
$name,
$type,
$value,
$description,
$valueIsDefault,
$opts
);
}
/**
* Displays errors
*
* @return string|null HTML for errors
*/
public function displayErrors()
{
$this->validate();
if (count($this->errors) === 0) {
return null;
}
$htmlOutput = '';
foreach ($this->errors as $systemPath => $errorList) {
if (isset($this->systemPaths[$systemPath])) {
$name = Descriptions::get($this->systemPaths[$systemPath]);
} else {
$name = Descriptions::get('Form_' . $systemPath);
}
$htmlOutput .= $this->formDisplayTemplate->displayErrors($name, $errorList);
}
return $htmlOutput;
}
/**
* Reverts erroneous fields to their default values
*
* @return void
*/
public function fixErrors()
{
$this->validate();
if (count($this->errors) === 0) {
return;
}
$cf = $this->configFile;
foreach (array_keys($this->errors) as $workPath) {
if (! isset($this->systemPaths[$workPath])) {
continue;
}
$canonicalPath = $this->systemPaths[$workPath];
$cf->set($workPath, $cf->getDefault($canonicalPath));
}
}
/**
* Validates select field and casts $value to correct type
*
* @param string|bool $value Current value
* @param array $allowed List of allowed values
*/
private function validateSelect(&$value, array $allowed): bool
{
$valueCmp = is_bool($value)
? (int) $value
: $value;
foreach ($allowed as $vk => $v) {
// equality comparison only if both values are numeric or not numeric
// (allows to skip 0 == 'string' equalling to true)
// or identity (for string-string)
if (! (($vk == $value && ! (is_numeric($valueCmp) xor is_numeric($vk)))
|| $vk === $value)
) {
continue;
}
// keep boolean value as boolean
if (! is_bool($value)) {
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($value, gettype($vk));
}
return true;
}
return false;
}
/**
* Validates and saves form data to session
*
* @param array|string $forms array of form names
* @param bool $allowPartialSave allows for partial form saving on
* failed validation
*
* @return bool true on success (no errors and all saved)
*/
public function save($forms, $allowPartialSave = true)
{
$result = true;
$forms = (array) $forms;
$values = [];
$toSave = [];
$isSetupScript = $GLOBALS['PMA_Config']->get('is_setup');
if ($isSetupScript) {
$this->loadUserprefsInfo();
}
$this->errors = [];
foreach ($forms as $formName) {
if (! isset($this->forms[$formName])) {
continue;
}
/** @var Form $form */
$form = $this->forms[$formName];
// get current server id
$changeIndex = $form->index === 0
? $this->configFile->getServerCount() + 1
: false;
// grab POST values
foreach ($form->fields as $field => $systemPath) {
$workPath = array_search($systemPath, $this->systemPaths);
$key = $this->translatedPaths[$workPath];
$type = (string) $form->getOptionType($field);
// skip groups
if ($type === 'group') {
continue;
}
// ensure the value is set
if (! isset($_POST[$key])) {
// checkboxes aren't set by browsers if they're off
if ($type !== 'boolean') {
$this->errors[$form->name][] = sprintf(
__('Missing data for %s'),
'<i>' . Descriptions::get($systemPath) . '</i>'
);
$result = false;
continue;
}
$_POST[$key] = false;
}
// user preferences allow/disallow
if ($isSetupScript
&& isset($this->userprefsKeys[$systemPath])
) {
if (isset($this->userprefsDisallow[$systemPath], $_POST[$key . '-userprefs-allow'])
) {
unset($this->userprefsDisallow[$systemPath]);
} elseif (! isset($_POST[$key . '-userprefs-allow'])) {
$this->userprefsDisallow[$systemPath] = true;
}
}
// cast variables to correct type
switch ($type) {
case 'double':
$_POST[$key] = Util::requestString($_POST[$key]);
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($_POST[$key], 'float');
break;
case 'boolean':
case 'integer':
if ($_POST[$key] !== '') {
$_POST[$key] = Util::requestString($_POST[$key]);
// phpcs:ignore Generic.PHP.ForbiddenFunctions
settype($_POST[$key], $type);
}
break;
case 'select':
$successfullyValidated = $this->validateSelect(
$_POST[$key],
$form->getOptionValueList($systemPath)
);
if (! $successfullyValidated) {
$this->errors[$workPath][] = __('Incorrect value!');
$result = false;
// "continue" for the $form->fields foreach-loop
continue 2;
}
break;
case 'string':
case 'short_string':
$_POST[$key] = Util::requestString($_POST[$key]);
break;
case 'array':
// eliminate empty values and ensure we have an array
$postValues = is_array($_POST[$key])
? $_POST[$key]
: explode("\n", $_POST[$key]);
$_POST[$key] = [];
$this->fillPostArrayParameters($postValues, $key);
break;
}
// now we have value with proper type
$values[$systemPath] = $_POST[$key];
if ($changeIndex !== false) {
$workPath = str_replace(
'Servers/' . $form->index . '/',
'Servers/' . $changeIndex . '/',
$workPath
);
}
$toSave[$workPath] = $systemPath;
}
}
// save forms
if (! $allowPartialSave && ! empty($this->errors)) {
// don't look for non-critical errors
$this->validate();
return $result;
}
foreach ($toSave as $workPath => $path) {
// TrustedProxies requires changes before saving
if ($path === 'TrustedProxies') {
$proxies = [];
$i = 0;
foreach ($values[$path] as $value) {
$matches = [];
$match = preg_match(
'/^(.+):(?:[ ]?)(\\w+)$/',
$value,
$matches
);
if ($match) {
// correct 'IP: HTTP header' pair
$ip = trim($matches[1]);
$proxies[$ip] = trim($matches[2]);
} else {
// save also incorrect values
$proxies['-' . $i] = $value;
$i++;
}
}
$values[$path] = $proxies;
}
$this->configFile->set($workPath, $values[$path], $path);
}
if ($isSetupScript) {
$this->configFile->set(
'UserprefsDisallow',
array_keys($this->userprefsDisallow)
);
}
// don't look for non-critical errors
$this->validate();
return $result;
}
/**
* Tells whether form validation failed
*
* @return bool
*/
public function hasErrors()
{
return count($this->errors) > 0;
}
/**
* Returns link to documentation
*
* @param string $path Path to documentation
*
* @return string
*/
public function getDocLink($path)
{
$test = mb_substr($path, 0, 6);
if ($test === 'Import' || $test === 'Export') {
return '';
}
return MySQLDocumentation::getDocumentationLink(
'config',
'cfg_' . $this->getOptName($path),
Sanitize::isSetup() ? '../' : './'
);
}
/**
* Changes path so it can be used in URLs
*
* @param string $path Path
*
* @return string
*/
private function getOptName($path)
{
return str_replace(['Servers/1/', '/'], ['Servers/', '_'], $path);
}
/**
* Fills out {@link userprefs_keys} and {@link userprefs_disallow}
*
* @return void
*/
private function loadUserprefsInfo()
{
if ($this->userprefsKeys !== null) {
return;
}
$this->userprefsKeys = array_flip(UserFormList::getFields());
// read real config for user preferences display
$userPrefsDisallow = $GLOBALS['PMA_Config']->get('is_setup')
? $this->configFile->get('UserprefsDisallow', [])
: $GLOBALS['cfg']['UserprefsDisallow'];
$this->userprefsDisallow = array_flip($userPrefsDisallow ?? []);
}
/**
* Sets field comments and warnings based on current environment
*
* @param string $systemPath Path to settings
* @param array $opts Chosen options
*
* @return void
*/
private function setComments($systemPath, array &$opts)
{
// RecodingEngine - mark unavailable types
if ($systemPath === 'RecodingEngine') {
$comment = '';
if (! function_exists('iconv')) {
$opts['values']['iconv'] .= ' (' . __('unavailable') . ')';
$comment = sprintf(
__('"%s" requires %s extension'),
'iconv',
'iconv'
);
}
if (! function_exists('recode_string')) {
$opts['values']['recode'] .= ' (' . __('unavailable') . ')';
$comment .= ($comment ? ', ' : '') . sprintf(
__('"%s" requires %s extension'),
'recode',
'recode'
);
}
/* mbstring is always there thanks to polyfill */
$opts['comment'] = $comment;
$opts['comment_warning'] = true;
}
// ZipDump, GZipDump, BZipDump - check function availability
if ($systemPath === 'ZipDump'
|| $systemPath === 'GZipDump'
|| $systemPath === 'BZipDump'
) {
$comment = '';
$funcs = [
'ZipDump' => [
'zip_open',
'gzcompress',
],
'GZipDump' => [
'gzopen',
'gzencode',
],
'BZipDump' => [
'bzopen',
'bzcompress',
],
];
if (! function_exists($funcs[$systemPath][0])) {
$comment = sprintf(
__(
'Compressed import will not work due to missing function %s.'
),
$funcs[$systemPath][0]
);
}
if (! function_exists($funcs[$systemPath][1])) {
$comment .= ($comment ? '; ' : '') . sprintf(
__(
'Compressed export will not work due to missing function %s.'
),
$funcs[$systemPath][1]
);
}
$opts['comment'] = $comment;
$opts['comment_warning'] = true;
}
if ($GLOBALS['PMA_Config']->get('is_setup')) {
return;
}
if ($systemPath !== 'MaxDbList' && $systemPath !== 'MaxTableList'
&& $systemPath !== 'QueryHistoryMax'
) {
return;
}
$opts['comment'] = sprintf(
__('maximum %s'),
$GLOBALS['cfg'][$systemPath]
);
}
/**
* Copy items of an array to $_POST variable
*
* @param array $postValues List of parameters
* @param string $key Array key
*
* @return void
*/
private function fillPostArrayParameters(array $postValues, $key)
{
foreach ($postValues as $v) {
$v = Util::requestString($v);
if ($v === '') {
continue;
}
$_POST[$key][] = $v;
}
}
}

View file

@ -0,0 +1,491 @@
<?php
/**
* Form templates
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Template;
use function array_flip;
use function array_merge;
use function array_shift;
use function defined;
use function htmlspecialchars;
use function htmlspecialchars_decode;
use function implode;
use function is_bool;
use function mb_strtolower;
use function sprintf;
use function is_string;
/**
* PhpMyAdmin\Config\FormDisplayTemplate class
*/
class FormDisplayTemplate
{
/** @var int */
public $group;
/** @var Config */
protected $config;
/** @var Template */
public $template;
/**
* @param Config $config Config instance
*/
public function __construct(Config $config)
{
$this->config = $config;
$this->template = new Template();
}
/**
* Displays top part of the form
*
* @param string $action default: $_SERVER['REQUEST_URI']
* @param string $method 'post' or 'get'
* @param array|null $hiddenFields array of form hidden fields (key: field name)
*/
public function displayFormTop(
$action = null,
$method = 'post',
$hiddenFields = null
): string {
static $hasCheckPageRefresh = false;
if ($action === null) {
$action = $_SERVER['REQUEST_URI'];
}
if ($method !== 'post') {
$method = 'get';
}
/**
* We do validation on page refresh when browser remembers field values,
* add a field with known value which will be used for checks.
*/
if (! $hasCheckPageRefresh) {
$hasCheckPageRefresh = true;
}
return $this->template->render('config/form_display/form_top', [
'method' => $method,
'action' => $action,
'has_check_page_refresh' => $hasCheckPageRefresh,
'hidden_fields' => (array) $hiddenFields,
]);
}
/**
* Displays form tabs which are given by an array indexed by fieldset id
* ({@link self::displayFieldsetTop}), with values being tab titles.
*
* @param array $tabs tab names
*/
public function displayTabsTop(array $tabs): string
{
return $this->template->render('config/form_display/tabs_top', ['tabs' => $tabs]);
}
/**
* Displays top part of a fieldset
*
* @param string $title title of fieldset
* @param string $description description shown on top of fieldset
* @param array|null $errors error messages to display
* @param array $attributes optional extra attributes of fieldset
*/
public function displayFieldsetTop(
$title = '',
$description = '',
$errors = null,
array $attributes = []
): string {
$this->group = 0;
$attributes = array_merge(['class' => 'optbox'], $attributes);
return $this->template->render('config/form_display/fieldset_top', [
'attributes' => $attributes,
'title' => $title,
'description' => $description,
'errors' => $errors,
]);
}
/**
* Displays input field
*
* $opts keys:
* o doc - (string) documentation link
* o errors - error array
* o setvalue - (string) shows button allowing to set predefined value
* o show_restore_default - (boolean) whether show "restore default" button
* o userprefs_allow - whether user preferences are enabled for this field
* (null - no support, true/false - enabled/disabled)
* o userprefs_comment - (string) field comment
* o values - key - value pairs for <select> fields
* o values_escaped - (boolean) tells whether values array is already escaped
* (defaults to false)
* o values_disabled - (array)list of disabled values (keys from values)
* o comment - (string) tooltip comment
* o comment_warning - (bool) whether this comments warns about something
*
* @param string $path config option path
* @param string $name config option name
* @param string $type type of config option
* @param mixed $value current value
* @param string $description verbose description
* @param bool $valueIsDefault whether value is default
* @param array|null $opts see above description
*/
public function displayInput(
$path,
$name,
$type,
$value,
$description = '',
$valueIsDefault = true,
$opts = null
): string {
static $icons; // An array of IMG tags used further below in the function
if (defined('TESTSUITE')) {
$icons = null;
}
$isSetupScript = $this->config->get('is_setup');
if ($icons === null) { // if the static variables have not been initialised
$icons = [];
// Icon definitions:
// The same indexes will be used in the $icons array.
// The first element contains the filename and the second
// element is used for the "alt" and "title" attributes.
$iconInit = [
'edit' => [
'b_edit',
'',
],
'help' => [
'b_help',
__('Documentation'),
],
'reload' => [
's_reload',
'',
],
'tblops' => [
'b_tblops',
'',
],
];
if ($isSetupScript) {
// When called from the setup script, we don't have access to the
// sprite-aware getImage() function because the PMA_theme class
// has not been loaded, so we generate the img tags manually.
foreach ($iconInit as $k => $v) {
$title = '';
if (! empty($v[1])) {
$title = ' title="' . $v[1] . '"';
}
$icons[$k] = sprintf(
'<img alt="%s" src="%s"%s>',
$v[1],
'../themes/pmahomme/img/' . $v[0] . '.png',
$title
);
}
} else {
// In this case we just use getImage() because it's available
foreach ($iconInit as $k => $v) {
$icons[$k] = Generator::getImage(
$v[0],
$v[1]
);
}
}
}
$hasErrors = isset($opts['errors']) && ! empty($opts['errors']);
$optionIsDisabled = ! $isSetupScript && isset($opts['userprefs_allow'])
&& ! $opts['userprefs_allow'];
$nameId = 'name="' . htmlspecialchars($path) . '" id="'
. htmlspecialchars($path) . '"';
$fieldClass = $type === 'checkbox' ? 'checkbox' : '';
if (! $valueIsDefault) {
$fieldClass .= ($fieldClass == '' ? '' : ' ')
. ($hasErrors ? 'custom field-error' : 'custom');
}
$fieldClass = $fieldClass ? ' class="' . $fieldClass . '"' : '';
$trClass = $this->group > 0
? 'group-field group-field-' . $this->group
: '';
if (isset($opts['setvalue']) && $opts['setvalue'] === ':group') {
unset($opts['setvalue']);
$this->group++;
$trClass = 'group-header-field group-header-' . $this->group;
}
if ($optionIsDisabled) {
$trClass .= ($trClass ? ' ' : '') . 'disabled-field';
}
$trClass = $trClass ? ' class="' . $trClass . '"' : '';
$htmlOutput = '<tr' . $trClass . '>';
$htmlOutput .= '<th>';
$htmlOutput .= '<label for="' . htmlspecialchars($path) . '">' . htmlspecialchars_decode($name)
. '</label>';
if (! empty($opts['doc'])) {
$htmlOutput .= '<span class="doc">';
$htmlOutput .= '<a href="' . $opts['doc']
. '" target="documentation">' . $icons['help'] . '</a>';
$htmlOutput .= "\n";
$htmlOutput .= '</span>';
}
if ($optionIsDisabled) {
$htmlOutput .= '<span class="disabled-notice" title="';
$htmlOutput .= __(
'This setting is disabled, it will not be applied to your configuration.'
);
$htmlOutput .= '">' . __('Disabled') . '</span>';
}
if (! empty($description)) {
$htmlOutput .= '<small>' . $description . '</small>';
}
$htmlOutput .= '</th>';
$htmlOutput .= '<td>';
switch ($type) {
case 'text':
$htmlOutput .= '<input type="text" class="w-75" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars($value) . '">';
break;
case 'password':
$htmlOutput .= '<input type="password" class="w-75" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars($value) . '">';
break;
case 'short_text':
// As seen in the reporting server (#15042) we sometimes receive
// an array here. No clue about its origin nor content, so let's avoid
// a notice on htmlspecialchars().
if (is_string($value)) {
$htmlOutput .= '<input type="text" size="25" ' . $nameId
. $fieldClass . ' value="' . htmlspecialchars($value)
. '">';
}
break;
case 'number_text':
$htmlOutput .= '<input type="number" ' . $nameId . $fieldClass
. ' value="' . htmlspecialchars((string) $value) . '">';
break;
case 'checkbox':
$htmlOutput .= '<span' . $fieldClass . '><input type="checkbox" ' . $nameId
. ($value ? ' checked="checked"' : '') . '></span>';
break;
case 'select':
$htmlOutput .= '<select class="w-75" ' . $nameId . $fieldClass . '>';
$escape = ! (isset($opts['values_escaped']) && $opts['values_escaped']);
$valuesDisabled = isset($opts['values_disabled'])
? array_flip($opts['values_disabled']) : [];
foreach ($opts['values'] as $optValueKey => $optValue) {
// set names for boolean values
if (is_bool($optValue)) {
$optValue = mb_strtolower(
$optValue ? __('Yes') : __('No')
);
}
// escape if necessary
if ($escape) {
$display = htmlspecialchars((string) $optValue);
$displayValue = htmlspecialchars((string) $optValueKey);
} else {
$display = $optValue;
$displayValue = $optValueKey;
}
// compare with selected value
// boolean values are cast to integers when used as array keys
$selected = is_bool($value)
? (int) $value === $optValueKey
: $optValueKey === $value;
$htmlOutput .= '<option value="' . $displayValue . '"';
if ($selected) {
$htmlOutput .= ' selected="selected"';
}
if (isset($valuesDisabled[$optValueKey])) {
$htmlOutput .= ' disabled="disabled"';
}
$htmlOutput .= '>' . $display . '</option>';
}
$htmlOutput .= '</select>';
break;
case 'list':
$val = $value;
if (isset($val['wrapper_params'])) {
unset($val['wrapper_params']);
}
$htmlOutput .= '<textarea cols="35" rows="5" ' . $nameId . $fieldClass
. '>' . htmlspecialchars(implode("\n", $val)) . '</textarea>';
break;
}
if ($isSetupScript
&& isset($opts['userprefs_comment'])
&& $opts['userprefs_comment']
) {
$htmlOutput .= '<a class="userprefs-comment" title="'
. htmlspecialchars($opts['userprefs_comment']) . '">'
. $icons['tblops'] . '</a>';
}
if (isset($opts['setvalue']) && $opts['setvalue']) {
$htmlOutput .= '<a class="set-value hide" href="#'
. htmlspecialchars($path . '=' . $opts['setvalue']) . '" title="'
. sprintf(__('Set value: %s'), htmlspecialchars($opts['setvalue']))
. '">' . $icons['edit'] . '</a>';
}
if (isset($opts['show_restore_default']) && $opts['show_restore_default']) {
$htmlOutput .= '<a class="restore-default hide" href="#' . $path . '" title="'
. __('Restore default value') . '">' . $icons['reload'] . '</a>';
}
// this must match with displayErrors() in scripts/config.js
if ($hasErrors) {
$htmlOutput .= "\n <dl class=\"inline_errors\">";
foreach ($opts['errors'] as $error) {
$htmlOutput .= '<dd>' . htmlspecialchars($error) . '</dd>';
}
$htmlOutput .= '</dl>';
}
$htmlOutput .= '</td>';
if ($isSetupScript && isset($opts['userprefs_allow'])) {
$htmlOutput .= '<td class="userprefs-allow" title="' .
__('Allow users to customize this value') . '">';
$htmlOutput .= '<input type="checkbox" name="' . $path
. '-userprefs-allow" ';
if ($opts['userprefs_allow']) {
$htmlOutput .= 'checked="checked"';
}
$htmlOutput .= '>';
$htmlOutput .= '</td>';
} elseif ($isSetupScript) {
$htmlOutput .= '<td>&nbsp;</td>';
}
$htmlOutput .= '</tr>';
return $htmlOutput;
}
/**
* Display group header
*
* @param string $headerText Text of header
*/
public function displayGroupHeader(string $headerText): string
{
$this->group++;
if ($headerText === '') {
return '';
}
$colspan = $this->config->get('is_setup') ? 3 : 2;
return $this->template->render('config/form_display/group_header', [
'group' => $this->group,
'colspan' => $colspan,
'header_text' => $headerText,
]);
}
/**
* Display group footer
*/
public function displayGroupFooter(): void
{
$this->group--;
}
/**
* Displays bottom part of a fieldset
*
* @param bool $showButtons Whether show submit and reset button
*/
public function displayFieldsetBottom(bool $showButtons = true): string
{
return $this->template->render('config/form_display/fieldset_bottom', [
'show_buttons' => $showButtons,
'is_setup' => $this->config->get('is_setup'),
]);
}
/**
* Closes form tabs
*/
public function displayTabsBottom(): string
{
return $this->template->render('config/form_display/tabs_bottom');
}
/**
* Displays bottom part of the form
*/
public function displayFormBottom(): string
{
return $this->template->render('config/form_display/form_bottom');
}
/**
* Appends JS validation code to $js_array
*
* @param string $fieldId ID of field to validate
* @param string|array $validators validators callback
* @param array $jsArray will be updated with javascript code
*/
public function addJsValidate($fieldId, $validators, array &$jsArray): void
{
foreach ((array) $validators as $validator) {
$validator = (array) $validator;
$vName = array_shift($validator);
$vArgs = [];
foreach ($validator as $arg) {
$vArgs[] = Sanitize::escapeJsString($arg);
}
$vArgs = $vArgs ? ", ['" . implode("', '", $vArgs) . "']" : '';
$jsArray[] = "registerFieldValidator('" . $fieldId . "', '" . $vName . "', true" . $vArgs . ')';
}
}
/**
* Displays JavaScript code
*
* @param array $jsArray lines of javascript code
*/
public function displayJavascript(array $jsArray): string
{
if (empty($jsArray)) {
return '';
}
return $this->template->render('javascript/display', ['js_array' => $jsArray]);
}
/**
* Displays error list
*
* @param string $name Name of item with errors
* @param array $errorList List of errors to show
*
* @return string HTML for errors
*/
public function displayErrors($name, array $errorList): string
{
return $this->template->render('config/form_display/errors', [
'name' => $name,
'error_list' => $errorList,
]);
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* Base class for preferences.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\FormDisplay;
use function is_int;
/**
* Base form for user preferences
*/
abstract class BaseForm extends FormDisplay
{
/**
* @param ConfigFile $cf Config file instance
* @param int|null $serverId 0 if new server, validation; >= 1 if editing a server
*/
public function __construct(ConfigFile $cf, $serverId = null)
{
parent::__construct($cf);
foreach (static::getForms() as $formName => $form) {
$this->registerForm($formName, $form, $serverId);
}
}
/**
* List of available forms, each form is described as an array of fields to display.
* Fields MUST have their counterparts in the $cfg array.
*
* To define form field, use the notation below:
* $forms['Form group']['Form name'] = array('Option/path');
*
* You can assign default values set by special button ("set value: ..."), eg.:
* 'Servers/1/pmadb' => 'phpmyadmin'
*
* To group options, use:
* ':group:' . __('group name') // just define a group
* or
* 'option' => ':group' // group starting from this option
* End group blocks with:
* ':group:end'
*
* @return array
*
* @todo This should be abstract, but that does not work in PHP 5
*/
public static function getForms()
{
return [];
}
/**
* Returns list of fields used in the form.
*
* @return string[]
*/
public static function getFields()
{
$names = [];
foreach (static::getForms() as $form) {
foreach ($form as $k => $v) {
$names[] = is_int($k) ? $v : $k;
}
}
return $names;
}
/**
* Returns name of the form
*
* @return string
*
* @todo This should be abstract, but that does not work in PHP 5
*/
public static function getName()
{
return '';
}
}

View file

@ -0,0 +1,149 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms;
use PhpMyAdmin\Config\ConfigFile;
use function array_merge;
use function in_array;
class BaseFormList
{
/**
* List of all forms
*
* @var array
*/
protected static $all = [];
/** @var string */
protected static $ns = 'PhpMyAdmin\\Config\\Forms\\';
/** @var array */
private $forms;
/**
* @return array
*/
public static function getAll()
{
return static::$all;
}
/**
* @param string $name Name
*
* @return bool
*/
public static function isValid($name)
{
return in_array($name, static::$all);
}
/**
* @param string $name Name
*
* @return string|null
*/
public static function get($name)
{
if (static::isValid($name)) {
return static::$ns . $name . 'Form';
}
return null;
}
/**
* @param ConfigFile $cf Config file instance
*/
public function __construct(ConfigFile $cf)
{
$this->forms = [];
foreach (static::$all as $form) {
$class = static::get($form);
$this->forms[] = new $class($cf);
}
}
/**
* Processes forms, returns true on successful save
*
* @param bool $allowPartialSave allows for partial form saving
* on failed validation
* @param bool $checkFormSubmit whether check for $_POST['submit_save']
*
* @return bool whether processing was successful
*/
public function process($allowPartialSave = true, $checkFormSubmit = true)
{
$ret = true;
foreach ($this->forms as $form) {
$ret = $ret && $form->process($allowPartialSave, $checkFormSubmit);
}
return $ret;
}
/**
* Displays errors
*
* @return string HTML for errors
*/
public function displayErrors()
{
$ret = '';
foreach ($this->forms as $form) {
$ret .= $form->displayErrors();
}
return $ret;
}
/**
* Reverts erroneous fields to their default values
*
* @return void
*/
public function fixErrors()
{
foreach ($this->forms as $form) {
$form->fixErrors();
}
}
/**
* Tells whether form validation failed
*
* @return bool
*/
public function hasErrors()
{
$ret = false;
foreach ($this->forms as $form) {
$ret = $ret || $form->hasErrors();
}
return $ret;
}
/**
* Returns list of fields used in the form.
*
* @return string[]
*/
public static function getFields()
{
$names = [];
foreach (static::$all as $form) {
$class = static::get($form);
$names = array_merge($names, $class::getFields());
}
return $names;
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class BrowseForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Browse' => MainForm::getForms()['Browse'],
];
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class DbStructureForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'DbStructure' => MainForm::getForms()['DbStructure'],
];
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\FeaturesForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class EditForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Edit' => MainForm::getForms()['Edit'],
'Text_fields' => FeaturesForm::getForms()['Text_fields'],
];
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class ExportForm extends \PhpMyAdmin\Config\Forms\User\ExportForm
{
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class ImportForm extends \PhpMyAdmin\Config\Forms\User\ImportForm
{
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class NaviForm extends \PhpMyAdmin\Config\Forms\User\NaviForm
{
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Page preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseFormList;
class PageFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Browse',
'DbStructure',
'Edit',
'Export',
'Import',
'Navi',
'Sql',
'TableStructure',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\Page\\';
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
class SqlForm extends \PhpMyAdmin\Config\Forms\User\SqlForm
{
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Page;
use PhpMyAdmin\Config\Forms\BaseForm;
use PhpMyAdmin\Config\Forms\User\MainForm;
class TableStructureForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'TableStructure' => MainForm::getForms()['TableStructure'],
];
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseForm;
class ConfigForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Config' => [
'DefaultLang',
'ServerDefault',
],
];
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class ExportForm extends \PhpMyAdmin\Config\Forms\User\ExportForm
{
}

View file

@ -0,0 +1,76 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use function array_diff;
class FeaturesForm extends \PhpMyAdmin\Config\Forms\User\FeaturesForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
$result = parent::getForms();
/* Remove only_db/hide_db, we have proper Server form in setup */
$result['Databases'] = array_diff(
$result['Databases'],
[
'Servers/1/only_db',
'Servers/1/hide_db',
]
);
/* Following are not available to user */
$result['Import_export'] = [
'UploadDir',
'SaveDir',
'RecodingEngine' => ':group',
'IconvExtraParams',
':group:end',
'ZipDump',
'GZipDump',
'BZipDump',
'CompressOnFly',
];
$result['Security'] = [
'blowfish_secret',
'CheckConfigurationPermissions',
'TrustedProxies',
'AllowUserDropDatabase',
'AllowArbitraryServer',
'ArbitraryServerRegexp',
'LoginCookieRecall',
'LoginCookieStore',
'LoginCookieDeleteAll',
'CaptchaLoginPublicKey',
'CaptchaLoginPrivateKey',
'CaptchaSiteVerifyURL',
];
$result['Developer'] = [
'UserprefsDeveloperTab',
'DBG/sql',
];
$result['Other_core_settings'] = [
'OBGzip',
'PersistentConnections',
'ExecTimeLimit',
'MemoryLimit',
'UseDbSearch',
'ProxyUrl',
'ProxyUser',
'ProxyPass',
'AllowThirdPartyFraming',
'ZeroConf',
];
return $result;
// phpcs:enable
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class ImportForm extends \PhpMyAdmin\Config\Forms\User\ImportForm
{
}

View file

@ -0,0 +1,24 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class MainForm extends \PhpMyAdmin\Config\Forms\User\MainForm
{
/**
* @return array
*/
public static function getForms()
{
$result = parent::getForms();
/* Following are not available to user */
$result['Startup'][] = 'ShowPhpInfo';
$result['Startup'][] = 'ShowChgPassword';
return $result;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class NaviForm extends \PhpMyAdmin\Config\Forms\User\NaviForm
{
}

View file

@ -0,0 +1,111 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseForm;
class ServersForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
return [
'Server' => [
'Servers' => [
1 => [
'verbose',
'host',
'port',
'socket',
'ssl',
'compress',
],
],
],
'Server_auth' => [
'Servers' => [
1 => [
'auth_type',
':group:' . __('Config authentication'),
'user',
'password',
':group:end',
':group:' . __('HTTP authentication'),
'auth_http_realm',
':group:end',
':group:' . __('Signon authentication'),
'SignonSession',
'SignonURL',
'LogoutURL',
],
],
],
'Server_config' => [
'Servers' => [
1 => [
'only_db',
'hide_db',
'AllowRoot',
'AllowNoPassword',
'DisableIS',
'AllowDeny/order',
'AllowDeny/rules',
'SessionTimeZone',
],
],
],
'Server_pmadb' => [
'Servers' => [
1 => [
'pmadb' => 'phpmyadmin',
'controlhost',
'controlport',
'controluser',
'controlpass',
'bookmarktable' => 'pma__bookmark',
'relation' => 'pma__relation',
'userconfig' => 'pma__userconfig',
'users' => 'pma__users',
'usergroups' => 'pma__usergroups',
'navigationhiding' => 'pma__navigationhiding',
'table_info' => 'pma__table_info',
'column_info' => 'pma__column_info',
'history' => 'pma__history',
'recent' => 'pma__recent',
'favorite' => 'pma__favorite',
'table_uiprefs' => 'pma__table_uiprefs',
'tracking' => 'pma__tracking',
'table_coords' => 'pma__table_coords',
'pdf_pages' => 'pma__pdf_pages',
'savedsearches' => 'pma__savedsearches',
'central_columns' => 'pma__central_columns',
'designer_settings' => 'pma__designer_settings',
'export_templates' => 'pma__export_templates',
'MaxTableUiprefs' => 100,
],
],
],
'Server_tracking' => [
'Servers' => [
1 => [
'tracking_version_auto_create',
'tracking_default_statements',
'tracking_add_drop_view',
'tracking_add_drop_table',
'tracking_add_drop_database',
],
],
],
];
// phpcs:enable
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Setup preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
use PhpMyAdmin\Config\Forms\BaseFormList;
class SetupFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Config',
'Export',
'Features',
'Import',
'Main',
'Navi',
'Servers',
'Sql',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\Setup\\';
}

View file

@ -0,0 +1,23 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\Setup;
class SqlForm extends \PhpMyAdmin\Config\Forms\User\SqlForm
{
/**
* @return array
*/
public static function getForms()
{
$result = parent::getForms();
/* Following are not available to user */
$result['Sql_queries'][] = 'QueryHistoryDB';
return $result;
}
}

View file

@ -0,0 +1,153 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class ExportForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
// phpcs:disable Squiz.Arrays.ArrayDeclaration.KeySpecified,Squiz.Arrays.ArrayDeclaration.NoKeySpecified
return [
'Export_defaults' => [
'Export/method',
':group:' . __('Quick'),
'Export/quick_export_onserver',
'Export/quick_export_onserver_overwrite',
':group:end',
':group:' . __('Custom'),
'Export/format',
'Export/compression',
'Export/charset',
'Export/lock_tables',
'Export/as_separate_files',
'Export/asfile' => ':group',
'Export/onserver',
'Export/onserver_overwrite',
':group:end',
'Export/file_template_table',
'Export/file_template_database',
'Export/file_template_server',
],
'Sql' => [
'Export/sql_include_comments' => ':group',
'Export/sql_dates',
'Export/sql_relation',
'Export/sql_mime',
':group:end',
'Export/sql_use_transaction',
'Export/sql_disable_fk',
'Export/sql_views_as_tables',
'Export/sql_metadata',
'Export/sql_compatibility',
'Export/sql_structure_or_data',
':group:' . __('Structure'),
'Export/sql_drop_database',
'Export/sql_create_database',
'Export/sql_drop_table',
'Export/sql_create_table' => ':group',
'Export/sql_if_not_exists',
'Export/sql_auto_increment',
':group:end',
'Export/sql_create_view' => ':group',
'Export/sql_view_current_user',
'Export/sql_or_replace_view',
':group:end',
'Export/sql_procedure_function',
'Export/sql_create_trigger',
'Export/sql_backquotes',
':group:end',
':group:' . __('Data'),
'Export/sql_delayed',
'Export/sql_ignore',
'Export/sql_type',
'Export/sql_insert_syntax',
'Export/sql_max_query_size',
'Export/sql_hex_for_binary',
'Export/sql_utc_time',
],
'CodeGen' => ['Export/codegen_format'],
'Csv' => [
':group:' . __('CSV'),
'Export/csv_separator',
'Export/csv_enclosed',
'Export/csv_escaped',
'Export/csv_terminated',
'Export/csv_null',
'Export/csv_removeCRLF',
'Export/csv_columns',
':group:end',
':group:' . __('CSV for MS Excel'),
'Export/excel_null',
'Export/excel_removeCRLF',
'Export/excel_columns',
'Export/excel_edition',
],
'Latex' => [
'Export/latex_caption',
'Export/latex_structure_or_data',
':group:' . __('Structure'),
'Export/latex_structure_caption',
'Export/latex_structure_continued_caption',
'Export/latex_structure_label',
'Export/latex_relation',
'Export/latex_comments',
'Export/latex_mime',
':group:end',
':group:' . __('Data'),
'Export/latex_columns',
'Export/latex_data_caption',
'Export/latex_data_continued_caption',
'Export/latex_data_label',
'Export/latex_null',
],
'Microsoft_Office' => [
':group:' . __('Microsoft Word 2000'),
'Export/htmlword_structure_or_data',
'Export/htmlword_null',
'Export/htmlword_columns',
],
'Open_Document' => [
':group:' . __('OpenDocument Spreadsheet'),
'Export/ods_columns',
'Export/ods_null',
':group:end',
':group:' . __('OpenDocument Text'),
'Export/odt_structure_or_data',
':group:' . __('Structure'),
'Export/odt_relation',
'Export/odt_comments',
'Export/odt_mime',
':group:end',
':group:' . __('Data'),
'Export/odt_columns',
'Export/odt_null',
],
'Texy' => [
'Export/texytext_structure_or_data',
':group:' . __('Data'),
'Export/texytext_null',
'Export/texytext_columns',
],
];
// phpcs:enable
}
/**
* @return string
*/
public static function getName()
{
return __('Export');
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class FeaturesForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
$result = [
'General' => [
'VersionCheck',
'NaturalOrder',
'InitialSlidersState',
'LoginCookieValidity',
'SkipLockedTables',
'DisableMultiTableMaintenance',
'ShowHint',
'SendErrorReports',
'ConsoleEnterExecutes',
'DisableShortcutKeys',
'FirstDayOfCalendar',
],
'Databases' => [
'Servers/1/only_db', // saves to Server/only_db
'Servers/1/hide_db', // saves to Server/hide_db
'MaxDbList',
'MaxTableList',
'DefaultConnectionCollation',
],
'Text_fields' => [
'CharEditing',
'MinSizeForInputField',
'MaxSizeForInputField',
'CharTextareaCols',
'CharTextareaRows',
'TextareaCols',
'TextareaRows',
'LongtextDoubleTextarea',
],
'Page_titles' => [
'TitleDefault',
'TitleTable',
'TitleDatabase',
'TitleServer',
],
'Warnings' => [
'PmaNoRelation_DisableWarning',
'SuhosinDisableWarning',
'LoginCookieValidityDisableWarning',
'ReservedWordDisableWarning',
],
'Console' => [
'Console/Mode',
'Console/StartHistory',
'Console/AlwaysExpand',
'Console/CurrentQuery',
'Console/EnterExecutes',
'Console/DarkTheme',
'Console/Height',
'Console/GroupQueries',
'Console/OrderBy',
'Console/Order',
],
];
// skip Developer form if no setting is available
if ($GLOBALS['cfg']['UserprefsDeveloperTab']) {
$result['Developer'] = ['DBG/sql'];
}
return $result;
}
/**
* @return string
*/
public static function getName()
{
return __('Features');
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class ImportForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Import_defaults' => [
'Import/format',
'Import/charset',
'Import/allow_interrupt',
'Import/skip_queries',
'enable_drag_drop_import',
],
'Sql' => [
'Import/sql_compatibility',
'Import/sql_no_auto_value_on_zero',
'Import/sql_read_as_multibytes',
],
'Csv' => [
':group:' . __('CSV'),
'Import/csv_replace',
'Import/csv_ignore',
'Import/csv_terminated',
'Import/csv_enclosed',
'Import/csv_escaped',
'Import/csv_col_names',
':group:end',
':group:' . __('CSV using LOAD DATA'),
'Import/ldi_replace',
'Import/ldi_ignore',
'Import/ldi_terminated',
'Import/ldi_enclosed',
'Import/ldi_escaped',
'Import/ldi_local_option',
],
'Open_Document' => [
':group:' . __('OpenDocument Spreadsheet'),
'Import/ods_col_names',
'Import/ods_empty_rows',
'Import/ods_recognize_percentages',
'Import/ods_recognize_currency',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Import');
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class MainForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Startup' => [
'ShowCreateDb',
'ShowStats',
'ShowServerInfo',
],
'DbStructure' => [
'ShowDbStructureCharset',
'ShowDbStructureComment',
'ShowDbStructureCreation',
'ShowDbStructureLastUpdate',
'ShowDbStructureLastCheck',
],
'TableStructure' => [
'HideStructureActions',
'ShowColumnComments',
':group:' . __('Default transformations'),
'DefaultTransformations/Hex',
'DefaultTransformations/Substring',
'DefaultTransformations/Bool2Text',
'DefaultTransformations/External',
'DefaultTransformations/PreApPend',
'DefaultTransformations/DateFormat',
'DefaultTransformations/Inline',
'DefaultTransformations/TextImageLink',
'DefaultTransformations/TextLink',
':group:end',
],
'Browse' => [
'TableNavigationLinksMode',
'ActionLinksMode',
'ShowAll',
'MaxRows',
'Order',
'BrowsePointerEnable',
'BrowseMarkerEnable',
'GridEditing',
'SaveCellsAtOnce',
'RepeatCells',
'LimitChars',
'RowActionLinks',
'RowActionLinksWithoutUnique',
'TablePrimaryKeyOrder',
'RememberSorting',
'RelationalDisplay',
],
'Edit' => [
'ProtectBinary',
'ShowFunctionFields',
'ShowFieldTypesInDataEditView',
'InsertRows',
'ForeignKeyDropdownOrder',
'ForeignKeyMaxLimit',
],
'Tabs' => [
'TabsMode',
'DefaultTabServer',
'DefaultTabDatabase',
'DefaultTabTable',
],
'DisplayRelationalSchema' => ['PDFDefaultPageSize'],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Main panel');
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class NaviForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Navi_panel' => [
'ShowDatabasesNavigationAsTree',
'NavigationLinkWithMainPanel',
'NavigationDisplayLogo',
'NavigationLogoLink',
'NavigationLogoLinkWindow',
'NavigationTreePointerEnable',
'FirstLevelNavigationItems',
'NavigationTreeDisplayItemFilterMinimum',
'NumRecentTables',
'NumFavoriteTables',
'NavigationWidth',
],
'Navi_tree' => [
'MaxNavigationItems',
'NavigationTreeEnableGrouping',
'NavigationTreeEnableExpansion',
'NavigationTreeShowTables',
'NavigationTreeShowViews',
'NavigationTreeShowFunctions',
'NavigationTreeShowProcedures',
'NavigationTreeShowEvents',
'NavigationTreeAutoexpandSingleDb',
],
'Navi_servers' => [
'NavigationDisplayServers',
'DisplayServersList',
],
'Navi_databases' => [
'NavigationTreeDisplayDbFilterMinimum',
'NavigationTreeDbSeparator',
],
'Navi_tables' => [
'NavigationTreeDefaultTabTable',
'NavigationTreeDefaultTabTable2',
'NavigationTreeTableSeparator',
'NavigationTreeTableLevel',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('Navigation panel');
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseForm;
class SqlForm extends BaseForm
{
/**
* @return array
*/
public static function getForms()
{
return [
'Sql_queries' => [
'ShowSQL',
'Confirm',
'QueryHistoryMax',
'IgnoreMultiSubmitErrors',
'MaxCharactersInDisplayedSQL',
'RetainQueryBox',
'CodemirrorEnable',
'LintEnable',
'EnableAutocompleteForTablesAndColumns',
'DefaultForeignKeyChecks',
],
'Sql_box' => [
'SQLQuery/Edit',
'SQLQuery/Explain',
'SQLQuery/ShowAsPHP',
'SQLQuery/Refresh',
],
];
}
/**
* @return string
*/
public static function getName()
{
return __('SQL queries');
}
}

View file

@ -0,0 +1,25 @@
<?php
/**
* User preferences form
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config\Forms\User;
use PhpMyAdmin\Config\Forms\BaseFormList;
class UserFormList extends BaseFormList
{
/** @var array */
protected static $all = [
'Features',
'Sql',
'Navi',
'Main',
'Export',
'Import',
];
/** @var string */
protected static $ns = '\\PhpMyAdmin\\Config\\Forms\\User\\';
}

View file

@ -0,0 +1,199 @@
<?php
/**
* Page-related settings
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Config\Forms\Page\PageFormList;
use PhpMyAdmin\Core;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\UserPreferences;
/**
* Page-related settings
*/
class PageSettings
{
/**
* Contains id of the form element
*
* @var string
*/
private $elemId = 'page_settings_modal';
/**
* Name of the group to show
*
* @var string
*/
private $groupName = '';
/**
* Contains HTML of errors
*
* @var string
*/
private $errorHTML = '';
/**
* Contains HTML of settings
*
* @var string
*/
private $HTML = '';
/** @var UserPreferences */
private $userPreferences;
/**
* @param string $formGroupName The name of config form group to display
* @param string $elemId Id of the div containing settings
*/
public function __construct($formGroupName, $elemId = null)
{
$this->userPreferences = new UserPreferences();
$formClass = PageFormList::get($formGroupName);
if ($formClass === null) {
return;
}
if (isset($_REQUEST['printview']) && $_REQUEST['printview'] == '1') {
return;
}
if (! empty($elemId)) {
$this->elemId = $elemId;
}
$this->groupName = $formGroupName;
$cf = new ConfigFile($GLOBALS['PMA_Config']->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new $formClass($cf);
// Process form
$error = null;
if (isset($_POST['submit_save'])
&& $_POST['submit_save'] == $formGroupName
) {
$this->processPageSettings($formDisplay, $cf, $error);
}
// Display forms
$this->HTML = $this->getPageSettingsDisplay($formDisplay, $error);
}
/**
* Process response to form
*
* @param FormDisplay $formDisplay Form
* @param ConfigFile $cf Configuration file
* @param Message|null $error Error message
*
* @return void
*/
private function processPageSettings(&$formDisplay, &$cf, &$error)
{
if (! $formDisplay->process(false) || $formDisplay->hasErrors()) {
return;
}
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
if ($result === true) {
// reload page
$response = Response::getInstance();
Core::sendHeaderLocation(
$response->getFooter()->getSelfUrl()
);
exit;
}
$error = $result;
}
/**
* Store errors in _errorHTML
*
* @param FormDisplay $formDisplay Form
* @param Message|null $error Error message
*
* @return void
*/
private function storeError(&$formDisplay, &$error)
{
$retval = '';
if ($error) {
$retval .= $error->getDisplay();
}
if ($formDisplay->hasErrors()) {
// form has errors
$retval .= '<div class="alert alert-danger config-form" role="alert">'
. '<b>' . __(
'Cannot save settings, submitted configuration form contains '
. 'errors!'
) . '</b>'
. $formDisplay->displayErrors()
. '</div>';
}
$this->errorHTML = $retval;
}
/**
* Display page-related settings
*
* @param FormDisplay $formDisplay Form
* @param Message $error Error message
*
* @return string
*/
private function getPageSettingsDisplay(&$formDisplay, &$error)
{
$response = Response::getInstance();
$retval = '';
$this->storeError($formDisplay, $error);
$retval .= '<div id="' . $this->elemId . '">';
$retval .= '<div class="page_settings">';
$retval .= $formDisplay->getDisplay(
true,
true,
false,
$response->getFooter()->getSelfUrl(),
[
'submit_save' => $this->groupName,
]
);
$retval .= '</div>';
$retval .= '</div>';
return $retval;
}
/**
* Get HTML output
*
* @return string
*/
public function getHTML()
{
return $this->HTML;
}
/**
* Get error HTML output
*
* @return string
*/
public function getErrorHTML()
{
return $this->errorHTML;
}
}

View file

@ -0,0 +1,568 @@
<?php
/**
* Server config checks management
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Setup\Index as SetupIndex;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function function_exists;
use function htmlspecialchars;
use function implode;
use function ini_get;
use function preg_match;
use function sprintf;
use function strlen;
/**
* Performs various compatibility, security and consistency checks on current config
*
* Outputs results to message list, must be called between SetupIndex::messagesBegin()
* and SetupIndex::messagesEnd()
*/
class ServerConfigChecks
{
/** @var ConfigFile configurations being checked */
protected $cfg;
/**
* @param ConfigFile $cfg Configuration
*/
public function __construct(ConfigFile $cfg)
{
$this->cfg = $cfg;
}
/**
* Perform config checks
*
* @return void
*/
public function performConfigChecks()
{
$blowfishSecret = $this->cfg->get('blowfish_secret');
$blowfishSecretSet = false;
$cookieAuthUsed = false;
[$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet]
= $this->performConfigChecksServers(
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet
);
$this->performConfigChecksCookieAuthUsed(
$cookieAuthUsed,
$blowfishSecretSet,
$blowfishSecret
);
// $cfg['AllowArbitraryServer']
// should be disabled
if ($this->cfg->getValue('AllowArbitraryServer')) {
$sAllowArbitraryServerWarn = sprintf(
__(
'This %soption%s should be disabled as it allows attackers to '
. 'bruteforce login to any MySQL server. If you feel this is necessary, '
. 'use %srestrict login to MySQL server%s or %strusted proxies list%s. '
. 'However, IP-based protection with trusted proxies list may not be '
. 'reliable if your IP belongs to an ISP where thousands of users, '
. 'including you, are connected to.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
);
SetupIndex::messagesSet(
'notice',
'AllowArbitraryServer',
Descriptions::get('AllowArbitraryServer'),
Sanitize::sanitizeMessage($sAllowArbitraryServerWarn)
);
}
$this->performConfigChecksLoginCookie();
$sDirectoryNotice = __(
'This value should be double checked to ensure that this directory is '
. 'neither world accessible nor readable or writable by other users on '
. 'your server.'
);
// $cfg['SaveDir']
// should not be world-accessible
if ($this->cfg->getValue('SaveDir') != '') {
SetupIndex::messagesSet(
'notice',
'SaveDir',
Descriptions::get('SaveDir'),
Sanitize::sanitizeMessage($sDirectoryNotice)
);
}
// $cfg['TempDir']
// should not be world-accessible
if ($this->cfg->getValue('TempDir') != '') {
SetupIndex::messagesSet(
'notice',
'TempDir',
Descriptions::get('TempDir'),
Sanitize::sanitizeMessage($sDirectoryNotice)
);
}
$this->performConfigChecksZips();
}
/**
* Check config of servers
*
* @param bool $cookieAuthUsed Cookie auth is used
* @param string $blowfishSecret Blowfish secret
* @param bool $blowfishSecretSet Blowfish secret set
*
* @return array
*/
protected function performConfigChecksServers(
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet
) {
$serverCnt = $this->cfg->getServerCount();
for ($i = 1; $i <= $serverCnt; $i++) {
$cookieAuthServer
= ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'cookie');
$cookieAuthUsed |= $cookieAuthServer;
$serverName = $this->performConfigChecksServersGetServerName(
$this->cfg->getServerName($i),
$i
);
$serverName = htmlspecialchars($serverName);
[$blowfishSecret, $blowfishSecretSet]
= $this->performConfigChecksServersSetBlowfishSecret(
$blowfishSecret,
$cookieAuthServer,
$blowfishSecretSet
);
// $cfg['Servers'][$i]['ssl']
// should be enabled if possible
if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) {
$title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/ssl',
$title,
__(
'You should use SSL connections if your database server '
. 'supports it.'
)
);
}
$sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf(
__(
'If you feel this is necessary, use additional protection settings - '
. '%1$shost authentication%2$s settings and %3$strusted proxies list%4$s. '
. 'However, IP-based protection may not be reliable if your IP belongs '
. 'to an ISP where thousands of users, including you, are connected to.'
),
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
));
// $cfg['Servers'][$i]['auth_type']
// warn about full user credentials if 'auth_type' is 'config'
if ($this->cfg->getValue('Servers/' . $i . '/auth_type') === 'config'
&& $this->cfg->getValue('Servers/' . $i . '/user') != ''
&& $this->cfg->getValue('Servers/' . $i . '/password') != ''
) {
$title = Descriptions::get('Servers/1/auth_type')
. ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/auth_type',
$title,
Sanitize::sanitizeMessage(sprintf(
__(
'You set the [kbd]config[/kbd] authentication type and included '
. 'username and password for auto-login, which is not a desirable '
. 'option for live hosts. Anyone who knows or guesses your phpMyAdmin '
. 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication '
. 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].'
),
'[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]',
'[/a]'
))
. ' ' . $sSecurityInfoMsg
);
}
// $cfg['Servers'][$i]['AllowRoot']
// $cfg['Servers'][$i]['AllowNoPassword']
// serious security flaw
if (! $this->cfg->getValue('Servers/' . $i . '/AllowRoot')
|| ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword')
) {
continue;
}
$title = Descriptions::get('Servers/1/AllowNoPassword')
. ' (' . $serverName . ')';
SetupIndex::messagesSet(
'notice',
'Servers/' . $i . '/AllowNoPassword',
$title,
__('You allow for connecting to the server without a password.')
. ' ' . $sSecurityInfoMsg
);
}
return [
$cookieAuthUsed,
$blowfishSecret,
$blowfishSecretSet,
];
}
/**
* Set blowfish secret
*
* @param string|null $blowfishSecret Blowfish secret
* @param bool $cookieAuthServer Cookie auth is used
* @param bool $blowfishSecretSet Blowfish secret set
*
* @return array
*/
protected function performConfigChecksServersSetBlowfishSecret(
$blowfishSecret,
$cookieAuthServer,
$blowfishSecretSet
): array {
if ($cookieAuthServer && $blowfishSecret === null) {
$blowfishSecretSet = true;
$this->cfg->set('blowfish_secret', Util::generateRandom(32));
}
return [
$blowfishSecret,
$blowfishSecretSet,
];
}
/**
* Define server name
*
* @param string $serverName Server name
* @param int $serverId Server id
*
* @return string Server name
*/
protected function performConfigChecksServersGetServerName(
$serverName,
$serverId
) {
if ($serverName === 'localhost') {
return $serverName . ' [' . $serverId . ']';
}
return $serverName;
}
/**
* Perform config checks for zip part.
*
* @return void
*/
protected function performConfigChecksZips()
{
$this->performConfigChecksServerGZipdump();
$this->performConfigChecksServerBZipdump();
$this->performConfigChecksServersZipdump();
}
/**
* Perform config checks for zip part.
*
* @return void
*/
protected function performConfigChecksServersZipdump()
{
// $cfg['ZipDump']
// requires zip_open in import
if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) {
SetupIndex::messagesSet(
'error',
'ZipDump_import',
Descriptions::get('ZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sZip decompression%s requires functions (%s) which are unavailable '
. 'on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'zip_open'
))
);
}
// $cfg['ZipDump']
// requires gzcompress in export
if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) {
return;
}
SetupIndex::messagesSet(
'error',
'ZipDump_export',
Descriptions::get('ZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sZip compression%s requires functions (%s) which are unavailable on '
. 'this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'gzcompress'
))
);
}
/**
* Check config of servers
*
* @param bool $cookieAuthUsed Cookie auth is used
* @param bool $blowfishSecretSet Blowfish secret set
* @param string $blowfishSecret Blowfish secret
*
* @return void
*/
protected function performConfigChecksCookieAuthUsed(
$cookieAuthUsed,
$blowfishSecretSet,
$blowfishSecret
) {
// $cfg['blowfish_secret']
// it's required for 'cookie' authentication
if (! $cookieAuthUsed) {
return;
}
if ($blowfishSecretSet) {
// 'cookie' auth used, blowfish_secret was generated
SetupIndex::messagesSet(
'notice',
'blowfish_secret_created',
Descriptions::get('blowfish_secret'),
Sanitize::sanitizeMessage(__(
'You didn\'t have blowfish secret set and have enabled '
. '[kbd]cookie[/kbd] authentication, so a key was automatically '
. 'generated for you. It is used to encrypt cookies; you don\'t need to '
. 'remember it.'
))
);
} else {
$blowfishWarnings = [];
// check length
if (strlen($blowfishSecret) < 32) {
// too short key
$blowfishWarnings[] = __(
'Key is too short, it should have at least 32 characters.'
);
}
// check used characters
$hasDigits = (bool) preg_match('/\d/', $blowfishSecret);
$hasChars = (bool) preg_match('/\S/', $blowfishSecret);
$hasNonword = (bool) preg_match('/\W/', $blowfishSecret);
if (! $hasDigits || ! $hasChars || ! $hasNonword) {
$blowfishWarnings[] = Sanitize::sanitizeMessage(
__(
'Key should contain letters, numbers [em]and[/em] '
. 'special characters.'
)
);
}
if (! empty($blowfishWarnings)) {
SetupIndex::messagesSet(
'error',
'blowfish_warnings' . count($blowfishWarnings),
Descriptions::get('blowfish_secret'),
implode('<br>', $blowfishWarnings)
);
}
}
}
/**
* Check configuration for login cookie
*
* @return void
*/
protected function performConfigChecksLoginCookie()
{
// $cfg['LoginCookieValidity']
// value greater than session.gc_maxlifetime will cause
// random session invalidation after that time
$loginCookieValidity = $this->cfg->getValue('LoginCookieValidity');
if ($loginCookieValidity > ini_get('session.gc_maxlifetime')
) {
SetupIndex::messagesSet(
'error',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may '
. 'cause random session invalidation (currently session.gc_maxlifetime '
. 'is %5$d).'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']',
'[/a]',
ini_get('session.gc_maxlifetime')
))
);
}
// $cfg['LoginCookieValidity']
// should be at most 1800 (30 min)
if ($loginCookieValidity > 1800) {
SetupIndex::messagesSet(
'notice',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) '
. 'at most. Values larger than 1800 may pose a security risk such as '
. 'impersonation.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
))
);
}
// $cfg['LoginCookieValidity']
// $cfg['LoginCookieStore']
// LoginCookieValidity must be less or equal to LoginCookieStore
if (($this->cfg->getValue('LoginCookieStore') == 0)
|| ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore'))
) {
return;
}
SetupIndex::messagesSet(
'error',
'LoginCookieValidity',
Descriptions::get('LoginCookieValidity'),
Sanitize::sanitizeMessage(sprintf(
__(
'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s '
. 'is not 0, %sLogin cookie validity%s must be set to a value less or '
. 'equal to it.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]',
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
'[/a]'
))
);
}
/**
* Check GZipDump configuration
*
* @return void
*/
protected function performConfigChecksServerBZipdump()
{
// $cfg['BZipDump']
// requires bzip2 functions
if (! $this->cfg->getValue('BZipDump')
|| ($this->functionExists('bzopen') && $this->functionExists('bzcompress'))
) {
return;
}
$functions = $this->functionExists('bzopen')
? '' :
'bzopen';
$functions .= $this->functionExists('bzcompress')
? ''
: ($functions ? ', ' : '') . 'bzcompress';
SetupIndex::messagesSet(
'error',
'BZipDump',
Descriptions::get('BZipDump'),
Sanitize::sanitizeMessage(
sprintf(
__(
'%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which '
. 'are unavailable on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
$functions
)
)
);
}
/**
* Check GZipDump configuration
*
* @return void
*/
protected function performConfigChecksServerGZipdump()
{
// $cfg['GZipDump']
// requires zlib functions
if (! $this->cfg->getValue('GZipDump')
|| ($this->functionExists('gzopen') && $this->functionExists('gzencode'))
) {
return;
}
SetupIndex::messagesSet(
'error',
'GZipDump',
Descriptions::get('GZipDump'),
Sanitize::sanitizeMessage(sprintf(
__(
'%1$sGZip compression and decompression%2$s requires functions (%3$s) which '
. 'are unavailable on this system.'
),
'[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
'[/a]',
'gzencode'
))
);
}
/**
* Wrapper around function_exists to allow mock in test
*
* @param string $name Function name
*
* @return bool
*/
protected function functionExists($name)
{
return function_exists($name);
}
}

View file

@ -0,0 +1,489 @@
<?php
/**
* Links configuration for MySQL system tables
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
class SpecialSchemaLinks
{
/**
* This array represent the details for generating links inside
* special schemas like mysql, information_schema etc.
* Major element represent a schema.
* All the strings in this array represented in lower case
*
* Array structure ex:
* array(
* // Database name is the major element
* 'mysql' => array(
* // Table name
* 'db' => array(
* // Column name
* 'user' => array(
* // Main url param (can be an array where represent sql)
* 'link_param' => 'username',
* // Other url params
* 'link_dependancy_params' => array(
* 0 => array(
* // URL parameter name
* // (can be array where url param has static value)
* 'param_info' => 'hostname',
* // Column name related to url param
* 'column_name' => 'host'
* )
* ),
* // Page to link
* 'default_page' => './' . Url::getFromRoute('/server/privileges')
* )
* )
* )
* );
*
* @return array<string,array<string,array<string,array<string,array<int,array<string,string>>|string>>>>
*
* @phpstan-return array<
* string, array<
* string, array<
* string,
* array{
* 'link_param': string,
* 'link_dependancy_params'?: array<
* int,
* array{'param_info': string, 'column_name': string}
* >,
* 'default_page': string
* }>
* >
* >
* }
*/
public static function get(): array
{
global $cfg;
$defaultPage = './' . Util::getScriptNameForOption(
$cfg['DefaultTabTable'],
'table'
);
return [
'mysql' => [
'columns_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
1 => [
'param_info' => 'table',
'column_name' => 'Table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'db' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'event' => [
'name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
],
'default_page' => './' . Url::getFromRoute('/database/events', ['edit_item' => 1]),
],
],
'innodb_index_stats' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
],
'default_page' => $defaultPage,
],
'index_name' => [
'link_param' => 'index',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure'),
],
],
'innodb_table_stats' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'database_name',
],
],
'default_page' => $defaultPage,
],
],
'proc' => [
'name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
'specific_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
],
'proc_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'routine_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'Routine_type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines', ['edit_item' => 1]),
],
],
'proxies_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'tables_priv' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'Host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'Db',
],
],
'default_page' => $defaultPage,
],
],
'user' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
],
'information_schema' => [
'columns' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'key_column_usage' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
'referenced_table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'referenced_table_schema',
],
],
'default_page' => $defaultPage,
],
'referenced_column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'referenced_table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'referenced_table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'partitions' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'processlist' => [
'user' => [
'link_param' => 'username',
'link_dependancy_params' => [
0 => [
'param_info' => 'hostname',
'column_name' => 'host',
],
],
'default_page' => './' . Url::getFromRoute('/server/privileges'),
],
],
'referential_constraints' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
'referenced_table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'constraint_schema',
],
],
'default_page' => $defaultPage,
],
],
'routines' => [
'routine_name' => [
'link_param' => 'item_name',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'routine_schema',
],
1 => [
'param_info' => 'item_type',
'column_name' => 'routine_type',
],
],
'default_page' => './' . Url::getFromRoute('/database/routines'),
],
],
'schemata' => [
'schema_name' => [
'link_param' => 'db',
'default_page' => $defaultPage,
],
],
'statistics' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
'column_name' => [
'link_param' => 'field',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
1 => [
'param_info' => 'table',
'column_name' => 'table_name',
],
],
'default_page' => './' . Url::getFromRoute('/table/structure/change', ['change_column' => 1]),
],
],
'tables' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'table_constraints' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
'views' => [
'table_name' => [
'link_param' => 'table',
'link_dependancy_params' => [
0 => [
'param_info' => 'db',
'column_name' => 'table_schema',
],
],
'default_page' => $defaultPage,
],
],
],
];
}
}

View file

@ -0,0 +1,608 @@
<?php
/**
* Form validation for configuration editor
*/
declare(strict_types=1);
namespace PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\Util;
use function mysqli_report;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
use const MYSQLI_REPORT_OFF;
use const PHP_INT_MAX;
use function array_map;
use function array_merge;
use function array_shift;
use function call_user_func_array;
use function count;
use function error_clear_last;
use function error_get_last;
use function explode;
use function filter_var;
use function htmlspecialchars;
use function intval;
use function is_array;
use function is_object;
use function mb_strpos;
use function mb_substr;
use function mysqli_close;
use function mysqli_connect;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use function trim;
/**
* Validation class for various validation functions
*
* Validation function takes two argument: id for which it is called
* and array of fields' values (usually values for entire formset).
* The function must always return an array with an error (or error array)
* assigned to a form element (formset name or field path). Even if there are
* no errors, key must be set with an empty value.
*
* Validation functions are assigned in $cfg_db['_validators'] (config.values.php).
*/
class Validator
{
/**
* Returns validator list
*
* @param ConfigFile $cf Config file instance
*
* @return array
*/
public static function getValidators(ConfigFile $cf)
{
static $validators = null;
if ($validators !== null) {
return $validators;
}
$validators = $cf->getDbEntry('_validators', []);
if ($GLOBALS['PMA_Config']->get('is_setup')) {
return $validators;
}
// not in setup script: load additional validators for user
// preferences we need original config values not overwritten
// by user preferences, creating a new PhpMyAdmin\Config instance is a
// better idea than hacking into its code
$uvs = $cf->getDbEntry('_userValidators', []);
foreach ($uvs as $field => $uvList) {
$uvList = (array) $uvList;
foreach ($uvList as &$uv) {
if (! is_array($uv)) {
continue;
}
for ($i = 1, $nb = count($uv); $i < $nb; $i++) {
if (mb_substr($uv[$i], 0, 6) !== 'value:') {
continue;
}
$uv[$i] = Core::arrayRead(
mb_substr($uv[$i], 6),
$GLOBALS['PMA_Config']->baseSettings
);
}
}
$validators[$field] = isset($validators[$field])
? array_merge((array) $validators[$field], $uvList)
: $uvList;
}
return $validators;
}
/**
* Runs validation $validator_id on values $values and returns error list.
*
* Return values:
* o array, keys - field path or formset id, values - array of errors
* when $isPostSource is true values is an empty array to allow for error list
* cleanup in HTML document
* o false - when no validators match name(s) given by $validator_id
*
* @param ConfigFile $cf Config file instance
* @param string|array $validatorId ID of validator(s) to run
* @param array $values Values to validate
* @param bool $isPostSource tells whether $values are directly from
* POST request
*
* @return bool|array
*/
public static function validate(
ConfigFile $cf,
$validatorId,
array &$values,
$isPostSource
) {
// find validators
$validatorId = (array) $validatorId;
$validators = static::getValidators($cf);
$vids = [];
foreach ($validatorId as &$vid) {
$vid = $cf->getCanonicalPath($vid);
if (! isset($validators[$vid])) {
continue;
}
$vids[] = $vid;
}
if (empty($vids)) {
return false;
}
// create argument list with canonical paths and remember path mapping
$arguments = [];
$keyMap = [];
foreach ($values as $k => $v) {
$k2 = $isPostSource ? str_replace('-', '/', $k) : $k;
$k2 = mb_strpos($k2, '/')
? $cf->getCanonicalPath($k2)
: $k2;
$keyMap[$k2] = $k;
$arguments[$k2] = $v;
}
// validate
$result = [];
foreach ($vids as $vid) {
// call appropriate validation functions
foreach ((array) $validators[$vid] as $validator) {
$vdef = (array) $validator;
$vname = array_shift($vdef);
$vname = 'PhpMyAdmin\Config\Validator::' . $vname;
$args = array_merge([$vid, &$arguments], $vdef);
$r = call_user_func_array($vname, $args);
// merge results
if (! is_array($r)) {
continue;
}
foreach ($r as $key => $errorList) {
// skip empty values if $isPostSource is false
if (! $isPostSource && empty($errorList)) {
continue;
}
if (! isset($result[$key])) {
$result[$key] = [];
}
$result[$key] = array_merge(
$result[$key],
(array) $errorList
);
}
}
}
// restore original paths
$newResult = [];
foreach ($result as $k => $v) {
$k2 = $keyMap[$k] ?? $k;
if (is_array($v)) {
$newResult[$k2] = array_map('htmlspecialchars', $v);
} else {
$newResult[$k2] = htmlspecialchars($v);
}
}
return empty($newResult) ? true : $newResult;
}
/**
* Test database connection
*
* @param string $host host name
* @param string $port tcp port to use
* @param string $socket socket to use
* @param string $user username to use
* @param string $pass password to use
* @param string $errorKey key to use in return array
*
* @return bool|array
*/
public static function testDBConnection(
$host,
$port,
$socket,
$user,
$pass = null,
$errorKey = 'Server'
) {
if ($GLOBALS['cfg']['DBG']['demo']) {
// Connection test disabled on the demo server!
return true;
}
$error = null;
$host = Core::sanitizeMySQLHost($host);
error_clear_last();
$socket = empty($socket) ? null : $socket;
$port = empty($port) ? null : $port;
mysqli_report(MYSQLI_REPORT_OFF);
$conn = @mysqli_connect($host, $user, (string) $pass, '', $port, (string) $socket);
if (! $conn) {
$error = __('Could not connect to the database server!');
} else {
mysqli_close($conn);
}
if ($error !== null) {
$lastError = error_get_last();
if ($lastError !== null) {
$error .= ' - ' . $lastError['message'];
}
}
return $error === null ? true : [$errorKey => $error];
}
/**
* Validate server config
*
* @param string $path path to config, not used
* keep this parameter since the method is invoked using
* reflection along with other similar methods
* @param array $values config values
*
* @return array
*/
public static function validateServer($path, array $values)
{
$result = [
'Server' => '',
'Servers/1/user' => '',
'Servers/1/SignonSession' => '',
'Servers/1/SignonURL' => '',
];
$error = false;
if (empty($values['Servers/1/auth_type'])) {
$values['Servers/1/auth_type'] = '';
$result['Servers/1/auth_type'] = __('Invalid authentication type!');
$error = true;
}
if ($values['Servers/1/auth_type'] === 'config'
&& empty($values['Servers/1/user'])
) {
$result['Servers/1/user'] = __(
'Empty username while using [kbd]config[/kbd] authentication method!'
);
$error = true;
}
if ($values['Servers/1/auth_type'] === 'signon'
&& empty($values['Servers/1/SignonSession'])
) {
$result['Servers/1/SignonSession'] = __(
'Empty signon session name '
. 'while using [kbd]signon[/kbd] authentication method!'
);
$error = true;
}
if ($values['Servers/1/auth_type'] === 'signon'
&& empty($values['Servers/1/SignonURL'])
) {
$result['Servers/1/SignonURL'] = __(
'Empty signon URL while using [kbd]signon[/kbd] authentication '
. 'method!'
);
$error = true;
}
if (! $error && $values['Servers/1/auth_type'] === 'config') {
$password = '';
if (! empty($values['Servers/1/password'])) {
$password = $values['Servers/1/password'];
}
$test = static::testDBConnection(
empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
empty($values['Servers/1/user']) ? '' : $values['Servers/1/user'],
$password,
'Server'
);
if ($test !== true) {
$result = array_merge($result, $test);
}
}
return $result;
}
/**
* Validate pmadb config
*
* @param string $path path to config, not used
* keep this parameter since the method is invoked using
* reflection along with other similar methods
* @param array $values config values
*
* @return array
*/
public static function validatePMAStorage($path, array $values)
{
$result = [
'Server_pmadb' => '',
'Servers/1/controluser' => '',
'Servers/1/controlpass' => '',
];
$error = false;
if (empty($values['Servers/1/pmadb'])) {
return $result;
}
$result = [];
if (empty($values['Servers/1/controluser'])) {
$result['Servers/1/controluser'] = __(
'Empty phpMyAdmin control user while using phpMyAdmin configuration '
. 'storage!'
);
$error = true;
}
if (empty($values['Servers/1/controlpass'])) {
$result['Servers/1/controlpass'] = __(
'Empty phpMyAdmin control user password while using phpMyAdmin '
. 'configuration storage!'
);
$error = true;
}
if (! $error) {
$test = static::testDBConnection(
empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
empty($values['Servers/1/controluser']) ? '' : $values['Servers/1/controluser'],
empty($values['Servers/1/controlpass']) ? '' : $values['Servers/1/controlpass'],
'Server_pmadb'
);
if ($test !== true) {
$result = array_merge($result, $test);
}
}
return $result;
}
/**
* Validates regular expression
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateRegex($path, array $values)
{
$result = [$path => ''];
if (empty($values[$path])) {
return $result;
}
error_clear_last();
$matches = [];
// in libraries/ListDatabase.php _checkHideDatabase(),
// a '/' is used as the delimiter for hide_db
@preg_match('/' . Util::requestString($values[$path]) . '/', '', $matches);
$currentError = error_get_last();
if ($currentError !== null) {
$error = preg_replace('/^preg_match\(\): /', '', $currentError['message']);
return [$path => $error];
}
return $result;
}
/**
* Validates TrustedProxies field
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateTrustedProxies($path, array $values)
{
$result = [$path => []];
if (empty($values[$path])) {
return $result;
}
if (is_array($values[$path]) || is_object($values[$path])) {
// value already processed by FormDisplay::save
$lines = [];
foreach ($values[$path] as $ip => $v) {
$v = Util::requestString($v);
$lines[] = preg_match('/^-\d+$/', $ip)
? $v
: $ip . ': ' . $v;
}
} else {
// AJAX validation
$lines = explode("\n", $values[$path]);
}
foreach ($lines as $line) {
$line = trim($line);
$matches = [];
// we catch anything that may (or may not) be an IP
if (! preg_match('/^(.+):(?:[ ]?)\\w+$/', $line, $matches)) {
$result[$path][] = __('Incorrect value:') . ' '
. htmlspecialchars($line);
continue;
}
// now let's check whether we really have an IP address
if (filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false
&& filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false
) {
$ip = htmlspecialchars(trim($matches[1]));
$result[$path][] = sprintf(__('Incorrect IP address: %s'), $ip);
continue;
}
}
return $result;
}
/**
* Tests integer value
*
* @param string $path path to config
* @param array $values config values
* @param bool $allowNegative allow negative values
* @param bool $allowZero allow zero
* @param int $maxValue max allowed value
* @param string $errorString error message string
*
* @return string empty string if test is successful
*/
public static function validateNumber(
$path,
array $values,
$allowNegative,
$allowZero,
$maxValue,
$errorString
) {
if (empty($values[$path])) {
return '';
}
$value = Util::requestString($values[$path]);
if (intval($value) != $value
|| (! $allowNegative && $value < 0)
|| (! $allowZero && $value == 0)
|| $value > $maxValue
) {
return $errorString;
}
return '';
}
/**
* Validates port number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validatePortNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
false,
65535,
__('Not a valid port number!')
),
];
}
/**
* Validates positive number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validatePositiveNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
false,
PHP_INT_MAX,
__('Not a positive number!')
),
];
}
/**
* Validates non-negative number
*
* @param string $path path to config
* @param array $values config values
*
* @return array
*/
public static function validateNonNegativeNumber($path, array $values)
{
return [
$path => static::validateNumber(
$path,
$values,
false,
true,
PHP_INT_MAX,
__('Not a non-negative number!')
),
];
}
/**
* Validates value according to given regular expression
* Pattern and modifiers must be a valid for PCRE <b>and</b> JavaScript RegExp
*
* @param string $path path to config
* @param array $values config values
* @param string $regex regular expression to match
*
* @return array|string
*/
public static function validateByRegex($path, array $values, $regex)
{
if (! isset($values[$path])) {
return '';
}
$result = preg_match($regex, Util::requestString($values[$path]));
return [$path => $result ? '' : __('Incorrect value!')];
}
/**
* Validates upper bound for numeric inputs
*
* @param string $path path to config
* @param array $values config values
* @param int $maxValue maximal allowed value
*
* @return array
*/
public static function validateUpperBound($path, array $values, $maxValue)
{
$result = $values[$path] <= $maxValue;
return [
$path => $result ? '' : sprintf(
__('Value must be less than or equal to %s!'),
$maxValue
),
];
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* Used to render the console of PMA's pages
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use function count;
use function sprintf;
/**
* Class used to output the console
*/
class Console
{
/**
* Whether to display anything
*
* @access private
* @var bool
*/
private $isEnabled;
/**
* Whether we are servicing an ajax request.
*
* @access private
* @var bool
*/
private $isAjax;
/** @var Relation */
private $relation;
/** @var Template */
public $template;
/**
* Creates a new class instance
*/
public function __construct()
{
global $dbi;
$this->isEnabled = true;
$this->relation = new Relation($dbi);
$this->template = new Template();
}
/**
* Set the ajax flag to indicate whether
* we are servicing an ajax request
*
* @param bool $isAjax Whether we are servicing an ajax request
*/
public function setAjax(bool $isAjax): void
{
$this->isAjax = $isAjax;
}
/**
* Disables the rendering of the footer
*/
public function disable(): void
{
$this->isEnabled = false;
}
/**
* Renders the bookmark content
*
* @access public
*/
public static function getBookmarkContent(): string
{
global $dbi;
$template = new Template();
$cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']);
if ($cfgBookmark) {
$bookmarks = Bookmark::getList(
$dbi,
$GLOBALS['cfg']['Server']['user']
);
$count_bookmarks = count($bookmarks);
if ($count_bookmarks > 0) {
$welcomeMessage = sprintf(
_ngettext(
'Showing %1$d bookmark (both private and shared)',
'Showing %1$d bookmarks (both private and shared)',
$count_bookmarks
),
$count_bookmarks
);
} else {
$welcomeMessage = __('No bookmarks');
}
return $template->render('console/bookmark_content', [
'welcome_message' => $welcomeMessage,
'bookmarks' => $bookmarks,
]);
}
return '';
}
/**
* Returns the list of JS scripts required by console
*
* @return array list of scripts
*/
public function getScripts(): array
{
return ['console.js'];
}
/**
* Renders the console
*
* @access public
*/
public function getDisplay(): string
{
if (! $this->isAjax && $this->isEnabled) {
$cfgBookmark = Bookmark::getParams(
$GLOBALS['cfg']['Server']['user']
);
$image = Html\Generator::getImage('console', __('SQL Query Console'));
$_sql_history = $this->relation->getHistory(
$GLOBALS['cfg']['Server']['user']
);
$bookmarkContent = static::getBookmarkContent();
return $this->template->render('console/display', [
'cfg_bookmark' => $cfgBookmark,
'image' => $image,
'sql_history' => $_sql_history,
'bookmark_content' => $bookmarkContent,
]);
}
return '';
}
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function strlen;
abstract class AbstractController
{
/** @var Response */
protected $response;
/** @var Template */
protected $template;
/**
* @param Response $response
*/
public function __construct($response, Template $template)
{
$this->response = $response;
$this->template = $template;
}
/**
* @param array<string, mixed> $templateData
*/
protected function render(string $templatePath, array $templateData = []): void
{
$this->response->addHTML($this->template->render($templatePath, $templateData));
}
/**
* @param string[] $files
*/
protected function addScriptFiles(array $files): void
{
$header = $this->response->getHeader();
$scripts = $header->getScripts();
$scripts->addFiles($files);
}
protected function hasDatabase(): bool
{
global $db, $is_db, $errno, $dbi, $message;
if (isset($is_db) && $is_db) {
return true;
}
$is_db = false;
if (strlen($db) > 0) {
$is_db = $dbi->selectDb($db);
// This "Command out of sync" 2014 error may happen, for example
// after calling a MySQL procedure; at this point we can't select
// the db but it's not necessarily wrong
if ($dbi->getError() && $errno == 2014) {
$is_db = true;
unset($errno);
}
}
if (strlen($db) === 0 || ! $is_db) {
if ($this->response->isAjax()) {
$this->response->setRequestStatus(false);
$this->response->addJSON(
'message',
Message::error(__('No databases selected.'))
);
return false;
}
// Not a valid db name -> back to the welcome page
$params = ['reload' => '1'];
if (isset($message)) {
$params['message'] = $message;
}
$uri = './index.php?route=/' . Url::getCommonRaw($params, '&');
Core::sendHeaderLocation($uri);
return false;
}
return $is_db;
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\BrowseForeigners;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
/**
* Display selection for relational field values
*/
class BrowseForeignersController extends AbstractController
{
/** @var BrowseForeigners */
private $browseForeigners;
/** @var Relation */
private $relation;
/**
* @param Response $response
* @param BrowseForeigners $browseForeigners
* @param Relation $relation
*/
public function __construct($response, Template $template, $browseForeigners, $relation)
{
parent::__construct($response, $template);
$this->browseForeigners = $browseForeigners;
$this->relation = $relation;
}
public function index(): void
{
$params = [
'db' => $_POST['db'] ?? null,
'table' => $_POST['table'] ?? null,
'field' => $_POST['field'] ?? null,
'fieldkey' => $_POST['fieldkey'] ?? null,
'data' => $_POST['data'] ?? null,
'foreign_showAll' => $_POST['foreign_showAll'] ?? null,
'foreign_filter' => $_POST['foreign_filter'] ?? null,
];
if (! isset($params['db'], $params['table'], $params['field'])) {
return;
}
$this->response->getFooter()->setMinimal();
$header = $this->response->getHeader();
$header->disableMenuAndConsole();
$header->setBodyId('body_browse_foreigners');
$foreigners = $this->relation->getForeigners(
$params['db'],
$params['table']
);
$foreignLimit = $this->browseForeigners->getForeignLimit(
$params['foreign_showAll']
);
$foreignData = $this->relation->getForeignData(
$foreigners,
$params['field'],
true,
$params['foreign_filter'] ?? '',
$foreignLimit ?? null,
true
);
$this->response->addHTML($this->browseForeigners->getHtmlForRelationalFieldSelection(
$params['db'],
$params['table'],
$params['field'],
$foreignData,
$params['fieldkey'] ?? '',
$params['data'] ?? ''
));
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Simple script to set correct charset for changelog
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function array_keys;
use function file_get_contents;
use function htmlspecialchars;
use function is_readable;
use function ob_get_clean;
use function ob_start;
use function preg_replace;
use function printf;
use function readgzfile;
use function substr;
class ChangeLogController extends AbstractController
{
public function index(): void
{
$this->response->disable();
$this->response->getHeader()->sendHttpHeaders();
$filename = CHANGELOG_FILE;
/**
* Read changelog.
*/
// Check if the file is available, some distributions remove these.
if (! @is_readable($filename)) {
printf(
__(
'The %s file is not available on this system, please visit ' .
'%s for more information.'
),
$filename,
'<a href="https://www.phpmyadmin.net/">phpmyadmin.net</a>'
);
return;
}
// Test if the if is in a compressed format
if (substr($filename, -3) === '.gz') {
ob_start();
readgzfile($filename);
$changelog = ob_get_clean();
} else {
$changelog = file_get_contents($filename);
}
/**
* Whole changelog in variable.
*/
$changelog = htmlspecialchars((string) $changelog);
$github_url = 'https://github.com/phpmyadmin/phpmyadmin/';
$faq_url = 'https://docs.phpmyadmin.net/en/latest/faq.html';
$replaces = [
'@(https?://[./a-zA-Z0-9.-_-]*[/a-zA-Z0-9_])@'
=> '<a href="url.php?url=\\1">\\1</a>',
// mail address
'/([0-9]{4}-[0-9]{2}-[0-9]{2}) (.+[^ ]) +&lt;(.*@.*)&gt;/i'
=> '\\1 <a href="mailto:\\3">\\2</a>',
// FAQ entries
'/FAQ ([0-9]+)\.([0-9a-z]+)/i'
=> '<a href="url.php?url=' . $faq_url . '#faq\\1-\\2">FAQ \\1.\\2</a>',
// GitHub issues
'/issue\s*#?([0-9]{4,5}) /i'
=> '<a href="url.php?url=' . $github_url . 'issues/\\1">issue #\\1</a> ',
// CVE/CAN entries
'/((CAN|CVE)-[0-9]+-[0-9]+)/'
=> '<a href="url.php?url=https://cve.mitre.org/cgi-bin/cvename.cgi?name=\\1">\\1</a>',
// PMASAentries
'/(PMASA-[0-9]+-[0-9]+)/'
=> '<a href="url.php?url=https://www.phpmyadmin.net/security/\\1/">\\1</a>',
// Highlight releases (with links)
'/([0-9]+)\.([0-9]+)\.([0-9]+)\.0 (\([0-9-]+\))/'
=> '<a id="\\1_\\2_\\3"></a>'
. '<a href="url.php?url=' . $github_url . 'commits/RELEASE_\\1_\\2_\\3">'
. '\\1.\\2.\\3.0 \\4</a>',
'/([0-9]+)\.([0-9]+)\.([0-9]+)\.([1-9][0-9]*) (\([0-9-]+\))/'
=> '<a id="\\1_\\2_\\3_\\4"></a>'
. '<a href="url.php?url=' . $github_url . 'commits/RELEASE_\\1_\\2_\\3_\\4">'
. '\\1.\\2.\\3.\\4 \\5</a>',
// Highlight releases (not linkable)
'/( ### )(.*)/' => '\\1<b>\\2</b>',
// Links target and rel
'/a href="/' => 'a target="_blank" rel="noopener noreferrer" href="',
];
$this->response->header('Content-type: text/html; charset=utf-8');
echo $this->template->render('changelog', [
'changelog' => preg_replace(array_keys($replaces), $replaces, $changelog),
]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* Displays status of phpMyAdmin configuration storage
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
class CheckRelationsController extends AbstractController
{
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Relation $relation)
{
parent::__construct($response, $template);
$this->relation = $relation;
}
public function index(): void
{
global $db;
$params = [
'create_pmadb' => $_POST['create_pmadb'] ?? null,
'fixall_pmadb' => $_POST['fixall_pmadb'] ?? null,
'fix_pmadb' => $_POST['fix_pmadb'] ?? null,
];
// If request for creating the pmadb
if (isset($params['create_pmadb']) && $this->relation->createPmaDatabase()) {
$this->relation->fixPmaTables('phpmyadmin');
}
// If request for creating all PMA tables.
if (isset($params['fixall_pmadb'])) {
$this->relation->fixPmaTables($db);
}
$cfgRelation = $this->relation->getRelationsParam();
// If request for creating missing PMA tables.
if (isset($params['fix_pmadb'])) {
$this->relation->fixPmaTables($cfgRelation['db']);
}
$this->response->addHTML($this->relation->getRelationsParamDiagnostic($cfgRelation));
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
final class ColumnController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $dbi)
{
parent::__construct($response, $template);
$this->dbi = $dbi;
}
public function all(): void
{
if (! isset($_POST['db'], $_POST['table'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$this->response->addJSON(['columns' => $this->dbi->getColumnNames($_POST['db'], $_POST['table'])]);
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Config;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function json_decode;
final class ConfigController extends AbstractController
{
/** @var Config */
private $config;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Config $config)
{
parent::__construct($response, $template);
$this->config = $config;
}
public function get(): void
{
if (! isset($_POST['key'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$this->response->addJSON(['value' => $this->config->get($_POST['key'])]);
}
public function set(): void
{
if (! isset($_POST['key'], $_POST['value'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => Message::error()]);
return;
}
$result = $this->config->setUserValue(null, $_POST['key'], json_decode($_POST['value']));
if ($result === true) {
return;
}
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => $result]);
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Controllers\AbstractController as Controller;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
abstract class AbstractController extends Controller
{
/** @var string */
protected $db;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct($response, Template $template, $db)
{
parent::__construct($response, $template);
$this->db = $db;
}
}

View file

@ -0,0 +1,302 @@
<?php
/**
* Central Columns view/edit
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Core;
use PhpMyAdmin\Database\CentralColumns;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function is_bool;
use function parse_str;
use function sprintf;
class CentralColumnsController extends AbstractController
{
/** @var CentralColumns */
private $centralColumns;
/**
* @param Response $response
* @param string $db Database name
* @param CentralColumns $centralColumns
*/
public function __construct($response, Template $template, $db, $centralColumns)
{
parent::__construct($response, $template, $db);
$this->centralColumns = $centralColumns;
}
public function index(): void
{
global $cfg, $db, $message, $pos, $num_cols;
if (isset($_POST['edit_save'])) {
echo $this->editSave([
'col_name' => $_POST['col_name'] ?? null,
'orig_col_name' => $_POST['orig_col_name'] ?? null,
'col_default' => $_POST['col_default'] ?? null,
'col_default_sel' => $_POST['col_default_sel'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
'col_isNull' => $_POST['col_isNull'] ?? null,
'col_length' => $_POST['col_length'] ?? null,
'col_attribute' => $_POST['col_attribute'] ?? null,
'col_type' => $_POST['col_type'] ?? null,
'collation' => $_POST['collation'] ?? null,
]);
return;
}
if (isset($_POST['add_new_column'])) {
$tmp_msg = $this->addNewColumn([
'col_name' => $_POST['col_name'] ?? null,
'col_default' => $_POST['col_default'] ?? null,
'col_default_sel' => $_POST['col_default_sel'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
'col_isNull' => $_POST['col_isNull'] ?? null,
'col_length' => $_POST['col_length'] ?? null,
'col_attribute' => $_POST['col_attribute'] ?? null,
'col_type' => $_POST['col_type'] ?? null,
'collation' => $_POST['collation'] ?? null,
]);
}
if (isset($_POST['getColumnList'])) {
$this->response->addJSON('message', $this->getColumnList([
'cur_table' => $_POST['cur_table'] ?? null,
]));
return;
}
if (isset($_POST['add_column'])) {
$tmp_msg = $this->addColumn([
'table-select' => $_POST['table-select'] ?? null,
'column-select' => $_POST['column-select'] ?? null,
]);
}
$this->addScriptFiles([
'vendor/jquery/jquery.uitablefilter.js',
'vendor/jquery/jquery.tablesorter.js',
'database/central_columns.js',
]);
if (isset($_POST['edit_central_columns_page'])) {
$this->editPage([
'selected_fld' => $_POST['selected_fld'] ?? null,
'db' => $_POST['db'] ?? null,
]);
return;
}
if (isset($_POST['multi_edit_central_column_save'])) {
$message = $this->updateMultipleColumn([
'db' => $_POST['db'] ?? null,
'orig_col_name' => $_POST['orig_col_name'] ?? null,
'field_name' => $_POST['field_name'] ?? null,
'field_default_type' => $_POST['field_default_type'] ?? null,
'field_default_value' => $_POST['field_default_value'] ?? null,
'field_length' => $_POST['field_length'] ?? null,
'field_attribute' => $_POST['field_attribute'] ?? null,
'field_type' => $_POST['field_type'] ?? null,
'field_collation' => $_POST['field_collation'] ?? null,
'field_null' => $_POST['field_null'] ?? null,
'col_extra' => $_POST['col_extra'] ?? null,
]);
if (! is_bool($message)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
}
}
if (isset($_POST['delete_save'])) {
$tmp_msg = $this->deleteSave([
'db' => $_POST['db'] ?? null,
'col_name' => $_POST['col_name'] ?? null,
]);
}
$this->main([
'pos' => $_POST['pos'] ?? null,
'total_rows' => $_POST['total_rows'] ?? null,
]);
$pos = 0;
if (Core::isValid($_POST['pos'], 'integer')) {
$pos = (int) $_POST['pos'];
}
$num_cols = $this->centralColumns->getColumnsCount(
$db,
$pos,
(int) $cfg['MaxRows']
);
$message = Message::success(
sprintf(__('Showing rows %1$s - %2$s.'), $pos + 1, $pos + $num_cols)
);
if (! isset($tmp_msg) || $tmp_msg === true) {
return;
}
$message = $tmp_msg;
}
/**
* @param array $params Request parameters
*/
public function main(array $params): void
{
global $text_dir, $PMA_Theme;
if (! empty($params['total_rows'])
&& Core::isValid($params['total_rows'], 'integer')
) {
$totalRows = (int) $params['total_rows'];
} else {
$totalRows = $this->centralColumns->getCount($this->db);
}
$pos = 0;
if (Core::isValid($params['pos'], 'integer')) {
$pos = (int) $params['pos'];
}
$variables = $this->centralColumns->getTemplateVariablesForMain(
$this->db,
$totalRows,
$pos,
$PMA_Theme->getImgPath(),
$text_dir
);
$this->render('database/central_columns/main', $variables);
}
/**
* @param array $params Request parameters
*
* @return array JSON
*/
public function getColumnList(array $params): array
{
return $this->centralColumns->getListRaw(
$this->db,
$params['cur_table'] ?? ''
);
}
public function populateColumns(): void
{
$columns = $this->centralColumns->getColumnsNotInCentralList($this->db, $_POST['selectedTable']);
$this->render('database/central_columns/populate_columns', ['columns' => $columns]);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function editSave(array $params)
{
$columnDefault = $params['col_default'];
if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') {
$columnDefault = '';
}
return $this->centralColumns->updateOneColumn(
$this->db,
$params['orig_col_name'],
$params['col_name'],
$params['col_type'],
$params['col_attribute'],
$params['col_length'],
isset($params['col_isNull']) ? 1 : 0,
$params['collation'],
$params['col_extra'] ?? '',
$columnDefault
);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function addNewColumn(array $params)
{
$columnDefault = $params['col_default'];
if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') {
$columnDefault = '';
}
return $this->centralColumns->updateOneColumn(
$this->db,
'',
$params['col_name'],
$params['col_type'],
$params['col_attribute'],
$params['col_length'],
isset($params['col_isNull']) ? 1 : 0,
$params['collation'],
$params['col_extra'] ?? '',
$columnDefault
);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function addColumn(array $params)
{
return $this->centralColumns->syncUniqueColumns(
[$params['column-select']],
false,
$params['table-select']
);
}
/**
* @param array $params Request parameters
*/
public function editPage(array $params): void
{
$rows = $this->centralColumns->getHtmlForEditingPage(
$params['selected_fld'],
$params['db']
);
$this->render('database/central_columns/edit', ['rows' => $rows]);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function updateMultipleColumn(array $params)
{
return $this->centralColumns->updateMultipleColumn($params);
}
/**
* @param array $params Request parameters
*
* @return true|Message
*/
public function deleteSave(array $params)
{
$name = [];
parse_str($params['col_name'], $name);
return $this->centralColumns->deleteColumnsFromList(
$params['db'],
$name['selected_fld'],
false
);
}
}

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Index;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Util;
use function is_array;
use function str_replace;
class DataDictionaryController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var Transformations */
private $transformations;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param Relation $relation
* @param Transformations $transformations
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $relation, $transformations, $dbi)
{
parent::__construct($response, $template, $db);
$this->relation = $relation;
$this->transformations = $transformations;
$this->dbi = $dbi;
}
public function index(): void
{
Util::checkParameters(['db'], true);
$header = $this->response->getHeader();
$header->enablePrintView();
$cfgRelation = $this->relation->getRelationsParam();
$comment = $this->relation->getDbComment($this->db);
$this->dbi->selectDb($this->db);
$tablesNames = $this->dbi->getTables($this->db);
$tables = [];
foreach ($tablesNames as $tableName) {
$showComment = (string) $this->dbi->getTable(
$this->db,
$tableName
)->getStatusInfo('TABLE_COMMENT');
[, $primaryKeys] = Util::processIndexData(
$this->dbi->getTableIndexes($this->db, $tableName)
);
[$foreigners, $hasRelation] = $this->relation->getRelationsAndStatus(
! empty($cfgRelation['relation']),
$this->db,
$tableName
);
$columnsComments = $this->relation->getComments($this->db, $tableName);
$columns = $this->dbi->getColumns($this->db, $tableName);
$rows = [];
foreach ($columns as $row) {
$extractedColumnSpec = Util::extractColumnSpec($row['Type']);
$relation = '';
if ($hasRelation) {
$foreigner = $this->relation->searchColumnInForeigners(
$foreigners,
$row['Field']
);
if (is_array($foreigner) && isset($foreigner['foreign_table'], $foreigner['foreign_field'])) {
$relation = $foreigner['foreign_table'];
$relation .= ' -> ';
$relation .= $foreigner['foreign_field'];
}
}
$mime = '';
if ($cfgRelation['mimework']) {
$mimeMap = $this->transformations->getMime(
$this->db,
$tableName,
true
);
if (is_array($mimeMap) && isset($mimeMap[$row['Field']]['mimetype'])) {
$mime = str_replace(
'_',
'/',
$mimeMap[$row['Field']]['mimetype']
);
}
}
$rows[$row['Field']] = [
'name' => $row['Field'],
'has_primary_key' => isset($primaryKeys[$row['Field']]),
'type' => $extractedColumnSpec['type'],
'print_type' => $extractedColumnSpec['print_type'],
'is_nullable' => $row['Null'] !== '' && $row['Null'] !== 'NO',
'default' => $row['Default'] ?? null,
'comment' => $columnsComments[$row['Field']] ?? '',
'mime' => $mime,
'relation' => $relation,
];
}
$tables[$tableName] = [
'name' => $tableName,
'comment' => $showComment,
'has_relation' => $hasRelation,
'has_mime' => $cfgRelation['mimework'],
'columns' => $rows,
'indexes' => Index::getFromTable($tableName, $this->db),
];
}
$this->render('database/data_dictionary/index', [
'database' => $this->db,
'comment' => $comment,
'tables' => $tables,
]);
}
}

View file

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Designer;
use PhpMyAdmin\Database\Designer\Common as DesignerCommon;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function htmlspecialchars;
use function in_array;
use function sprintf;
class DesignerController extends AbstractController
{
/** @var Designer */
private $databaseDesigner;
/** @var DesignerCommon */
private $designerCommon;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct(
$response,
Template $template,
$db,
Designer $databaseDesigner,
DesignerCommon $designerCommon
) {
parent::__construct($response, $template, $db);
$this->databaseDesigner = $databaseDesigner;
$this->designerCommon = $designerCommon;
}
public function index(): void
{
global $db, $script_display_field, $tab_column, $tables_all_keys, $tables_pk_or_unique_keys;
global $success, $page, $message, $display_page, $selected_page, $tab_pos, $fullTableNames, $script_tables;
global $script_contr, $params, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos, $classes_side_menu, $cfg, $err_url;
if (isset($_POST['dialog'])) {
if ($_POST['dialog'] === 'edit') {
$html = $this->databaseDesigner->getHtmlForEditOrDeletePages($_POST['db'], 'editPage');
} elseif ($_POST['dialog'] === 'delete') {
$html = $this->databaseDesigner->getHtmlForEditOrDeletePages($_POST['db'], 'deletePage');
} elseif ($_POST['dialog'] === 'save_as') {
$html = $this->databaseDesigner->getHtmlForPageSaveAs($_POST['db']);
} elseif ($_POST['dialog'] === 'export') {
$html = $this->databaseDesigner->getHtmlForSchemaExport(
$_POST['db'],
$_POST['selected_page']
);
} elseif ($_POST['dialog'] === 'add_table') {
// Pass the db and table to the getTablesInfo so we only have the table we asked for
$script_display_field = $this->designerCommon->getTablesInfo($_POST['db'], $_POST['table']);
$tab_column = $this->designerCommon->getColumnsInfo($script_display_field);
$tables_all_keys = $this->designerCommon->getAllKeys($script_display_field);
$tables_pk_or_unique_keys = $this->designerCommon->getPkOrUniqueKeys($script_display_field);
$html = $this->databaseDesigner->getDatabaseTables(
$_POST['db'],
$script_display_field,
[],
-1,
$tab_column,
$tables_all_keys,
$tables_pk_or_unique_keys
);
}
if (! empty($html)) {
$this->response->addHTML($html);
}
return;
}
if (isset($_POST['operation'])) {
if ($_POST['operation'] === 'deletePage') {
$success = $this->designerCommon->deletePage($_POST['selected_page']);
$this->response->setRequestStatus($success);
} elseif ($_POST['operation'] === 'savePage') {
if ($_POST['save_page'] === 'same') {
$page = $_POST['selected_page'];
} elseif ($this->designerCommon->getPageExists($_POST['selected_value'])) {
$this->response->addJSON(
'message',
/* l10n: The user tries to save a page with an existing name in Designer */
__(
sprintf(
'There already exists a page named "%s" please rename it to something else.',
htmlspecialchars($_POST['selected_value'])
)
)
);
$this->response->setRequestStatus(false);
return;
} else {
$page = $this->designerCommon->createNewPage($_POST['selected_value'], $_POST['db']);
$this->response->addJSON('id', $page);
}
$success = $this->designerCommon->saveTablePositions($page);
$this->response->setRequestStatus($success);
} elseif ($_POST['operation'] === 'setDisplayField') {
[
$success,
$message,
] = $this->designerCommon->saveDisplayField(
$_POST['db'],
$_POST['table'],
$_POST['field']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'addNewRelation') {
[$success, $message] = $this->designerCommon->addNewRelation(
$_POST['db'],
$_POST['T1'],
$_POST['F1'],
$_POST['T2'],
$_POST['F2'],
$_POST['on_delete'],
$_POST['on_update'],
$_POST['DB1'],
$_POST['DB2']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'removeRelation') {
[$success, $message] = $this->designerCommon->removeRelation(
$_POST['T1'],
$_POST['F1'],
$_POST['T2'],
$_POST['F2']
);
$this->response->setRequestStatus($success);
$this->response->addJSON('message', $message);
} elseif ($_POST['operation'] === 'save_setting_value') {
$success = $this->designerCommon->saveSetting($_POST['index'], $_POST['value']);
$this->response->setRequestStatus($success);
}
return;
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$script_display_field = $this->designerCommon->getTablesInfo();
$display_page = -1;
$selected_page = null;
$visualBuilderMode = isset($_GET['query']);
if ($visualBuilderMode) {
$display_page = $this->designerCommon->getDefaultPage($_GET['db']);
} elseif (! empty($_GET['page'])) {
$display_page = $_GET['page'];
} else {
$display_page = $this->designerCommon->getLoadingPage($_GET['db']);
}
if ($display_page != -1) {
$selected_page = $this->designerCommon->getPageName($display_page);
}
$tab_pos = $this->designerCommon->getTablePositions($display_page);
$fullTableNames = [];
foreach ($script_display_field as $designerTable) {
$fullTableNames[] = $designerTable->getDbTableString();
}
foreach ($tab_pos as $position) {
if (in_array($position['dbName'] . '.' . $position['tableName'], $fullTableNames)) {
continue;
}
$designerTables = $this->designerCommon->getTablesInfo($position['dbName'], $position['tableName']);
foreach ($designerTables as $designerTable) {
$script_display_field[] = $designerTable;
}
}
$tab_column = $this->designerCommon->getColumnsInfo($script_display_field);
$script_tables = $this->designerCommon->getScriptTabs($script_display_field);
$tables_pk_or_unique_keys = $this->designerCommon->getPkOrUniqueKeys($script_display_field);
$tables_all_keys = $this->designerCommon->getAllKeys($script_display_field);
$classes_side_menu = $this->databaseDesigner->returnClassNamesFromMenuButtons();
$script_contr = $this->designerCommon->getScriptContr($script_display_field);
$params = ['lang' => $GLOBALS['lang']];
if (isset($_GET['db'])) {
$params['db'] = $_GET['db'];
}
$this->response->getFooter()->setMinimal();
$header = $this->response->getHeader();
$header->setBodyId('designer_body');
$this->addScriptFiles([
'vendor/jquery/jquery.fullscreen.js',
'designer/database.js',
'designer/objects.js',
'designer/page.js',
'designer/history.js',
'designer/move.js',
'designer/init.js',
]);
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
// Embed some data into HTML, later it will be read
// by designer/init.js and converted to JS variables.
$this->response->addHTML(
$this->databaseDesigner->getHtmlForMain(
$db,
$_GET['db'],
$script_display_field,
$script_tables,
$script_contr,
$script_display_field,
$display_page,
$visualBuilderMode,
$selected_page,
$classes_side_menu,
$tab_pos,
$tab_column,
$tables_all_keys,
$tables_pk_or_unique_keys
)
);
$this->response->addHTML('<div id="PMA_disable_floating_menubar"></div>');
}
}

View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Events;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function strlen;
final class EventsController extends AbstractController
{
/** @var Events */
private $events;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Events $events, $dbi)
{
parent::__construct($response, $template, $db);
$this->events = $events;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $tables, $num_tables, $total_num_tables, $sub_part, $errors, $text_dir, $PMA_Theme;
global $tooltip_truename, $tooltip_aliasname, $pos, $cfg, $err_url;
if (! $this->response->isAjax()) {
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$this->events->handleEditor();
$this->events->export();
$items = $this->dbi->getEvents($db);
$this->render('database/events/index', [
'db' => $db,
'items' => $items,
'select_all_arrow_src' => $PMA_Theme->getImgPath() . 'arrow_' . $text_dir . '.png',
'has_privilege' => Util::currentUserHasPrivilege('EVENT', $db),
'scheduler_state' => $this->events->getEventSchedulerStatus(),
'text_dir' => $text_dir,
'theme_image_path' => $PMA_Theme->getImgPath(),
'is_ajax' => $this->response->isAjax() && empty($_REQUEST['ajax_page_request']),
]);
}
}

View file

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Export;
use PhpMyAdmin\Export\Options;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function array_merge;
use function is_array;
final class ExportController extends AbstractController
{
/** @var Export */
private $export;
/** @var Options */
private $exportOptions;
/**
* @param Response $response
* @param string $db Database name.
*/
public function __construct($response, Template $template, $db, Export $export, Options $exportOptions)
{
parent::__construct($response, $template, $db);
$this->export = $export;
$this->exportOptions = $exportOptions;
}
public function index(): void
{
global $db, $table, $sub_part, $url_params, $sql_query;
global $tables, $num_tables, $total_num_tables, $tooltip_truename;
global $tooltip_aliasname, $pos, $table_select, $unlim_num_rows, $cfg, $err_url;
$pageSettings = new PageSettings('Export');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['export.js']);
// $sub_part is used in Util::getDbInfo() to see if we are coming from
// /database/export, in which case we don't obey $cfg['MaxTableList']
$sub_part = '_export';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/export');
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
// exit if no tables in db found
if ($num_tables < 1) {
$this->response->addHTML(
Message::error(__('No tables found in database.'))->getDisplay()
);
return;
}
if (! empty($_POST['selected_tbl']) && empty($table_select)) {
$table_select = $_POST['selected_tbl'];
}
$tablesForMultiValues = [];
foreach ($tables as $each_table) {
if (isset($_POST['table_select']) && is_array($_POST['table_select'])) {
$is_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_select']
);
} elseif (isset($table_select)) {
$is_checked = $this->export->getCheckedClause(
$each_table['Name'],
$table_select
);
} else {
$is_checked = true;
}
if (isset($_POST['table_structure']) && is_array($_POST['table_structure'])) {
$structure_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_structure']
);
} else {
$structure_checked = $is_checked;
}
if (isset($_POST['table_data']) && is_array($_POST['table_data'])) {
$data_checked = $this->export->getCheckedClause(
$each_table['Name'],
$_POST['table_data']
);
} else {
$data_checked = $is_checked;
}
$tablesForMultiValues[] = [
'name' => $each_table['Name'],
'is_checked_select' => $is_checked,
'is_checked_structure' => $structure_checked,
'is_checked_data' => $data_checked,
];
}
if (! isset($sql_query)) {
$sql_query = '';
}
if (! isset($unlim_num_rows)) {
$unlim_num_rows = 0;
}
$isReturnBackFromRawExport = isset($_POST['export_type']) && $_POST['export_type'] === 'raw';
if (isset($_POST['raw_query']) || $isReturnBackFromRawExport) {
$export_type = 'raw';
} else {
$export_type = 'database';
}
$GLOBALS['single_table'] = $_POST['single_table'] ?? $_GET['single_table'] ?? $GLOBALS['single_table'] ?? null;
$exportList = Plugins::getExport($export_type, isset($GLOBALS['single_table']));
if (empty($exportList)) {
$this->response->addHTML(Message::error(
__('Could not load export plugins, please check your installation!')
)->getDisplay());
return;
}
$options = $this->exportOptions->getOptions(
$export_type,
$db,
$table,
$sql_query,
$num_tables,
$unlim_num_rows,
$exportList
);
$this->render('database/export/index', array_merge($options, [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'structure_or_data_forced' => $_POST['structure_or_data_forced'] ?? 0,
'tables' => $tablesForMultiValues,
]));
}
public function tables(): void
{
if (empty($_POST['selected_tbl'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', __('No table selected.'));
return;
}
$this->index();
}
}

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\Import;
use PhpMyAdmin\Import\Ajax;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function intval;
final class ImportController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $max_upload_size, $table, $tables, $num_tables, $total_num_tables, $cfg;
global $tooltip_truename, $tooltip_aliasname, $pos, $sub_part, $SESSION_KEY, $PMA_Theme, $err_url;
$pageSettings = new PageSettings('Import');
$pageSettingsErrorHtml = $pageSettings->getErrorHTML();
$pageSettingsHtml = $pageSettings->getHTML();
$this->addScriptFiles(['import.js']);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
[$SESSION_KEY, $uploadId] = Ajax::uploadProgressSetup();
$importList = Plugins::getImport('database');
if (empty($importList)) {
$this->response->addHTML(Message::error(__(
'Could not load import plugins, please check your installation!'
))->getDisplay());
return;
}
$offset = null;
if (Core::isValid($_REQUEST['offset'], 'numeric')) {
$offset = intval($_REQUEST['offset']);
}
$timeoutPassed = $_REQUEST['timeout_passed'] ?? null;
$localImportFile = $_REQUEST['local_import_file'] ?? null;
$compressions = Import::getCompressions();
$allCharsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$charsets = [];
/** @var Charset $charset */
foreach ($allCharsets as $charset) {
$charsets[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
];
}
$idKey = $_SESSION[$SESSION_KEY]['handler']::getIdKey();
$hiddenInputs = [
$idKey => $uploadId,
'import_type' => 'database',
'db' => $db,
];
$this->render('database/import/index', [
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'upload_id' => $uploadId,
'handler' => $_SESSION[$SESSION_KEY]['handler'],
'theme_image_path' => $PMA_Theme->getImgPath(),
'hidden_inputs' => $hiddenInputs,
'db' => $db,
'table' => $table,
'max_upload_size' => $max_upload_size,
'import_list' => $importList,
'local_import_file' => $localImportFile,
'is_upload' => $GLOBALS['is_upload'],
'upload_dir' => $cfg['UploadDir'] ?? null,
'timeout_passed_global' => $GLOBALS['timeout_passed'] ?? null,
'compressions' => $compressions,
'is_encoding_supported' => Encoding::isSupported(),
'encodings' => Encoding::listEncodings(),
'import_charset' => $cfg['Import']['charset'] ?? null,
'timeout_passed' => $timeoutPassed,
'offset' => $offset,
'can_convert_kanji' => Encoding::canConvertKanji(),
'charsets' => $charsets,
'is_foreign_key_check' => Util::isForeignKeyCheck(),
'user_upload_dir' => Util::userDir($cfg['UploadDir'] ?? ''),
'local_files' => Import::getLocalFiles($importList),
]);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\MultiTableQuery;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
/**
* Handles database multi-table querying
*/
class MultiTableQueryController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
$this->addScriptFiles([
'vendor/jquery/jquery.md5.js',
'database/multi_table_query.js',
'database/query_generator.js',
]);
$queryInstance = new MultiTableQuery($this->dbi, $this->template, $this->db);
$this->response->addHTML($queryInstance->getFormHtml());
}
public function displayResults(): void
{
global $PMA_Theme;
$params = [
'sql_query' => $_POST['sql_query'],
'db' => $_POST['db'] ?? $_GET['db'] ?? null,
];
$this->response->addHTML(MultiTableQuery::displayResults(
$params['sql_query'],
$params['db'],
$PMA_Theme->getImgPath()
));
}
public function table(): void
{
$params = [
'tables' => $_GET['tables'],
'db' => $_GET['db'] ?? null,
];
$constrains = $this->dbi->getForeignKeyConstrains(
$params['db'],
$params['tables']
);
$this->response->addJSON(['foreignKeyConstrains' => $constrains]);
}
}

View file

@ -0,0 +1,419 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Operations;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\Export\ExportSql;
use PhpMyAdmin\Query\Utilities;
use PhpMyAdmin\Relation;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function mb_strtolower;
use function strlen;
/**
* Handles miscellaneous database operations.
*/
class OperationsController extends AbstractController
{
/** @var Operations */
private $operations;
/** @var CheckUserPrivileges */
private $checkUserPrivileges;
/** @var Relation */
private $relation;
/** @var RelationCleanup */
private $relationCleanup;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct(
$response,
Template $template,
$db,
Operations $operations,
CheckUserPrivileges $checkUserPrivileges,
Relation $relation,
RelationCleanup $relationCleanup,
$dbi
) {
parent::__construct($response, $template, $db);
$this->operations = $operations;
$this->checkUserPrivileges = $checkUserPrivileges;
$this->relation = $relation;
$this->relationCleanup = $relationCleanup;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $server, $sql_query, $move, $message, $tables_full, $err_url;
global $export_sql_plugin, $views, $sqlConstratints, $local_query, $reload, $url_params, $tables;
global $total_num_tables, $sub_part, $tooltip_truename;
global $db_collation, $tooltip_aliasname, $pos, $is_information_schema, $single_table, $num_tables;
$this->checkUserPrivileges->getPrivileges();
$this->addScriptFiles(['database/operations.js']);
$sql_query = '';
/**
* Rename/move or copy database
*/
if (strlen($db) > 0
&& (! empty($_POST['db_rename']) || ! empty($_POST['db_copy']))
) {
if (! empty($_POST['db_rename'])) {
$move = true;
} else {
$move = false;
}
if (! isset($_POST['newname']) || strlen($_POST['newname']) === 0) {
$message = Message::error(__('The database name is empty!'));
} else {
// lower_case_table_names=1 `DB` becomes `db`
if ($this->dbi->getLowerCaseNames() === '1') {
$_POST['newname'] = mb_strtolower(
$_POST['newname']
);
}
if ($_POST['newname'] === $_REQUEST['db']) {
$message = Message::error(
__('Cannot copy database to the same name. Change the name and try again.')
);
} else {
$_error = false;
if ($move || ! empty($_POST['create_database_before_copying'])) {
$this->operations->createDbBeforeCopy();
}
// here I don't use DELIMITER because it's not part of the
// language; I have to send each statement one by one
// to avoid selecting alternatively the current and new db
// we would need to modify the CREATE definitions to qualify
// the db name
$this->operations->runProcedureAndFunctionDefinitions($db);
// go back to current db, just in case
$this->dbi->selectDb($db);
$tables_full = $this->dbi->getTablesFull($db);
// remove all foreign key constraints, otherwise we can get errors
/** @var ExportSql $export_sql_plugin */
$export_sql_plugin = Plugins::getPlugin(
'export',
'sql',
'libraries/classes/Plugins/Export/',
[
'single_table' => isset($single_table),
'export_type' => 'database',
]
);
// create stand-in tables for views
$views = $this->operations->getViewsAndCreateSqlViewStandIn(
$tables_full,
$export_sql_plugin,
$db
);
// copy tables
$sqlConstratints = $this->operations->copyTables(
$tables_full,
$move,
$db
);
// handle the views
if (! $_error) {
$this->operations->handleTheViews($views, $move, $db);
}
unset($views);
// now that all tables exist, create all the accumulated constraints
if (! $_error && count($sqlConstratints) > 0) {
$this->operations->createAllAccumulatedConstraints($sqlConstratints);
}
unset($sqlConstratints);
if ($this->dbi->getVersion() >= 50100) {
// here DELIMITER is not used because it's not part of the
// language; each statement is sent one by one
$this->operations->runEventDefinitionsForDb($db);
}
// go back to current db, just in case
$this->dbi->selectDb($db);
// Duplicate the bookmarks for this db (done once for each db)
$this->operations->duplicateBookmarks($_error, $db);
if (! $_error && $move) {
if (isset($_POST['adjust_privileges'])
&& ! empty($_POST['adjust_privileges'])
) {
$this->operations->adjustPrivilegesMoveDb($db, $_POST['newname']);
}
/**
* cleanup pmadb stuff for this db
*/
$this->relationCleanup->database($db);
// if someday the RENAME DATABASE reappears, do not DROP
$local_query = 'DROP DATABASE '
. Util::backquote($db) . ';';
$sql_query .= "\n" . $local_query;
$this->dbi->query($local_query);
$message = Message::success(
__('Database %1$s has been renamed to %2$s.')
);
$message->addParam($db);
$message->addParam($_POST['newname']);
} elseif (! $_error) {
if (isset($_POST['adjust_privileges'])
&& ! empty($_POST['adjust_privileges'])
) {
$this->operations->adjustPrivilegesCopyDb($db, $_POST['newname']);
}
$message = Message::success(
__('Database %1$s has been copied to %2$s.')
);
$message->addParam($db);
$message->addParam($_POST['newname']);
} else {
$message = Message::error();
}
$reload = true;
/* Change database to be used */
if (! $_error && $move) {
$db = $_POST['newname'];
} elseif (! $_error) {
if (isset($_POST['switch_to_new'])
&& $_POST['switch_to_new'] === 'true'
) {
$_SESSION['pma_switch_to_new'] = true;
$db = $_POST['newname'];
} else {
$_SESSION['pma_switch_to_new'] = false;
}
}
}
}
/**
* Database has been successfully renamed/moved. If in an Ajax request,
* generate the output with {@link Response} and exit
*/
if ($this->response->isAjax()) {
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('newname', $_POST['newname']);
$this->response->addJSON(
'sql_query',
Generator::getMessage('', $sql_query)
);
$this->response->addJSON('db', $db);
return;
}
}
/**
* Settings for relations stuff
*/
$cfgRelation = $this->relation->getRelationsParam();
/**
* Check if comments were updated
* (must be done before displaying the menu tabs)
*/
if (isset($_POST['comment'])) {
$this->relation->setDbComment($db, $_POST['comment']);
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/operations');
// Gets the database structure
$sub_part = '_structure';
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,
$isSystemSchema,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
$oldMessage = '';
if (isset($message)) {
$oldMessage = Generator::getMessage($message, $sql_query);
unset($message);
}
$db_collation = $this->dbi->getDbCollation($db);
$is_information_schema = Utilities::isSystemSchema($db);
if ($is_information_schema) {
return;
}
$databaseComment = '';
if ($cfgRelation['commwork']) {
$databaseComment = $this->relation->getDbComment($db);
}
$hasAdjustPrivileges = $GLOBALS['db_priv'] && $GLOBALS['table_priv']
&& $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] && $GLOBALS['is_reload_priv'];
$isDropDatabaseAllowed = ($this->dbi->isSuperUser() || $cfg['AllowUserDropDatabase'])
&& ! $isSystemSchema && $db !== 'mysql';
$switchToNew = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new'];
$charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
if (! $cfgRelation['allworks']
&& $cfg['PmaNoRelation_DisableWarning'] == false
) {
$message = Message::notice(
__(
'The phpMyAdmin configuration storage has been deactivated. ' .
'%sFind out why%s.'
)
);
$message->addParamHtml(
'<a href="' . Url::getFromRoute('/check-relations')
. '" data-post="' . Url::getCommon(['db' => $db]) . '">'
);
$message->addParamHtml('</a>');
/* Show error if user has configured something, notice elsewhere */
if (! empty($cfg['Servers'][$server]['pmadb'])) {
$message->isError(true);
}
}
$this->render('database/operations/index', [
'message' => $oldMessage,
'db' => $db,
'has_comment' => $cfgRelation['commwork'],
'db_comment' => $databaseComment,
'db_collation' => $db_collation,
'has_adjust_privileges' => $hasAdjustPrivileges,
'is_drop_database_allowed' => $isDropDatabaseAllowed,
'switch_to_new' => $switchToNew,
'charsets' => $charsets,
'collations' => $collations,
]);
}
public function collation(): void
{
global $db, $cfg, $err_url;
if (! $this->response->isAjax()) {
return;
}
if (empty($_POST['db_collation'])) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No collation provided.')));
return;
}
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$sql_query = 'ALTER DATABASE ' . Util::backquote($db)
. ' DEFAULT' . Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
$this->dbi->query($sql_query);
$message = Message::success();
/**
* Changes tables charset if requested by the user
*/
if (isset($_POST['change_all_tables_collations']) &&
$_POST['change_all_tables_collations'] === 'on'
) {
[$tables] = Util::getDbInfo($db, null);
foreach ($tables as $tableName => $data) {
if ($this->dbi->getTable($db, $tableName)->isView()) {
// Skip views, we can not change the collation of a view.
// issue #15283
continue;
}
$sql_query = 'ALTER TABLE '
. Util::backquote($db)
. '.'
. Util::backquote($tableName)
. ' DEFAULT '
. Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
$this->dbi->query($sql_query);
/**
* Changes columns charset if requested by the user
*/
if (! isset($_POST['change_all_tables_columns_collations']) ||
$_POST['change_all_tables_columns_collations'] !== 'on'
) {
continue;
}
$this->operations->changeAllColumnsCollation($db, $tableName, $_POST['db_collation']);
}
}
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* Controller for database privileges
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Privileges;
use PhpMyAdmin\Template;
use PhpMyAdmin\Util;
/**
* Controller for database privileges
*/
class PrivilegesController extends AbstractController
{
/** @var Privileges */
private $privileges;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Privileges $privileges, $dbi)
{
parent::__construct($response, $template, $db);
$this->privileges = $privileges;
$this->dbi = $dbi;
}
/**
* @param array $params Request parameters
*/
public function index(array $params): string
{
global $cfg, $text_dir, $PMA_Theme;
$scriptName = Util::getScriptNameForOption(
$cfg['DefaultTabDatabase'],
'database'
);
$privileges = [];
if ($this->dbi->isSuperUser()) {
$privileges = $this->privileges->getAllPrivileges($params['checkprivsdb']);
}
return $this->template->render('database/privileges/index', [
'is_superuser' => $this->dbi->isSuperUser(),
'db' => $params['checkprivsdb'],
'database_url' => $scriptName,
'theme_image_path' => $PMA_Theme->getImgPath(),
'text_dir' => $text_dir,
'is_createuser' => $this->dbi->isCreateUser(),
'is_grantuser' => $this->dbi->isGrantUser(),
'privileges' => $privileges,
]);
}
}

View file

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Qbe;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Operations;
use PhpMyAdmin\Relation;
use PhpMyAdmin\RelationCleanup;
use PhpMyAdmin\Response;
use PhpMyAdmin\SavedSearches;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Transformations;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function stripos;
class QueryByExampleController extends AbstractController
{
/** @var Relation */
private $relation;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Relation $relation, $dbi)
{
parent::__construct($response, $template, $db);
$this->relation = $relation;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $savedSearchList, $savedSearch, $currentSearchId, $PMA_Theme;
global $sql_query, $goto, $sub_part, $tables, $num_tables, $total_num_tables;
global $tooltip_truename, $tooltip_aliasname, $pos, $url_params, $cfg, $err_url;
// Gets the relation settings
$cfgRelation = $this->relation->getRelationsParam();
$savedSearchList = [];
$savedSearch = null;
$currentSearchId = null;
$this->addScriptFiles(['database/qbe.js']);
if ($cfgRelation['savedsearcheswork']) {
//Get saved search list.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
if (! empty($_POST['searchId'])) {
$savedSearch->setId($_POST['searchId']);
}
//Action field is sent.
if (isset($_POST['action'])) {
$savedSearch->setSearchName($_POST['searchName']);
if ($_POST['action'] === 'create') {
$saveResult = $savedSearch->setId(null)
->setCriterias($_POST)
->save();
} elseif ($_POST['action'] === 'update') {
$saveResult = $savedSearch->setCriterias($_POST)
->save();
} elseif ($_POST['action'] === 'delete') {
$deleteResult = $savedSearch->delete();
//After deletion, reset search.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
$_POST = [];
} elseif ($_POST['action'] === 'load') {
if (empty($_POST['searchId'])) {
//when not loading a search, reset the object.
$savedSearch = new SavedSearches($GLOBALS, $this->relation);
$savedSearch->setUsername($GLOBALS['cfg']['Server']['user'])
->setDbname($db);
$_POST = [];
} else {
$loadResult = $savedSearch->load();
}
}
//Else, it's an "update query"
}
$savedSearchList = $savedSearch->getList();
$currentSearchId = $savedSearch->getId();
}
/**
* A query has been submitted -> (maybe) execute it
*/
$hasMessageToDisplay = false;
if (isset($_POST['submit_sql']) && ! empty($sql_query)) {
if (stripos($sql_query, 'SELECT') !== 0) {
$hasMessageToDisplay = true;
} else {
$goto = Url::getFromRoute('/database/sql');
$sql = new Sql(
$this->dbi,
$this->relation,
new RelationCleanup($this->dbi, $this->relation),
new Operations($this->dbi, $this->relation),
new Transformations(),
$this->template
);
$this->response->addHTML($sql->executeQueryAndSendQueryResponse(
null, // analyzed_sql_results
false, // is_gotofile
$_POST['db'], // db
null, // table
false, // find_real_end
null, // sql_query_for_bookmark
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
$PMA_Theme->getImgPath(),
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
));
}
}
$sub_part = '_qbe';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/database/qbe');
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
$databaseQbe = new Qbe($this->relation, $this->template, $this->dbi, $db, $savedSearchList, $savedSearch);
$this->render('database/qbe/index', [
'url_params' => $url_params,
'has_message_to_display' => $hasMessageToDisplay,
'selection_form_html' => $databaseQbe->getSelectionForm(),
]);
}
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Core;
use PhpMyAdmin\Database\Routines;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function in_array;
use function strlen;
/**
* Routines management.
*/
class RoutinesController extends AbstractController
{
/** @var CheckUserPrivileges */
private $checkUserPrivileges;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, CheckUserPrivileges $checkUserPrivileges, $dbi)
{
parent::__construct($response, $template, $db);
$this->checkUserPrivileges = $checkUserPrivileges;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
global $errors, $PMA_Theme, $text_dir, $err_url, $url_params, $cfg;
$type = $_REQUEST['type'] ?? null;
$this->checkUserPrivileges->getPrivileges();
if (! $this->response->isAjax()) {
/**
* Displays the header and tabs
*/
if (! empty($table) && in_array($table, $this->dbi->getTables($db))) {
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$err_url .= Url::getCommon($url_params, '&');
DbTableExists::check();
} else {
$table = '';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$routines = new Routines($this->dbi, $this->template, $this->response);
$routines->handleEditor();
$routines->handleExecute();
$routines->export();
if (! Core::isValid($type, ['FUNCTION', 'PROCEDURE'])) {
$type = null;
}
$items = $this->dbi->getRoutines($db, $type);
$isAjax = $this->response->isAjax() && empty($_REQUEST['ajax_page_request']);
$rows = '';
foreach ($items as $item) {
$rows .= $routines->getRow($item, $isAjax ? 'ajaxInsert hide' : '');
}
$this->render('database/routines/index', [
'db' => $db,
'table' => $table,
'items' => $items,
'rows' => $rows,
'select_all_arrow_src' => $PMA_Theme->getImgPath() . 'arrow_' . $text_dir . '.png',
'has_privilege' => Util::currentUserHasPrivilege('CREATE ROUTINE', $db, $table),
]);
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Search;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
class SearchController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $err_url, $url_params, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
$this->addScriptFiles([
'database/search.js',
'vendor/stickyfill.min.js',
'sql.js',
'makegrid.js',
]);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
// If config variable $cfg['UseDbSearch'] is on false : exit.
if (! $cfg['UseDbSearch']) {
Generator::mysqlDie(
__('Access denied!'),
'',
false,
$err_url
);
}
$url_params['goto'] = Url::getFromRoute('/database/search');
// Create a database search instance
$databaseSearch = new Search($this->dbi, $db, $this->template);
// Display top links if we are not in an Ajax request
if (! $this->response->isAjax()) {
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
// Main search form has been submitted, get results
if (isset($_POST['submit_search'])) {
$this->response->addHTML($databaseSearch->getSearchResults());
}
// If we are in an Ajax request, we need to exit after displaying all the HTML
if ($this->response->isAjax() && empty($_REQUEST['ajax_page_request'])) {
return;
}
// Display the search form
$this->response->addHTML($databaseSearch->getMainHtml());
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function json_encode;
/**
* Table/Column autocomplete in SQL editors.
*/
class SqlAutoCompleteController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $db, $sql_autocomplete;
$sql_autocomplete = true;
if ($cfg['EnableAutocompleteForTablesAndColumns']) {
$db = $_POST['db'] ?? $db;
$sql_autocomplete = [];
if ($db) {
$tableNames = $this->dbi->getTables($db);
foreach ($tableNames as $tableName) {
$sql_autocomplete[$tableName] = $this->dbi->getColumns(
$db,
$tableName
);
}
}
}
$this->response->addJSON(['tables' => json_encode($sql_autocomplete)]);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Response;
use PhpMyAdmin\SqlQueryForm;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function htmlspecialchars;
/**
* Database SQL executor
*/
class SqlController extends AbstractController
{
/** @var SqlQueryForm */
private $sqlQueryForm;
/**
* @param Response $response
* @param string $db Database name
*/
public function __construct($response, Template $template, $db, SqlQueryForm $sqlQueryForm)
{
parent::__construct($response, $template, $db);
$this->sqlQueryForm = $sqlQueryForm;
}
public function index(): void
{
global $goto, $back, $db, $cfg, $err_url;
$this->addScriptFiles([
'makegrid.js',
'vendor/jquery/jquery.uitablefilter.js',
'vendor/stickyfill.min.js',
'sql.js',
]);
$pageSettings = new PageSettings('Sql');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addHTML($pageSettings->getHTML());
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
/**
* After a syntax error, we return to this script
* with the typed query in the textarea.
*/
$goto = Url::getFromRoute('/database/sql');
$back = $goto;
$this->response->addHTML($this->sqlQueryForm->getHtml(
true,
false,
isset($_POST['delimiter'])
? htmlspecialchars($_POST['delimiter'])
: ';'
));
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\SqlParser\Utils\Formatter;
use function strlen;
/**
* Format SQL for SQL editors.
*/
class SqlFormatController extends AbstractController
{
public function index(): void
{
$params = ['sql' => $_POST['sql'] ?? null];
$query = strlen((string) $params['sql']) > 0 ? $params['sql'] : '';
$this->response->addJSON(['sql' => Formatter::format($query)]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tracker;
use PhpMyAdmin\Tracking;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function count;
use function htmlspecialchars;
use function sprintf;
/**
* Tracking configuration for database.
*/
class TrackingController extends AbstractController
{
/** @var Tracking */
private $tracking;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, Tracking $tracking, $dbi)
{
parent::__construct($response, $template, $db);
$this->tracking = $tracking;
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $text_dir, $url_params, $tables, $num_tables, $PMA_Theme;
global $total_num_tables, $sub_part, $pos, $data, $cfg;
global $tooltip_truename, $tooltip_aliasname, $err_url;
$this->addScriptFiles(['vendor/jquery/jquery.tablesorter.js', 'database/tracking.js']);
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
$url_params['goto'] = Url::getFromRoute('/table/tracking');
$url_params['back'] = Url::getFromRoute('/database/tracking');
// Get the database structure
$sub_part = '_structure';
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,
$isSystemSchema,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
if (isset($_POST['delete_tracking'], $_POST['table'])) {
Tracker::deleteTracking($db, $_POST['table']);
echo Message::success(
__('Tracking data deleted successfully.')
)->getDisplay();
} elseif (isset($_POST['submit_create_version'])) {
$this->tracking->createTrackingForMultipleTables($_POST['selected']);
echo Message::success(
sprintf(
__(
'Version %1$s was created for selected tables,'
. ' tracking is active for them.'
),
htmlspecialchars($_POST['version'])
)
)->getDisplay();
} elseif (isset($_POST['submit_mult'])) {
if (! empty($_POST['selected_tbl'])) {
if ($_POST['submit_mult'] === 'delete_tracking') {
foreach ($_POST['selected_tbl'] as $table) {
Tracker::deleteTracking($db, $table);
}
echo Message::success(
__('Tracking data deleted successfully.')
)->getDisplay();
} elseif ($_POST['submit_mult'] === 'track') {
echo $this->template->render('create_tracking_version', [
'route' => '/database/tracking',
'url_params' => $url_params,
'last_version' => 0,
'db' => $db,
'selected' => $_POST['selected_tbl'],
'type' => 'both',
'default_statements' => $cfg['Server']['tracking_default_statements'],
]);
return;
}
} else {
echo Message::notice(
__('No tables selected.')
)->getDisplay();
}
}
// Get tracked data about the database
$data = Tracker::getTrackedData($db, '', '1');
// No tables present and no log exist
if ($num_tables == 0 && count($data['ddlog']) === 0) {
echo '<p>' , __('No tables found in database.') , '</p>' , "\n";
if (empty($isSystemSchema)) {
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
echo $this->template->render('database/create_table', ['db' => $db]);
}
return;
}
echo $this->tracking->getHtmlForDbTrackingTables(
$db,
$url_params,
$PMA_Theme->getImgPath(),
$text_dir
);
// If available print out database log
if (count($data['ddlog']) <= 0) {
return;
}
$log = '';
foreach ($data['ddlog'] as $entry) {
$log .= '# ' . $entry['date'] . ' ' . $entry['username'] . "\n"
. $entry['statement'] . "\n";
}
echo Generator::getMessage(__('Database Log'), $log);
}
}

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Database\Triggers;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function in_array;
use function strlen;
/**
* Triggers management.
*/
class TriggersController extends AbstractController
{
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param string $db Database name.
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $db, $dbi)
{
parent::__construct($response, $template, $db);
$this->dbi = $dbi;
}
public function index(): void
{
global $db, $table, $tables, $num_tables, $total_num_tables, $sub_part;
global $tooltip_truename, $tooltip_aliasname, $pos;
global $errors, $url_params, $err_url, $cfg;
if (! $this->response->isAjax()) {
/**
* Displays the header and tabs
*/
if (! empty($table) && in_array($table, $this->dbi->getTables($db))) {
Util::checkParameters(['db', 'table']);
$url_params = ['db' => $db, 'table' => $table];
$err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
$err_url .= Url::getCommon($url_params, '&');
DbTableExists::check();
} else {
$table = '';
Util::checkParameters(['db']);
$err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
$err_url .= Url::getCommon(['db' => $db], '&');
if (! $this->hasDatabase()) {
return;
}
[
$tables,
$num_tables,
$total_num_tables,
$sub_part,,,
$tooltip_truename,
$tooltip_aliasname,
$pos,
] = Util::getDbInfo($db, $sub_part ?? '');
}
} elseif (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
/**
* Keep a list of errors that occurred while
* processing an 'Add' or 'Edit' operation.
*/
$errors = [];
$triggers = new Triggers($this->dbi, $this->template, $this->response);
$triggers->main();
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
final class DatabaseController extends AbstractController
{
public function all(): void
{
global $dblist;
$this->response->addJSON(['databases' => $dblist->databases]);
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* Handle error report submission
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\ErrorHandler;
use PhpMyAdmin\ErrorReport;
use PhpMyAdmin\Message;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\UserPreferences;
use function count;
use function in_array;
use function is_string;
use function json_decode;
use function time;
/**
* Handle error report submission
*/
class ErrorReportController extends AbstractController
{
/** @var ErrorReport */
private $errorReport;
/** @var ErrorHandler */
private $errorHandler;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
ErrorReport $errorReport,
ErrorHandler $errorHandler
) {
parent::__construct($response, $template);
$this->errorReport = $errorReport;
$this->errorHandler = $errorHandler;
}
public function index(): void
{
global $cfg;
if (! isset($_POST['exception_type'])
|| ! in_array($_POST['exception_type'], ['js', 'php'])
) {
return;
}
if (isset($_POST['send_error_report'])
&& ($_POST['send_error_report'] == true
|| $_POST['send_error_report'] == '1')
) {
if ($_POST['exception_type'] === 'php') {
/**
* Prevent infinite error submission.
* Happens in case error submissions fails.
* If reporting is done in some time interval,
* just clear them & clear json data too.
*/
if (isset($_SESSION['prev_error_subm_time'], $_SESSION['error_subm_count'])
&& $_SESSION['error_subm_count'] >= 3
&& ($_SESSION['prev_error_subm_time'] - time()) <= 3000
) {
$_SESSION['error_subm_count'] = 0;
$_SESSION['prev_errors'] = '';
$this->response->addJSON('stopErrorReportLoop', '1');
} else {
$_SESSION['prev_error_subm_time'] = time();
$_SESSION['error_subm_count'] = isset($_SESSION['error_subm_count'])
? $_SESSION['error_subm_count'] + 1
: 0;
}
}
$reportData = $this->errorReport->getData($_POST['exception_type']);
// report if and only if there were 'actual' errors.
if (count($reportData) > 0) {
$server_response = $this->errorReport->send($reportData);
if (! is_string($server_response)) {
$success = false;
} else {
$decoded_response = json_decode($server_response, true);
$success = ! empty($decoded_response) ?
$decoded_response['success'] : false;
}
/* Message to show to the user */
if ($success) {
if ((isset($_POST['automatic'])
&& $_POST['automatic'] === 'true')
|| $cfg['SendErrorReports'] === 'always'
) {
$msg = __(
'An error has been detected and an error report has been '
. 'automatically submitted based on your settings.'
);
} else {
$msg = __('Thank you for submitting this report.');
}
} else {
$msg = __(
'An error has been detected and an error report has been '
. 'generated but failed to be sent.'
);
$msg .= ' ';
$msg .= __(
'If you experience any '
. 'problems please submit a bug report manually.'
);
}
$msg .= ' ' . __('You may want to refresh the page.');
/* Create message object */
if ($success) {
$msg = Message::notice($msg);
} else {
$msg = Message::error($msg);
}
/* Add message to response */
if ($this->response->isAjax()) {
if ($_POST['exception_type'] === 'js') {
$this->response->addJSON('message', $msg);
} else {
$this->response->addJSON('errSubmitMsg', $msg);
}
} elseif ($_POST['exception_type'] === 'php') {
$jsCode = 'Functions.ajaxShowMessage(\'<div class="alert alert-danger" role="alert">'
. $msg
. '</div>\', false);';
$this->response->getFooter()->getScripts()->addCode($jsCode);
}
if ($_POST['exception_type'] === 'php') {
// clear previous errors & save new ones.
$this->errorHandler->savePreviousErrors();
}
/* Persist always send settings */
if (isset($_POST['always_send'])
&& $_POST['always_send'] === 'true'
) {
$userPreferences = new UserPreferences();
$userPreferences->persistOption('SendErrorReports', 'always', 'ask');
}
}
} elseif (! empty($_POST['get_settings'])) {
$this->response->addJSON('report_setting', $cfg['SendErrorReports']);
} elseif ($_POST['exception_type'] === 'js') {
$this->response->addHTML($this->errorReport->getForm());
} else {
// clear previous errors & save new ones.
$this->errorHandler->savePreviousErrors();
}
}
}

View file

@ -0,0 +1,704 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Controllers\Database\ExportController as DatabaseExportController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\Exceptions\ExportException;
use PhpMyAdmin\Export;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\ExportPlugin;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
use PhpMyAdmin\SqlParser\Utils\Misc;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use const PHP_EOL;
use function count;
use function function_exists;
use function in_array;
use function ini_set;
use function is_array;
use function ob_end_clean;
use function ob_get_length;
use function ob_get_level;
use function register_shutdown_function;
use function strlen;
use function time;
final class ExportController extends AbstractController
{
/** @var Export */
private $export;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Export $export, Relation $relation)
{
parent::__construct($response, $template);
$this->export = $export;
$this->relation = $relation;
}
public function index(): void
{
global $containerBuilder, $db, $export_type, $filename_template, $sql_query, $err_url, $message;
global $compression, $crlf, $asfile, $buffer_needed, $save_on_server, $file_handle, $separate_files;
global $output_charset_conversion, $output_kanji_conversion, $table, $what, $export_plugin, $single_table;
global $compression_methods, $onserver, $back_button, $refreshButton, $save_filename, $filename;
global $quick_export, $cfg, $tables, $table_select, $aliases, $dump_buffer, $dump_buffer_len;
global $time_start, $charset, $remember_template, $mime_type, $num_tables, $dump_buffer_objects;
global $active_page, $do_relation, $do_comments, $do_mime, $do_dates, $whatStrucOrData, $db_select;
global $table_structure, $table_data, $lock_tables, $allrows, $limit_to, $limit_from;
$this->addScriptFiles(['export_output.js']);
/**
* Sets globals from $_POST
*
* - Please keep the parameters in order of their appearance in the form
* - Some of these parameters are not used, as the code below directly
* verifies from the superglobal $_POST or $_REQUEST
* TODO: this should be removed to avoid passing user input to GLOBALS
* without checking
*/
$post_params = [
'db',
'table',
'what',
'single_table',
'export_type',
'export_method',
'quick_or_custom',
'db_select',
'table_select',
'table_structure',
'table_data',
'limit_to',
'limit_from',
'allrows',
'lock_tables',
'output_format',
'filename_template',
'maxsize',
'remember_template',
'charset',
'compression',
'as_separate_files',
'knjenc',
'xkana',
'htmlword_structure_or_data',
'htmlword_null',
'htmlword_columns',
'mediawiki_headers',
'mediawiki_structure_or_data',
'mediawiki_caption',
'pdf_structure_or_data',
'odt_structure_or_data',
'odt_relation',
'odt_comments',
'odt_mime',
'odt_columns',
'odt_null',
'codegen_structure_or_data',
'codegen_format',
'excel_null',
'excel_removeCRLF',
'excel_columns',
'excel_edition',
'excel_structure_or_data',
'yaml_structure_or_data',
'ods_null',
'ods_structure_or_data',
'ods_columns',
'json_structure_or_data',
'json_pretty_print',
'json_unicode',
'xml_structure_or_data',
'xml_export_events',
'xml_export_functions',
'xml_export_procedures',
'xml_export_tables',
'xml_export_triggers',
'xml_export_views',
'xml_export_contents',
'texytext_structure_or_data',
'texytext_columns',
'texytext_null',
'phparray_structure_or_data',
'sql_include_comments',
'sql_header_comment',
'sql_dates',
'sql_relation',
'sql_mime',
'sql_use_transaction',
'sql_disable_fk',
'sql_compatibility',
'sql_structure_or_data',
'sql_create_database',
'sql_drop_table',
'sql_procedure_function',
'sql_create_table',
'sql_create_view',
'sql_create_trigger',
'sql_view_current_user',
'sql_simple_view_export',
'sql_if_not_exists',
'sql_or_replace_view',
'sql_auto_increment',
'sql_backquotes',
'sql_truncate',
'sql_delayed',
'sql_ignore',
'sql_type',
'sql_insert_syntax',
'sql_max_query_size',
'sql_hex_for_binary',
'sql_utc_time',
'sql_drop_database',
'sql_views_as_tables',
'sql_metadata',
'csv_separator',
'csv_enclosed',
'csv_escaped',
'csv_terminated',
'csv_null',
'csv_removeCRLF',
'csv_columns',
'csv_structure_or_data',
// csv_replace should have been here but we use it directly from $_POST
'latex_caption',
'latex_structure_or_data',
'latex_structure_caption',
'latex_structure_continued_caption',
'latex_structure_label',
'latex_relation',
'latex_comments',
'latex_mime',
'latex_columns',
'latex_data_caption',
'latex_data_continued_caption',
'latex_data_label',
'latex_null',
'aliases',
];
foreach ($post_params as $one_post_param) {
if (! isset($_POST[$one_post_param])) {
continue;
}
$GLOBALS[$one_post_param] = $_POST[$one_post_param];
}
Util::checkParameters(['what', 'export_type']);
// sanitize this parameter which will be used below in a file inclusion
$what = Core::securePath($_POST['what']);
// export class instance, not array of properties, as before
/** @var ExportPlugin $export_plugin */
$export_plugin = Plugins::getPlugin(
'export',
$what,
'libraries/classes/Plugins/Export/',
[
'export_type' => $export_type,
'single_table' => isset($single_table),
]
);
// Check export type
if (empty($export_plugin)) {
Core::fatalError(__('Bad type!'));
}
/**
* valid compression methods
*/
$compression_methods = [];
if ($GLOBALS['cfg']['ZipDump'] && function_exists('gzcompress')) {
$compression_methods[] = 'zip';
}
if ($GLOBALS['cfg']['GZipDump'] && function_exists('gzencode')) {
$compression_methods[] = 'gzip';
}
/**
* init and variable checking
*/
$compression = '';
$onserver = false;
$save_on_server = false;
$buffer_needed = false;
$back_button = '';
$refreshButton = '';
$save_filename = '';
$file_handle = '';
$err_url = '';
$filename = '';
$separate_files = '';
// Is it a quick or custom export?
if (isset($_POST['quick_or_custom'])
&& $_POST['quick_or_custom'] === 'quick'
) {
$quick_export = true;
} else {
$quick_export = false;
}
if ($_POST['output_format'] === 'astext') {
$asfile = false;
} else {
$asfile = true;
$selectedCompression = $_POST['compression'] ?? '';
if (isset($_POST['as_separate_files'])
&& ! empty($_POST['as_separate_files'])
) {
if (! empty($selectedCompression)
&& $selectedCompression === 'zip'
) {
$separate_files = $_POST['as_separate_files'];
}
}
if (in_array($selectedCompression, $compression_methods)) {
$compression = $selectedCompression;
$buffer_needed = true;
}
if (($quick_export && ! empty($_POST['quick_export_onserver']))
|| (! $quick_export && ! empty($_POST['onserver']))
) {
if ($quick_export) {
$onserver = $_POST['quick_export_onserver'];
} else {
$onserver = $_POST['onserver'];
}
// Will we save dump on server?
$save_on_server = ! empty($cfg['SaveDir']) && $onserver;
}
}
/**
* If we are sending the export file (as opposed to just displaying it
* as text), we have to bypass the usual PhpMyAdmin\Response mechanism
*/
if (isset($_POST['output_format']) && $_POST['output_format'] === 'sendit' && ! $save_on_server) {
$this->response->disable();
//Disable all active buffers (see: ob_get_status(true) at this point)
do {
if (ob_get_length() > 0 || ob_get_level() > 0) {
$hasBuffer = ob_end_clean();
} else {
$hasBuffer = false;
}
} while ($hasBuffer);
}
$tables = [];
// Generate error url and check for needed variables
if ($export_type === 'server') {
$err_url = Url::getFromRoute('/server/export');
} elseif ($export_type === 'database' && strlen($db) > 0) {
$err_url = Url::getFromRoute('/database/export', ['db' => $db]);
// Check if we have something to export
if (isset($table_select)) {
$tables = $table_select;
} else {
$tables = [];
}
} elseif ($export_type === 'table' && strlen($db) > 0 && strlen($table) > 0) {
$err_url = Url::getFromRoute('/table/export', [
'db' => $db,
'table' => $table,
]);
} elseif ($export_type === 'raw') {
$err_url = Url::getFromRoute('/server/export', ['sql_query' => $sql_query]);
} else {
Core::fatalError(__('Bad parameters!'));
}
// Merge SQL Query aliases with Export aliases from
// export page, Export page aliases are given more
// preference over SQL Query aliases.
$parser = new Parser($sql_query);
$aliases = [];
if (! empty($parser->statements[0])
&& ($parser->statements[0] instanceof SelectStatement)
) {
$aliases = Misc::getAliases($parser->statements[0], $db);
}
if (! empty($_POST['aliases'])) {
$aliases = $this->export->mergeAliases($aliases, $_POST['aliases']);
$_SESSION['tmpval']['aliases'] = $_POST['aliases'];
}
/**
* Increase time limit for script execution and initializes some variables
*/
Util::setTimeLimit();
if (! empty($cfg['MemoryLimit'])) {
ini_set('memory_limit', $cfg['MemoryLimit']);
}
register_shutdown_function([$this->export, 'shutdown']);
// Start with empty buffer
$dump_buffer = '';
$dump_buffer_len = 0;
// Array of dump_buffers - used in separate file exports
$dump_buffer_objects = [];
// We send fake headers to avoid browser timeout when buffering
$time_start = time();
// Defines the default <CR><LF> format.
// For SQL always use \n as MySQL wants this on all platforms.
if ($what === 'sql') {
$crlf = "\n";
} else {
$crlf = PHP_EOL;
}
$output_kanji_conversion = Encoding::canConvertKanji();
// Do we need to convert charset?
$output_charset_conversion = $asfile
&& Encoding::isSupported()
&& isset($charset) && $charset !== 'utf-8';
// Use on the fly compression?
$GLOBALS['onfly_compression'] = $GLOBALS['cfg']['CompressOnFly']
&& $compression === 'gzip';
if ($GLOBALS['onfly_compression']) {
$GLOBALS['memory_limit'] = $this->export->getMemoryLimit();
}
// Generate filename and mime type if needed
if ($asfile) {
if (empty($remember_template)) {
$remember_template = '';
}
[$filename, $mime_type] = $this->export->getFilenameAndMimetype(
$export_type,
$remember_template,
$export_plugin,
$compression,
$filename_template
);
} else {
$mime_type = '';
}
// For raw query export, filename will be export.extension
if ($export_type === 'raw') {
[$filename] = $this->export->getFinalFilenameAndMimetypeForFilename(
$export_plugin,
$compression,
'export'
);
}
// Open file on server if needed
if ($save_on_server) {
[$save_filename, $message, $file_handle] = $this->export->openFile(
$filename,
$quick_export
);
// problem opening export file on server?
if (! empty($message)) {
$this->export->showPage($export_type);
return;
}
} else {
/**
* Send headers depending on whether the user chose to download a dump file
* or not
*/
if ($asfile) {
// Download
// (avoid rewriting data containing HTML with anchors and forms;
// this was reported to happen under Plesk)
ini_set('url_rewriter.tags', '');
$filename = Sanitize::sanitizeFilename($filename);
Core::downloadHeader($filename, $mime_type);
} else {
// HTML
if ($export_type === 'database') {
$num_tables = count($tables);
if ($num_tables === 0) {
$message = Message::error(
__('No tables found in database.')
);
$active_page = Url::getFromRoute('/database/export');
/** @var DatabaseExportController $controller */
$controller = $containerBuilder->get(DatabaseExportController::class);
$controller->index();
exit;
}
}
[$html, $back_button, $refreshButton] = $this->export->getHtmlForDisplayedExportHeader(
$export_type,
$db,
$table
);
echo $html;
unset($html);
}
}
try {
// Re - initialize
$dump_buffer = '';
$dump_buffer_len = 0;
// Add possibly some comments to export
if (! $export_plugin->exportHeader()) {
throw new ExportException('Failure during header export.');
}
// Will we need relation & co. setup?
$do_relation = isset($GLOBALS[$what . '_relation']);
$do_comments = isset($GLOBALS[$what . '_include_comments'])
|| isset($GLOBALS[$what . '_comments']);
$do_mime = isset($GLOBALS[$what . '_mime']);
if ($do_relation || $do_comments || $do_mime) {
$this->relation->getRelationsParam();
}
// Include dates in export?
$do_dates = isset($GLOBALS[$what . '_dates']);
$whatStrucOrData = $GLOBALS[$what . '_structure_or_data'];
if ($export_type === 'raw') {
$whatStrucOrData = 'raw';
}
/**
* Builds the dump
*/
if ($export_type === 'server') {
if (! isset($db_select)) {
$db_select = '';
}
$this->export->exportServer(
$db_select,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
} elseif ($export_type === 'database') {
if (! isset($table_structure) || ! is_array($table_structure)) {
$table_structure = [];
}
if (! isset($table_data) || ! is_array($table_data)) {
$table_data = [];
}
if (! empty($_POST['structure_or_data_forced'])) {
$table_structure = $tables;
$table_data = $tables;
}
if (isset($lock_tables)) {
$this->export->lockTables($db, $tables, 'READ');
try {
$this->export->exportDatabase(
$db,
$tables,
$whatStrucOrData,
$table_structure,
$table_data,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
} finally {
$this->export->unlockTables();
}
} else {
$this->export->exportDatabase(
$db,
$tables,
$whatStrucOrData,
$table_structure,
$table_data,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$aliases,
$separate_files
);
}
} elseif ($export_type === 'raw') {
Export::exportRaw(
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$sql_query,
$export_type
);
} else {
// We export just one table
// $allrows comes from the form when "Dump all rows" has been selected
if (! isset($allrows)) {
$allrows = '';
}
if (! isset($limit_to)) {
$limit_to = '0';
}
if (! isset($limit_from)) {
$limit_from = '0';
}
if (isset($lock_tables)) {
try {
$this->export->lockTables($db, [$table], 'READ');
$this->export->exportTable(
$db,
$table,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$allrows,
$limit_to,
$limit_from,
$sql_query,
$aliases
);
} finally {
$this->export->unlockTables();
}
} else {
$this->export->exportTable(
$db,
$table,
$whatStrucOrData,
$export_plugin,
$crlf,
$err_url,
$export_type,
$do_relation,
$do_comments,
$do_mime,
$do_dates,
$allrows,
$limit_to,
$limit_from,
$sql_query,
$aliases
);
}
}
if (! $export_plugin->exportFooter()) {
throw new ExportException('Failure during footer export.');
}
} catch (ExportException $e) {
null; // Avoid phpcs error...
}
if ($save_on_server && ! empty($message)) {
$this->export->showPage($export_type);
return;
}
/**
* Send the dump as a file...
*/
if (empty($asfile)) {
echo $this->export->getHtmlForDisplayedExportFooter($back_button, $refreshButton);
return;
}
// Convert the charset if required.
if ($output_charset_conversion) {
$dump_buffer = Encoding::convertString(
'utf-8',
$GLOBALS['charset'],
$dump_buffer
);
}
// Compression needed?
if ($compression) {
if (! empty($separate_files)) {
$dump_buffer = $this->export->compress(
$dump_buffer_objects,
$compression,
$filename
);
} else {
$dump_buffer = $this->export->compress($dump_buffer, $compression, $filename);
}
}
/* If we saved on server, we have to close file now */
if ($save_on_server) {
$message = $this->export->closeFile(
$file_handle,
$dump_buffer,
$save_filename
);
$this->export->showPage($export_type);
return;
}
echo $dump_buffer;
}
public function checkTimeOut(): void
{
$this->response->setAjax(true);
if (isset($_SESSION['pma_export_error'])) {
unset($_SESSION['pma_export_error']);
$this->response->addJSON('message', 'timeout');
return;
}
$this->response->addJSON('message', 'success');
}
}

View file

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Export\Template as ExportTemplate;
use PhpMyAdmin\Export\TemplateModel;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use function is_array;
use function is_string;
final class ExportTemplateController extends AbstractController
{
/** @var TemplateModel */
private $model;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
TemplateModel $model,
Relation $relation
) {
parent::__construct($response, $template);
$this->model = $model;
$this->relation = $relation;
}
public function create(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = ExportTemplate::fromArray([
'username' => $cfg['Server']['user'],
'exportType' => $_POST['exportType'] ?? '',
'name' => $_POST['templateName'] ?? '',
'data' => $_POST['templateData'] ?? '',
]);
$result = $this->model->create($cfgRelation['db'], $cfgRelation['export_templates'], $template);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$templates = $this->model->getAll(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$template->getUsername(),
$template->getExportType()
);
$this->response->setRequestStatus(true);
$this->response->addJSON(
'data',
$this->template->render('export/template_options', [
'templates' => is_array($templates) ? $templates : [],
'selected_template' => $_POST['template_id'] ?? null,
])
);
}
public function delete(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$result = $this->model->delete(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$cfg['Server']['user'],
(int) $_POST['templateId']
);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$this->response->setRequestStatus(true);
}
public function load(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = $this->model->load(
$cfgRelation['db'],
$cfgRelation['export_templates'],
$cfg['Server']['user'],
(int) $_POST['templateId']
);
if (! $template instanceof ExportTemplate) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $template);
return;
}
$this->response->setRequestStatus(true);
$this->response->addJSON('data', $template->getData());
}
public function update(): void
{
global $cfg;
$cfgRelation = $this->relation->getRelationsParam();
if (! $cfgRelation['exporttemplateswork']) {
return;
}
$template = ExportTemplate::fromArray([
'id' => (int) $_POST['templateId'],
'username' => $cfg['Server']['user'],
'data' => $_POST['templateData'],
]);
$result = $this->model->update($cfgRelation['db'], $cfgRelation['export_templates'], $template);
if (is_string($result)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $result);
return;
}
$this->response->setRequestStatus(true);
}
}

View file

@ -0,0 +1,151 @@
<?php
/**
* Editor for Geometry data types.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Gis\GisFactory;
use PhpMyAdmin\Gis\GisVisualization;
use function array_merge;
use function in_array;
use function intval;
use function mb_strpos;
use function mb_strtoupper;
use function mb_substr;
use function substr;
use function trim;
/**
* Editor for Geometry data types.
*/
class GisDataEditorController extends AbstractController
{
public function index(): void
{
global $gis_data, $gis_types, $start, $geom_type, $gis_obj, $srid, $wkt, $wkt_with_zero, $PMA_Theme;
global $result, $visualizationSettings, $data, $visualization, $open_layers, $geom_count, $dbi;
if (! isset($_POST['field'])) {
return;
}
// Get data if any posted
$gis_data = [];
if (Core::isValid($_POST['gis_data'], 'array')) {
$gis_data = $_POST['gis_data'];
}
$gis_types = [
'POINT',
'MULTIPOINT',
'LINESTRING',
'MULTILINESTRING',
'POLYGON',
'MULTIPOLYGON',
'GEOMETRYCOLLECTION',
];
// Extract type from the initial call and make sure that it's a valid one.
// Extract from field's values if available, if not use the column type passed.
if (! isset($gis_data['gis_type'])) {
if (isset($_POST['type']) && $_POST['type'] != '') {
$gis_data['gis_type'] = mb_strtoupper($_POST['type']);
}
if (isset($_POST['value']) && trim($_POST['value']) != '') {
$start = substr($_POST['value'], 0, 1) == "'" ? 1 : 0;
$gis_data['gis_type'] = mb_substr(
$_POST['value'],
$start,
mb_strpos($_POST['value'], '(') - $start
);
}
if (! isset($gis_data['gis_type'])
|| (! in_array($gis_data['gis_type'], $gis_types))
) {
$gis_data['gis_type'] = $gis_types[0];
}
}
$geom_type = $gis_data['gis_type'];
// Generate parameters from value passed.
$gis_obj = GisFactory::factory($geom_type);
if ($gis_obj === false) {
return;
}
if (isset($_POST['value'])) {
$gis_data = array_merge(
$gis_data,
$gis_obj->generateParams($_POST['value'])
);
}
// Generate Well Known Text
$srid = isset($gis_data['srid']) && $gis_data['srid'] != '' ? $gis_data['srid'] : 0;
$wkt = $gis_obj->generateWkt($gis_data, 0);
$wkt_with_zero = $gis_obj->generateWkt($gis_data, 0, '0');
$result = "'" . $wkt . "'," . $srid;
// Generate SVG based visualization
$visualizationSettings = [
'width' => 450,
'height' => 300,
'spatialColumn' => 'wkt',
'mysqlVersion' => $dbi->getVersion(),
'isMariaDB' => $dbi->isMariaDB(),
];
$data = [
[
'wkt' => $wkt_with_zero,
'srid' => $srid,
],
];
$visualization = GisVisualization::getByData($data, $visualizationSettings)
->toImage('svg');
$open_layers = GisVisualization::getByData($data, $visualizationSettings)
->asOl();
// If the call is to update the WKT and visualization make an AJAX response
if (isset($_POST['generate']) && $_POST['generate'] == true) {
$this->response->addJSON([
'result' => $result,
'visualization' => $visualization,
'openLayers' => $open_layers,
]);
return;
}
$geom_count = 1;
if ($geom_type === 'GEOMETRYCOLLECTION') {
$geom_count = isset($gis_data[$geom_type]['geom_count'])
? intval($gis_data[$geom_type]['geom_count']) : 1;
if (isset($gis_data[$geom_type]['add_geom'])) {
$geom_count++;
}
}
$templateOutput = $this->template->render('gis_data_editor_form', [
'width' => $visualizationSettings['width'],
'height' => $visualizationSettings['height'],
'theme_image_path' => $PMA_Theme->getImgPath(),
'field' => $_POST['field'],
'input_name' => $_POST['input_name'],
'srid' => $srid,
'visualization' => $visualization,
'open_layers' => $open_layers,
'gis_types' => $gis_types,
'geom_type' => $geom_type,
'geom_count' => $geom_count,
'gis_data' => $gis_data,
'result' => $result,
]);
$this->response->addJSON(['gis_editor' => $templateOutput]);
}
}

View file

@ -0,0 +1,500 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Charsets;
use PhpMyAdmin\Charsets\Charset;
use PhpMyAdmin\Charsets\Collation;
use PhpMyAdmin\CheckUserPrivileges;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Git;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Message;
use PhpMyAdmin\RecentFavoriteTable;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Server\Select;
use PhpMyAdmin\Template;
use PhpMyAdmin\ThemeManager;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use PhpMyAdmin\Util;
use const E_USER_NOTICE;
use const E_USER_WARNING;
use const PHP_VERSION;
use function count;
use function extension_loaded;
use function file_exists;
use function ini_get;
use function preg_match;
use function sprintf;
use function strlen;
use function strtotime;
use function trigger_error;
class HomeController extends AbstractController
{
/** @var Config */
private $config;
/** @var ThemeManager */
private $themeManager;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param Config $config
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, $config, ThemeManager $themeManager, $dbi)
{
parent::__construct($response, $template);
$this->config = $config;
$this->themeManager = $themeManager;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $server, $collation_connection, $message, $show_query, $db, $table, $err_url;
if ($this->response->isAjax() && ! empty($_REQUEST['access_time'])) {
return;
}
// This is for $cfg['ShowDatabasesNavigationAsTree'] = false;
// See: https://github.com/phpmyadmin/phpmyadmin/issues/16520
// The DB is defined here and sent to the JS front-end to refresh the DB tree
$db = $_POST['db'] ?? '';
$table = '';
$show_query = '1';
$err_url = Url::getFromRoute('/');
if ($server > 0 && $this->dbi->isSuperUser()) {
$this->dbi->selectDb('mysql');
}
$languageManager = LanguageManager::getInstance();
if (! empty($message)) {
$displayMessage = Generator::getMessage($message);
unset($message);
}
if (isset($_SESSION['partial_logout'])) {
$partialLogout = Message::success(__(
'You were logged out from one server, to logout completely '
. 'from phpMyAdmin, you need to logout from all servers.'
))->getDisplay();
unset($_SESSION['partial_logout']);
}
$syncFavoriteTables = RecentFavoriteTable::getInstance('favorite')
->getHtmlSyncFavoriteTables();
$hasServer = $server > 0 || count($cfg['Servers']) > 1;
if ($hasServer) {
$hasServerSelection = $cfg['ServerDefault'] == 0
|| (! $cfg['NavigationDisplayServers']
&& (count($cfg['Servers']) > 1
|| ($server == 0 && count($cfg['Servers']) === 1)));
if ($hasServerSelection) {
$serverSelection = Select::render(true, true);
}
if ($server > 0) {
$checkUserPrivileges = new CheckUserPrivileges($this->dbi);
$checkUserPrivileges->getPrivileges();
$charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
$collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']);
$charsetsList = [];
/** @var Charset $charset */
foreach ($charsets as $charset) {
$collationsList = [];
/** @var Collation $collation */
foreach ($collations[$charset->getName()] as $collation) {
$collationsList[] = [
'name' => $collation->getName(),
'description' => $collation->getDescription(),
'is_selected' => $collation_connection === $collation->getName(),
];
}
$charsetsList[] = [
'name' => $charset->getName(),
'description' => $charset->getDescription(),
'collations' => $collationsList,
];
}
}
}
$languageSelector = '';
if (empty($cfg['Lang']) && $languageManager->hasChoice()) {
$languageSelector = $languageManager->getSelectorDisplay($this->template);
}
$themeSelection = '';
if ($cfg['ThemeManager']) {
$themeSelection = $this->themeManager->getHtmlSelectBox();
}
$databaseServer = [];
if ($server > 0 && $cfg['ShowServerInfo']) {
$hostInfo = '';
if (! empty($cfg['Server']['verbose'])) {
$hostInfo .= $cfg['Server']['verbose'];
if ($cfg['ShowServerInfo']) {
$hostInfo .= ' (';
}
}
if ($cfg['ShowServerInfo'] || empty($cfg['Server']['verbose'])) {
$hostInfo .= $this->dbi->getHostInfo();
}
if (! empty($cfg['Server']['verbose']) && $cfg['ShowServerInfo']) {
$hostInfo .= ')';
}
$serverCharset = Charsets::getServerCharset($this->dbi, $cfg['Server']['DisableIS']);
$databaseServer = [
'host' => $hostInfo,
'type' => Util::getServerType(),
'connection' => Generator::getServerSSL(),
'version' => $this->dbi->getVersionString() . ' - ' . $this->dbi->getVersionComment(),
'protocol' => $this->dbi->getProtoInfo(),
'user' => $this->dbi->fetchValue('SELECT USER();'),
'charset' => $serverCharset->getDescription() . ' (' . $serverCharset->getName() . ')',
];
}
$webServer = [];
if ($cfg['ShowServerInfo']) {
$webServer['software'] = $_SERVER['SERVER_SOFTWARE'] ?? null;
if ($server > 0) {
$clientVersion = $this->dbi->getClientInfo();
if (preg_match('#\d+\.\d+\.\d+#', $clientVersion)) {
$clientVersion = 'libmysql - ' . $clientVersion;
}
$webServer['database'] = $clientVersion;
$webServer['php_extensions'] = Util::listPHPExtensions();
$webServer['php_version'] = PHP_VERSION;
}
}
$relation = new Relation($this->dbi);
if ($server > 0) {
$cfgRelation = $relation->getRelationsParam();
if (! $cfgRelation['allworks']
&& $cfg['PmaNoRelation_DisableWarning'] == false
) {
$messageText = __(
'The phpMyAdmin configuration storage is not completely '
. 'configured, some extended features have been deactivated. '
. '%sFind out why%s. '
);
if ($cfg['ZeroConf'] == true) {
$messageText .= '<br>' .
__(
'Or alternately go to \'Operations\' tab of any database '
. 'to set it up there.'
);
}
$messageInstance = Message::notice($messageText);
$messageInstance->addParamHtml(
'<a href="' . Url::getFromRoute('/check-relations')
. '" data-post="' . Url::getCommon() . '">'
);
$messageInstance->addParamHtml('</a>');
/* Show error if user has configured something, notice elsewhere */
if (! empty($cfg['Servers'][$server]['pmadb'])) {
$messageInstance->isError(true);
}
$configStorageMessage = $messageInstance->getDisplay();
}
}
$this->checkRequirements();
$git = new Git($this->config);
$this->render('home/index', [
'message' => $displayMessage ?? '',
'partial_logout' => $partialLogout ?? '',
'is_git_revision' => $git->isGitRevision(),
'server' => $server,
'sync_favorite_tables' => $syncFavoriteTables,
'has_server' => $hasServer,
'is_demo' => $cfg['DBG']['demo'],
'has_server_selection' => $hasServerSelection ?? false,
'server_selection' => $serverSelection ?? '',
'has_change_password_link' => $cfg['Server']['auth_type'] !== 'config' && $cfg['ShowChgPassword'],
'charsets' => $charsetsList ?? [],
'language_selector' => $languageSelector,
'theme_selection' => $themeSelection,
'database_server' => $databaseServer,
'web_server' => $webServer,
'show_php_info' => $cfg['ShowPhpInfo'],
'is_version_checked' => $cfg['VersionCheck'],
'phpmyadmin_version' => PMA_VERSION,
'config_storage_message' => $configStorageMessage ?? '',
]);
}
public function setTheme(): void
{
$this->themeManager->setActiveTheme($_POST['set_theme']);
$this->themeManager->setThemeCookie();
$userPreferences = new UserPreferences();
$preferences = $userPreferences->load();
$preferences['config_data']['ThemeDefault'] = $_POST['set_theme'];
$userPreferences->save($preferences['config_data']);
$this->response->header('Location: index.php?route=/' . Url::getCommonRaw([], '&'));
}
public function setCollationConnection(): void
{
$this->config->setUserValue(
null,
'DefaultConnectionCollation',
$_POST['collation_connection'],
'utf8mb4_unicode_ci'
);
$this->response->header('Location: index.php?route=/' . Url::getCommonRaw([], '&'));
}
public function reloadRecentTablesList(): void
{
if (! $this->response->isAjax()) {
return;
}
$this->response->addJSON([
'list' => RecentFavoriteTable::getInstance('recent')->getHtmlList(),
]);
}
public function gitRevision(): void
{
if (! $this->response->isAjax()) {
return;
}
$git = new Git($this->config);
if (! $git->isGitRevision()) {
return;
}
$commit = $git->checkGitRevision();
if (! $this->config->get('PMA_VERSION_GIT') || $commit === null) {
$this->response->setRequestStatus(false);
return;
}
$commit['author']['date'] = Util::localisedDate(strtotime($commit['author']['date']));
$commit['committer']['date'] = Util::localisedDate(strtotime($commit['committer']['date']));
$this->render('home/git_info', $commit);
}
private function checkRequirements(): void
{
global $cfg, $server, $lang;
/**
* mbstring is used for handling multibytes inside parser, so it is good
* to tell user something might be broken without it, see bug #1063149.
*/
if (! extension_loaded('mbstring')) {
trigger_error(
__(
'The mbstring PHP extension was not found and you seem to be using'
. ' a multibyte charset. Without the mbstring extension phpMyAdmin'
. ' is unable to split strings correctly and it may result in'
. ' unexpected results.'
),
E_USER_WARNING
);
}
/**
* Missing functionality
*/
if (! extension_loaded('curl') && ! ini_get('allow_url_fopen')) {
trigger_error(
__(
'The curl extension was not found and allow_url_fopen is '
. 'disabled. Due to this some features such as error reporting '
. 'or version check are disabled.'
)
);
}
if ($cfg['LoginCookieValidityDisableWarning'] == false) {
/**
* Check whether session.gc_maxlifetime limits session validity.
*/
$gc_time = (int) ini_get('session.gc_maxlifetime');
if ($gc_time < $cfg['LoginCookieValidity']) {
trigger_error(
__(
'Your PHP parameter [a@https://www.php.net/manual/en/session.' .
'configuration.php#ini.session.gc-maxlifetime@_blank]session.' .
'gc_maxlifetime[/a] is lower than cookie validity configured ' .
'in phpMyAdmin, because of this, your login might expire sooner ' .
'than configured in phpMyAdmin.'
),
E_USER_WARNING
);
}
}
/**
* Check whether LoginCookieValidity is limited by LoginCookieStore.
*/
if ($cfg['LoginCookieStore'] != 0
&& $cfg['LoginCookieStore'] < $cfg['LoginCookieValidity']
) {
trigger_error(
__(
'Login cookie store is lower than cookie validity configured in ' .
'phpMyAdmin, because of this, your login will expire sooner than ' .
'configured in phpMyAdmin.'
),
E_USER_WARNING
);
}
/**
* Warning if using the default MySQL controluser account
*/
if (isset($cfg['Server']['controluser'], $cfg['Server']['controlpass'])
&& $server != 0
&& $cfg['Server']['controluser'] === 'pma'
&& $cfg['Server']['controlpass'] === 'pmapass'
) {
trigger_error(
__(
'Your server is running with default values for the ' .
'controluser and password (controlpass) and is open to ' .
'intrusion; you really should fix this security weakness' .
' by changing the password for controluser \'pma\'.'
),
E_USER_WARNING
);
}
/**
* Check if user does not have defined blowfish secret and it is being used.
*/
if (! empty($_SESSION['encryption_key'])) {
if (empty($cfg['blowfish_secret'])) {
trigger_error(
__(
'The configuration file now needs a secret passphrase (blowfish_secret).'
),
E_USER_WARNING
);
} elseif (strlen($cfg['blowfish_secret']) < 32) {
trigger_error(
__(
'The secret passphrase in configuration (blowfish_secret) is too short.'
),
E_USER_WARNING
);
}
}
/**
* Check for existence of config directory which should not exist in
* production environment.
*/
if (@file_exists(ROOT_PATH . 'config')) {
trigger_error(
__(
'Directory [code]config[/code], which is used by the setup script, ' .
'still exists in your phpMyAdmin directory. It is strongly ' .
'recommended to remove it once phpMyAdmin has been configured. ' .
'Otherwise the security of your server may be compromised by ' .
'unauthorized people downloading your configuration.'
),
E_USER_WARNING
);
}
/**
* Warning about Suhosin only if its simulation mode is not enabled
*/
if ($cfg['SuhosinDisableWarning'] == false
&& ini_get('suhosin.request.max_value_length')
&& ini_get('suhosin.simulation') == '0'
) {
trigger_error(
sprintf(
__(
'Server running with Suhosin. Please refer ' .
'to %sdocumentation%s for possible issues.'
),
'[doc@faq1-38]',
'[/doc]'
),
E_USER_WARNING
);
}
/* Missing template cache */
if ($this->config->getTempDir('twig') === null) {
trigger_error(
sprintf(
__(
'The $cfg[\'TempDir\'] (%s) is not accessible. ' .
'phpMyAdmin is not able to cache templates and will ' .
'be slow because of this.'
),
$this->config->get('TempDir')
),
E_USER_WARNING
);
}
/**
* Warning about incomplete translations.
*
* The data file is created while creating release by ./scripts/remove-incomplete-mo
*/
if (! @file_exists(ROOT_PATH . 'libraries/language_stats.inc.php')) {
return;
}
include ROOT_PATH . 'libraries/language_stats.inc.php';
/*
* This message is intentionally not translated, because we're
* handling incomplete translations here and focus on english
* speaking users.
*/
if (! isset($GLOBALS['language_stats'][$lang])
|| $GLOBALS['language_stats'][$lang] >= $cfg['TranslationWarningThreshold']
) {
return;
}
trigger_error(
'You are using an incomplete translation, please help to make it '
. 'better by [a@https://www.phpmyadmin.net/translate/'
. '@_blank]contributing[/a].',
E_USER_NOTICE
);
}
}

View file

@ -0,0 +1,884 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Bookmark;
use PhpMyAdmin\Console;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Encoding;
use PhpMyAdmin\File;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Import;
use PhpMyAdmin\Message;
use PhpMyAdmin\ParseAnalyze;
use PhpMyAdmin\Plugins;
use PhpMyAdmin\Plugins\ImportPlugin;
use PhpMyAdmin\Response;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use Throwable;
use function define;
use function htmlspecialchars;
use function in_array;
use function ini_get;
use function ini_set;
use function intval;
use function is_array;
use function is_link;
use function is_uploaded_file;
use function mb_strlen;
use function mb_strtolower;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function strlen;
use function substr;
use function time;
use function trim;
final class ImportController extends AbstractController
{
/** @var Import */
private $import;
/** @var Sql */
private $sql;
/** @var DatabaseInterface */
private $dbi;
/**
* @param Response $response
* @param DatabaseInterface $dbi
*/
public function __construct($response, Template $template, Import $import, Sql $sql, $dbi)
{
parent::__construct($response, $template);
$this->import = $import;
$this->sql = $sql;
$this->dbi = $dbi;
}
public function index(): void
{
global $cfg, $collation_connection, $db, $import_type, $table, $goto, $display_query, $PMA_Theme;
global $format, $local_import_file, $ajax_reload, $import_text, $sql_query, $message, $err_url, $url_params;
global $memory_limit, $read_limit, $finished, $offset, $charset_conversion, $charset_of_file;
global $timestamp, $maximum_time, $timeout_passed, $import_file, $go_sql, $sql_file, $error, $max_sql_len, $msg;
global $sql_query_disabled, $executed_queries, $run_query, $reset_charset, $bookmark_created;
global $result, $import_file_name, $sql_data, $import_notice, $read_multiply, $my_die, $active_page;
global $show_as_php, $reload, $charset_connection, $is_js_confirmed, $MAX_FILE_SIZE, $message_to_show;
global $noplugin, $skip_queries;
$charset_of_file = $_POST['charset_of_file'] ?? null;
$format = $_POST['format'] ?? '';
$import_type = $_POST['import_type'] ?? null;
$is_js_confirmed = $_POST['is_js_confirmed'] ?? null;
$MAX_FILE_SIZE = $_POST['MAX_FILE_SIZE'] ?? null;
$message_to_show = $_POST['message_to_show'] ?? null;
$noplugin = $_POST['noplugin'] ?? null;
$skip_queries = $_POST['skip_queries'] ?? null;
$local_import_file = $_POST['local_import_file'] ?? null;
$show_as_php = $_POST['show_as_php'] ?? null;
/* Enable LOAD DATA LOCAL INFILE for LDI plugin */
if ($format === 'ldi') {
define('PMA_ENABLE_LDI', 1);
}
// If there is a request to 'Simulate DML'.
if (isset($_POST['simulate_dml'])) {
$this->import->handleSimulateDmlRequest();
return;
}
// If it's a refresh console bookmarks request
if (isset($_GET['console_bookmark_refresh'])) {
$this->response->addJSON(
'console_message_bookmark',
Console::getBookmarkContent()
);
return;
}
// If it's a console bookmark add request
if (isset($_POST['console_bookmark_add'])) {
if (! isset($_POST['label'], $_POST['db'], $_POST['bookmark_query'], $_POST['shared'])) {
$this->response->addJSON('message', __('Incomplete params'));
return;
}
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$bookmarkFields = [
'bkm_database' => $_POST['db'],
'bkm_user' => $cfgBookmark['user'],
'bkm_sql_query' => $_POST['bookmark_query'],
'bkm_label' => $_POST['label'],
];
$isShared = ($_POST['shared'] === 'true');
$bookmark = Bookmark::createBookmark(
$this->dbi,
$cfg['Server']['user'],
$bookmarkFields,
$isShared
);
if ($bookmark !== false && $bookmark->save()) {
$this->response->addJSON('message', __('Succeeded'));
$this->response->addJSON('data', $bookmarkFields);
$this->response->addJSON('isShared', $isShared);
} else {
$this->response->addJSON('message', __('Failed'));
}
return;
}
// reset import messages for ajax request
$_SESSION['Import_message']['message'] = null;
$_SESSION['Import_message']['go_back_url'] = null;
// default values
$reload = false;
// Use to identify current cycle is executing
// a multiquery statement or stored routine
if (! isset($_SESSION['is_multi_query'])) {
$_SESSION['is_multi_query'] = false;
}
$ajax_reload = [];
$import_text = '';
// Are we just executing plain query or sql file?
// (eg. non import, but query box/window run)
if (! empty($sql_query)) {
// apply values for parameters
if (! empty($_POST['parameterized'])
&& ! empty($_POST['parameters'])
&& is_array($_POST['parameters'])
) {
$parameters = $_POST['parameters'];
foreach ($parameters as $parameter => $replacement) {
$quoted = preg_quote($parameter, '/');
// making sure that :param does not apply values to :param1
$sql_query = preg_replace(
'/' . $quoted . '([^a-zA-Z0-9_])/',
$this->dbi->escapeString($replacement) . '${1}',
$sql_query
);
// for parameters the appear at the end of the string
$sql_query = preg_replace(
'/' . $quoted . '$/',
$this->dbi->escapeString($replacement),
$sql_query
);
}
}
// run SQL query
$import_text = $sql_query;
$import_type = 'query';
$format = 'sql';
$_SESSION['sql_from_query_box'] = true;
// If there is a request to ROLLBACK when finished.
if (isset($_POST['rollback_query'])) {
$this->import->handleRollbackRequest($import_text);
}
// refresh navigation and main panels
if (preg_match('/^(DROP)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i', $sql_query)) {
$reload = true;
$ajax_reload['reload'] = true;
}
// refresh navigation panel only
if (preg_match(
'/^(CREATE|ALTER)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$sql_query
)) {
$ajax_reload['reload'] = true;
}
// do a dynamic reload if table is RENAMED
// (by sending the instruction to the AJAX response handler)
if (preg_match(
'/^RENAME\s+TABLE\s+(.*?)\s+TO\s+(.*?)($|;|\s)/i',
$sql_query,
$rename_table_names
)) {
$ajax_reload['reload'] = true;
$ajax_reload['table_name'] = Util::unQuote(
$rename_table_names[2]
);
}
$sql_query = '';
} elseif (! empty($sql_file)) {
// run uploaded SQL file
$import_file = $sql_file;
$import_type = 'queryfile';
$format = 'sql';
unset($sql_file);
} elseif (! empty($_POST['id_bookmark'])) {
// run bookmark
$import_type = 'query';
$format = 'sql';
}
// If we didn't get any parameters, either user called this directly, or
// upload limit has been reached, let's assume the second possibility.
if ($_POST == [] && $_GET == []) {
$message = Message::error(
__(
'You probably tried to upload a file that is too large. Please refer ' .
'to %sdocumentation%s for a workaround for this limit.'
)
);
$message->addParam('[doc@faq1-16]');
$message->addParam('[/doc]');
// so we can obtain the message
$_SESSION['Import_message']['message'] = $message->getDisplay();
$_SESSION['Import_message']['go_back_url'] = $goto;
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message);
return; // the footer is displayed automatically
}
// Add console message id to response output
if (isset($_POST['console_message_id'])) {
$this->response->addJSON('console_message_id', $_POST['console_message_id']);
}
/**
* Sets globals from $_POST patterns, for import plugins
* We only need to load the selected plugin
*/
if (! in_array(
$format,
[
'csv',
'ldi',
'mediawiki',
'ods',
'shp',
'sql',
'xml',
]
)
) {
// this should not happen for a normal user
// but only during an attack
Core::fatalError('Incorrect format parameter');
}
$post_patterns = [
'/^force_file_/',
'/^' . $format . '_/',
];
Core::setPostAsGlobal($post_patterns);
// Check needed parameters
Util::checkParameters(['import_type', 'format']);
// We don't want anything special in format
$format = Core::securePath($format);
if (strlen($table) > 0 && strlen($db) > 0) {
$url_params = [
'db' => $db,
'table' => $table,
];
} elseif (strlen($db) > 0) {
$url_params = ['db' => $db];
} else {
$url_params = [];
}
// Create error and goto url
if ($import_type === 'table') {
$goto = Url::getFromRoute('/table/import');
} elseif ($import_type === 'database') {
$goto = Url::getFromRoute('/database/import');
} elseif ($import_type === 'server') {
$goto = Url::getFromRoute('/server/import');
} elseif (empty($goto) || ! preg_match('@^index\.php$@i', $goto)) {
if (strlen($table) > 0 && strlen($db) > 0) {
$goto = Url::getFromRoute('/table/structure');
} elseif (strlen($db) > 0) {
$goto = Url::getFromRoute('/database/structure');
} else {
$goto = Url::getFromRoute('/server/sql');
}
}
$err_url = $goto . Url::getCommon($url_params, '&');
$_SESSION['Import_message']['go_back_url'] = $err_url;
if (strlen($db) > 0) {
$this->dbi->selectDb($db);
}
Util::setTimeLimit();
if (! empty($cfg['MemoryLimit'])) {
ini_set('memory_limit', $cfg['MemoryLimit']);
}
$timestamp = time();
if (isset($_POST['allow_interrupt'])) {
$maximum_time = ini_get('max_execution_time');
} else {
$maximum_time = 0;
}
// set default values
$timeout_passed = false;
$error = false;
$read_multiply = 1;
$finished = false;
$offset = 0;
$max_sql_len = 0;
$sql_query = '';
$sql_query_disabled = false;
$go_sql = false;
$executed_queries = 0;
$run_query = true;
$charset_conversion = false;
$reset_charset = false;
$bookmark_created = false;
$msg = 'Sorry an unexpected error happened!';
/** @var mixed|bool $result */
$result = false;
// Bookmark Support: get a query back from bookmark if required
if (! empty($_POST['id_bookmark'])) {
$id_bookmark = (int) $_POST['id_bookmark'];
switch ($_POST['action_bookmark']) {
case 0: // bookmarked query that have to be run
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark,
'id',
isset($_POST['action_bookmark_all'])
);
if (! $bookmark instanceof Bookmark) {
break;
}
if (! empty($_POST['bookmark_variable'])) {
$import_text = $bookmark->applyVariables(
$_POST['bookmark_variable']
);
} else {
$import_text = $bookmark->getQuery();
}
// refresh navigation and main panels
if (preg_match(
'/^(DROP)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$import_text
)) {
$reload = true;
$ajax_reload['reload'] = true;
}
// refresh navigation panel only
if (preg_match(
'/^(CREATE|ALTER)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i',
$import_text
)
) {
$ajax_reload['reload'] = true;
}
break;
case 1: // bookmarked query that have to be displayed
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark
);
if (! $bookmark instanceof Bookmark) {
break;
}
$import_text = $bookmark->getQuery();
if ($this->response->isAjax()) {
$message = Message::success(__('Showing bookmark'));
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('sql_query', $import_text);
$this->response->addJSON('action_bookmark', $_POST['action_bookmark']);
return;
} else {
$run_query = false;
}
break;
case 2: // bookmarked query that have to be deleted
$bookmark = Bookmark::get(
$this->dbi,
$cfg['Server']['user'],
$db,
$id_bookmark
);
if (! $bookmark instanceof Bookmark) {
break;
}
$bookmark->delete();
if ($this->response->isAjax()) {
$message = Message::success(
__('The bookmark has been deleted.')
);
$this->response->setRequestStatus($message->isSuccess());
$this->response->addJSON('message', $message);
$this->response->addJSON('action_bookmark', $_POST['action_bookmark']);
$this->response->addJSON('id_bookmark', $id_bookmark);
return;
} else {
$run_query = false;
$error = true; // this is kind of hack to skip processing the query
}
break;
}
}
// Do no run query if we show PHP code
if (isset($show_as_php)) {
$run_query = false;
$go_sql = true;
}
// We can not read all at once, otherwise we can run out of memory
$memory_limit = trim((string) ini_get('memory_limit'));
// 2 MB as default
if (empty($memory_limit)) {
$memory_limit = 2 * 1024 * 1024;
}
// In case no memory limit we work on 10MB chunks
if ($memory_limit == -1) {
$memory_limit = 10 * 1024 * 1024;
}
// Calculate value of the limit
$memoryUnit = mb_strtolower(substr((string) $memory_limit, -1));
if ($memoryUnit === 'm') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024 * 1024;
} elseif ($memoryUnit === 'k') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024;
} elseif ($memoryUnit === 'g') {
$memory_limit = (int) substr((string) $memory_limit, 0, -1) * 1024 * 1024 * 1024;
} else {
$memory_limit = (int) $memory_limit;
}
// Just to be sure, there might be lot of memory needed for uncompression
$read_limit = $memory_limit / 8;
// handle filenames
if (isset($_FILES['import_file'])) {
$import_file = $_FILES['import_file']['tmp_name'];
$import_file_name = $_FILES['import_file']['name'];
}
if (! empty($local_import_file) && ! empty($cfg['UploadDir'])) {
// sanitize $local_import_file as it comes from a POST
$local_import_file = Core::securePath($local_import_file);
$import_file = Util::userDir($cfg['UploadDir'])
. $local_import_file;
/*
* Do not allow symlinks to avoid security issues
* (user can create symlink to file they can not access,
* but phpMyAdmin can).
*/
if (@is_link($import_file)) {
$import_file = 'none';
}
} elseif (empty($import_file) || ! is_uploaded_file($import_file)) {
$import_file = 'none';
}
// Do we have file to import?
if ($import_file !== 'none' && ! $error) {
/**
* Handle file compression
*/
$importHandle = new File($import_file);
$importHandle->checkUploadedFile();
if ($importHandle->isError()) {
/** @var Message $errorMessage */
$errorMessage = $importHandle->getError();
$importHandle->close();
$_SESSION['Import_message']['message'] = $errorMessage->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $errorMessage->getDisplay());
$this->response->addHTML($errorMessage->getDisplay());
return;
}
$importHandle->setDecompressContent(true);
$importHandle->open();
if ($importHandle->isError()) {
/** @var Message $errorMessage */
$errorMessage = $importHandle->getError();
$importHandle->close();
$_SESSION['Import_message']['message'] = $errorMessage->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $errorMessage->getDisplay());
$this->response->addHTML($errorMessage->getDisplay());
return;
}
} elseif (! $error && (! isset($import_text) || empty($import_text))) {
$message = Message::error(
__(
'No data was received to import. Either no file name was ' .
'submitted, or the file size exceeded the maximum size permitted ' .
'by your PHP configuration. See [doc@faq1-16]FAQ 1.16[/doc].'
)
);
$_SESSION['Import_message']['message'] = $message->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
$this->response->addHTML($message->getDisplay());
return;
}
// Convert the file's charset if necessary
if (Encoding::isSupported() && isset($charset_of_file)) {
if ($charset_of_file !== 'utf-8') {
$charset_conversion = true;
}
} elseif (isset($charset_of_file) && $charset_of_file !== 'utf-8') {
$this->dbi->query('SET NAMES \'' . $charset_of_file . '\'');
// We can not show query in this case, it is in different charset
$sql_query_disabled = true;
$reset_charset = true;
}
// Something to skip? (because timeout has passed)
if (! $error && isset($_POST['skip'])) {
$original_skip = $skip = intval($_POST['skip']);
while ($skip > 0 && ! $finished) {
$this->import->getNextChunk($importHandle ?? null, $skip < $read_limit ? $skip : $read_limit);
// Disable read progressivity, otherwise we eat all memory!
$read_multiply = 1;
$skip -= $read_limit;
}
unset($skip);
}
// This array contain the data like number of valid sql queries in the statement
// and complete valid sql statement (which affected for rows)
$sql_data = [
'valid_sql' => [],
'valid_queries' => 0,
];
if (! $error) {
/**
* @var ImportPlugin $import_plugin
*/
$import_plugin = Plugins::getPlugin(
'import',
$format,
'libraries/classes/Plugins/Import/',
$import_type
);
if ($import_plugin == null) {
$message = Message::error(
__('Could not load import plugins, please check your installation!')
);
$_SESSION['Import_message']['message'] = $message->getDisplay();
$this->response->setRequestStatus(false);
$this->response->addJSON('message', $message->getDisplay());
$this->response->addHTML($message->getDisplay());
return;
}
// Do the real import
$default_fk_check = Util::handleDisableFKCheckInit();
try {
$import_plugin->doImport($importHandle ?? null, $sql_data);
Util::handleDisableFKCheckCleanup($default_fk_check);
} catch (Throwable $e) {
Util::handleDisableFKCheckCleanup($default_fk_check);
throw $e;
}
}
if (isset($importHandle)) {
$importHandle->close();
}
// Reset charset back, if we did some changes
if ($reset_charset) {
$this->dbi->query('SET CHARACTER SET ' . $charset_connection);
$this->dbi->setCollation($collation_connection);
}
// Show correct message
if (! empty($id_bookmark) && $_POST['action_bookmark'] == 2) {
$message = Message::success(__('The bookmark has been deleted.'));
$display_query = $import_text;
$error = false; // unset error marker, it was used just to skip processing
} elseif (! empty($id_bookmark) && $_POST['action_bookmark'] == 1) {
$message = Message::notice(__('Showing bookmark'));
} elseif ($bookmark_created) {
$special_message = '[br]' . sprintf(
__('Bookmark %s has been created.'),
htmlspecialchars($_POST['bkm_label'])
);
} elseif ($finished && ! $error) {
// Do not display the query with message, we do it separately
$display_query = ';';
if ($import_type !== 'query') {
$message = Message::success(
'<em>'
. _ngettext(
'Import has been successfully finished, %d query executed.',
'Import has been successfully finished, %d queries executed.',
$executed_queries
)
. '</em>'
);
$message->addParam($executed_queries);
if (! empty($import_notice)) {
$message->addHtml($import_notice);
}
if (! empty($local_import_file)) {
$message->addText('(' . $local_import_file . ')');
} else {
$message->addText('(' . $_FILES['import_file']['name'] . ')');
}
}
}
// Did we hit timeout? Tell it user.
if ($timeout_passed) {
$url_params['timeout_passed'] = '1';
$url_params['offset'] = $offset;
if (isset($local_import_file)) {
$url_params['local_import_file'] = $local_import_file;
}
$importUrl = $err_url = $goto . Url::getCommon($url_params, '&');
$message = Message::error(
__(
'Script timeout passed, if you want to finish import,'
. ' please %sresubmit the same file%s and import will resume.'
)
);
$message->addParamHtml('<a href="' . $importUrl . '">');
$message->addParamHtml('</a>');
if ($offset == 0 || (isset($original_skip) && $original_skip == $offset)) {
$message->addText(
__(
'However on last run no data has been parsed,'
. ' this usually means phpMyAdmin won\'t be able to'
. ' finish this import unless you increase php time limits.'
)
);
}
}
// if there is any message, copy it into $_SESSION as well,
// so we can obtain it by AJAX call
if (isset($message)) {
$_SESSION['Import_message']['message'] = $message->getDisplay();
}
// Parse and analyze the query, for correct db and table name
// in case of a query typed in the query window
// (but if the query is too large, in case of an imported file, the parser
// can choke on it so avoid parsing)
$sqlLength = mb_strlen($sql_query);
if ($sqlLength <= $cfg['MaxCharactersInDisplayedSQL']) {
[
$analyzed_sql_results,
$db,
$table_from_sql,
] = ParseAnalyze::sqlQuery($sql_query, $db);
$reload = $analyzed_sql_results['reload'];
$offset = $analyzed_sql_results['offset'];
if ($table != $table_from_sql && ! empty($table_from_sql)) {
$table = $table_from_sql;
}
}
// There was an error?
if (isset($my_die)) {
foreach ($my_die as $key => $die) {
Generator::mysqlDie(
$die['error'],
$die['sql'],
false,
$err_url,
$error
);
}
}
if ($go_sql) {
if (! empty($sql_data) && ($sql_data['valid_queries'] > 1)) {
$_SESSION['is_multi_query'] = true;
$sql_queries = $sql_data['valid_sql'];
} else {
$sql_queries = [$sql_query];
}
$html_output = '';
foreach ($sql_queries as $sql_query) {
// parse sql query
[
$analyzed_sql_results,
$db,
$table_from_sql,
] = ParseAnalyze::sqlQuery($sql_query, $db);
$offset = $analyzed_sql_results['offset'];
$reload = $analyzed_sql_results['reload'];
// Check if User is allowed to issue a 'DROP DATABASE' Statement
if ($this->sql->hasNoRightsToDropDatabase(
$analyzed_sql_results,
$cfg['AllowUserDropDatabase'],
$this->dbi->isSuperUser()
)) {
Generator::mysqlDie(
__('"DROP DATABASE" statements are disabled.'),
'',
false,
$_SESSION['Import_message']['go_back_url']
);
return;
}
if ($table != $table_from_sql && ! empty($table_from_sql)) {
$table = $table_from_sql;
}
$html_output .= $this->sql->executeQueryAndGetQueryResponse(
$analyzed_sql_results, // analyzed_sql_results
false, // is_gotofile
$db, // db
$table, // table
null, // find_real_end
null, // sql_query_for_bookmark - see below
null, // extra_data
null, // message_to_show
null, // sql_data
$goto, // goto
$PMA_Theme->getImgPath(),
null, // disp_query
null, // disp_message
$sql_query, // sql_query
null // complete_query
);
}
// sql_query_for_bookmark is not included in Sql::executeQueryAndGetQueryResponse
// since only one bookmark has to be added for all the queries submitted through
// the SQL tab
if (! empty($_POST['bkm_label']) && ! empty($import_text)) {
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$this->sql->storeTheQueryAsBookmark(
$db,
$cfgBookmark['user'],
$_POST['sql_query'],
$_POST['bkm_label'],
isset($_POST['bkm_replace'])
);
}
$this->response->addJSON('ajax_reload', $ajax_reload);
$this->response->addHTML($html_output);
return;
}
if ($result) {
// Save a Bookmark with more than one queries (if Bookmark label given).
if (! empty($_POST['bkm_label']) && ! empty($import_text)) {
$cfgBookmark = Bookmark::getParams($cfg['Server']['user']);
if (! is_array($cfgBookmark)) {
$cfgBookmark = [];
}
$this->sql->storeTheQueryAsBookmark(
$db,
$cfgBookmark['user'],
$_POST['sql_query'],
$_POST['bkm_label'],
isset($_POST['bkm_replace'])
);
}
$this->response->setRequestStatus(true);
$this->response->addJSON('message', Message::success($msg));
$this->response->addJSON(
'sql_query',
Generator::getMessage($msg, $sql_query, 'success')
);
} elseif ($result === false) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error($msg));
} else {
$active_page = $goto;
include ROOT_PATH . $goto;
}
// If there is request for ROLLBACK in the end.
if (! isset($_POST['rollback_query'])) {
return;
}
$this->dbi->query('ROLLBACK');
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Import\Ajax;
use PhpMyAdmin\Message;
use PhpMyAdmin\Template;
use function header;
use function ini_get;
use function session_start;
use function session_write_close;
use function time;
use function usleep;
/**
* Import progress bar backend
*/
class ImportStatusController
{
/** @var Template */
private $template;
/**
* @param Template $template Template object
*/
public function __construct(Template $template)
{
$this->template = $template;
}
public function index(): void
{
global $SESSION_KEY, $upload_id, $plugins, $timestamp;
[
$SESSION_KEY,
$upload_id,
$plugins,
] = Ajax::uploadProgressSetup();
// $_GET["message"] is used for asking for an import message
if (isset($_GET['message']) && $_GET['message']) {
// AJAX requests can't be cached!
Core::noCacheHeader();
header('Content-type: text/html');
// wait 0.3 sec before we check for $_SESSION variable
usleep(300000);
$maximumTime = ini_get('max_execution_time');
$timestamp = time();
// wait until message is available
while (($_SESSION['Import_message']['message'] ?? null) == null) {
// close session before sleeping
session_write_close();
// sleep
usleep(250000); // 0.25 sec
// reopen session
session_start();
if (time() - $timestamp > $maximumTime) {
$_SESSION['Import_message']['message'] = Message::error(
__('Could not load the progress of the import.')
)->getDisplay();
break;
}
}
echo $_SESSION['Import_message']['message'] ?? '';
if (isset($_SESSION['Import_message']['go_back_url'])) {
echo $this->template->render('import_status', [
'go_back_url' => $_SESSION['Import_message']['go_back_url'],
]);
}
} else {
Ajax::status($_GET['id']);
}
}
}

View file

@ -0,0 +1,738 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function json_encode;
/**
* Exporting of translated messages from PHP to JavaScript.
*/
final class JavaScriptMessagesController
{
/** @var array<string, string> */
private $messages = [];
public function __construct()
{
$this->setMessages();
}
public function index(): void
{
echo 'var Messages = ' . json_encode($this->messages) . ';';
}
private function setMessages(): void
{
global $cfg, $PMA_Theme;
$ajaxClockSmallGifPath = $PMA_Theme !== null ? $PMA_Theme->getImgPath('ajax_clock_small.gif') : '';
$this->messages = [
/* For confirmations */
'strConfirm' => __('Confirm'),
'strDoYouReally' => __('Do you really want to execute "%s"?'),
'strDropDatabaseStrongWarning' => __('You are about to DESTROY a complete database!'),
'strDatabaseRenameToSameName' => __(
'Cannot rename database to the same name. Change the name and try again'
),
'strDropTableStrongWarning' => __('You are about to DESTROY a complete table!'),
'strTruncateTableStrongWarning' => __('You are about to TRUNCATE a complete table!'),
'strDeleteTrackingData' => __('Delete tracking data for this table?'),
'strDeleteTrackingDataMultiple' => __('Delete tracking data for these tables?'),
'strDeleteTrackingVersion' => __('Delete tracking data for this version?'),
'strDeleteTrackingVersionMultiple' => __('Delete tracking data for these versions?'),
'strDeletingTrackingEntry' => __('Delete entry from tracking report?'),
'strDeletingTrackingData' => __('Deleting tracking data'),
'strDroppingPrimaryKeyIndex' => __('Dropping Primary Key/Index'),
'strDroppingForeignKey' => __('Dropping Foreign key.'),
'strOperationTakesLongTime' => __('This operation could take a long time. Proceed anyway?'),
'strDropUserGroupWarning' => __('Do you really want to delete user group "%s"?'),
'strConfirmDeleteQBESearch' => __('Do you really want to delete the search "%s"?'),
'strConfirmNavigation' => __('You have unsaved changes; are you sure you want to leave this page?'),
'strConfirmRowChange' => __(
'You are trying to reduce the number of rows, but have already entered'
. ' data in those rows which will be lost. Do you wish to continue?'
),
'strDropUserWarning' => __('Do you really want to revoke the selected user(s) ?'),
'strDeleteCentralColumnWarning' => __('Do you really want to delete this central column?'),
'strDropRTEitems' => __('Do you really want to delete the selected items?'),
'strDropPartitionWarning' => __(
'Do you really want to DROP the selected partition(s)? This will also DELETE ' .
'the data related to the selected partition(s)!'
),
'strTruncatePartitionWarning' => __('Do you really want to TRUNCATE the selected partition(s)?'),
'strRemovePartitioningWarning' => __('Do you really want to remove partitioning?'),
'strResetSlaveWarning' => __('Do you really want to RESET SLAVE?'),
'strChangeColumnCollation' => __(
'This operation will attempt to convert your data to the new collation. In '
. 'rare cases, especially where a character doesn\'t exist in the new '
. 'collation, this process could cause the data to appear incorrectly under '
. 'the new collation; in this case we suggest you revert to the original '
. 'collation and refer to the tips at '
)
. '<a href="%s" target="garbled_data_wiki">' . __('Garbled Data') . '</a>.'
. '<br><br>'
. __('Are you sure you wish to change the collation and convert the data?'),
'strChangeAllColumnCollationsWarning' => __(
'Through this operation, MySQL attempts to map the data values between '
. 'collations. If the character sets are incompatible, there may be data loss '
. 'and this lost data may <b>NOT</b> be recoverable simply by changing back the '
. 'column collation(s). <b>To convert existing data, it is suggested to use the '
. 'column(s) editing feature (the "Change" Link) on the table structure page. '
. '</b>'
)
. '<br><br>'
. __(
'Are you sure you wish to change all the column collations and convert the data?'
),
/* For modal dialog buttons */
'strSaveAndClose' => __('Save & close'),
'strReset' => __('Reset'),
'strResetAll' => __('Reset all'),
/* For indexes */
'strFormEmpty' => __('Missing value in the form!'),
'strRadioUnchecked' => __('Select at least one of the options!'),
'strEnterValidNumber' => __('Please enter a valid number!'),
'strEnterValidLength' => __('Please enter a valid length!'),
'strAddIndex' => __('Add index'),
'strEditIndex' => __('Edit index'),
/* l10n: Rename a table Index */
'strRenameIndex' => __('Rename index'),
'strAddToIndex' => __('Add %s column(s) to index'),
'strCreateSingleColumnIndex' => __('Create single-column index'),
'strCreateCompositeIndex' => __('Create composite index'),
'strCompositeWith' => __('Composite with:'),
'strMissingColumn' => __('Please select column(s) for the index.'),
/* For Preview SQL*/
'strPreviewSQL' => __('Preview SQL'),
/* For Simulate DML*/
'strSimulateDML' => __('Simulate query'),
'strMatchedRows' => __('Matched rows:'),
'strSQLQuery' => __('SQL query:'),
/* Charts */
/* l10n: Default label for the y-Axis of Charts */
'strYValues' => __('Y values'),
/* Database multi-table query */
'strEmptyQuery' => __('Please enter the SQL query first.'),
/* For server/privileges.js */
'strHostEmpty' => __('The host name is empty!'),
'strUserEmpty' => __('The user name is empty!'),
'strPasswordEmpty' => __('The password is empty!'),
'strPasswordNotSame' => __('The passwords aren\'t the same!'),
'strRemovingSelectedUsers' => __('Removing Selected Users'),
'strClose' => __('Close'),
/* For export.js */
'strTemplateCreated' => __('Template was created.'),
'strTemplateLoaded' => __('Template was loaded.'),
'strTemplateUpdated' => __('Template was updated.'),
'strTemplateDeleted' => __('Template was deleted.'),
/* l10n: Other, small valued, queries */
'strOther' => __('Other'),
/* l10n: Thousands separator */
'strThousandsSeparator' => __(','),
/* l10n: Decimal separator */
'strDecimalSeparator' => __('.'),
'strChartConnectionsTitle' => __('Connections / Processes'),
/* server status monitor */
'strIncompatibleMonitorConfig' => __('Local monitor configuration incompatible!'),
'strIncompatibleMonitorConfigDescription' => __(
'The chart arrangement configuration in your browsers local storage is not '
. 'compatible anymore to the newer version of the monitor dialog. It is very '
. 'likely that your current configuration will not work anymore. Please reset '
. 'your configuration to default in the <i>Settings</i> menu.'
),
'strQueryCacheEfficiency' => __('Query cache efficiency'),
'strQueryCacheUsage' => __('Query cache usage'),
'strQueryCacheUsed' => __('Query cache used'),
'strSystemCPUUsage' => __('System CPU usage'),
'strSystemMemory' => __('System memory'),
'strSystemSwap' => __('System swap'),
'strAverageLoad' => __('Average load'),
'strTotalMemory' => __('Total memory'),
'strCachedMemory' => __('Cached memory'),
'strBufferedMemory' => __('Buffered memory'),
'strFreeMemory' => __('Free memory'),
'strUsedMemory' => __('Used memory'),
'strTotalSwap' => __('Total swap'),
'strCachedSwap' => __('Cached swap'),
'strUsedSwap' => __('Used swap'),
'strFreeSwap' => __('Free swap'),
'strBytesSent' => __('Bytes sent'),
'strBytesReceived' => __('Bytes received'),
'strConnections' => __('Connections'),
'strProcesses' => __('Processes'),
/* summary row */
'strB' => __('B'),
'strKiB' => __('KiB'),
'strMiB' => __('MiB'),
'strGiB' => __('GiB'),
'strTiB' => __('TiB'),
'strPiB' => __('PiB'),
'strEiB' => __('EiB'),
'strNTables' => __('%d table(s)'),
/* l10n: Questions is the name of a MySQL Status variable */
'strQuestions' => __('Questions'),
'strTraffic' => __('Traffic'),
'strSettings' => __('Settings'),
'strAddChart' => __('Add chart to grid'),
'strAddOneSeriesWarning' => __('Please add at least one variable to the series!'),
'strNone' => __('None'),
/* l10n: SQL Query on modal to show exported query */
'strQuery' => __('SQL Query'),
'strResumeMonitor' => __('Resume monitor'),
'strPauseMonitor' => __('Pause monitor'),
'strStartRefresh' => __('Start auto refresh'),
'strStopRefresh' => __('Stop auto refresh'),
/* Monitor: Instructions Dialog */
'strBothLogOn' => __('general_log and slow_query_log are enabled.'),
'strGenLogOn' => __('general_log is enabled.'),
'strSlowLogOn' => __('slow_query_log is enabled.'),
'strBothLogOff' => __('slow_query_log and general_log are disabled.'),
'strLogOutNotTable' => __('log_output is not set to TABLE.'),
'strLogOutIsTable' => __('log_output is set to TABLE.'),
'strSmallerLongQueryTimeAdvice' => __(
'slow_query_log is enabled, but the server logs only queries that take longer '
. 'than %d seconds. It is advisable to set this long_query_time 0-2 seconds, '
. 'depending on your system.'
),
'strLongQueryTimeSet' => __('long_query_time is set to %d second(s).'),
'strSettingsAppliedGlobal' => __(
'Following settings will be applied globally and reset to default on server '
. 'restart:'
),
/* l10n: %s is FILE or TABLE */
'strSetLogOutput' => __('Set log_output to %s'),
/* l10n: Enable in this context means setting a status variable to ON */
'strEnableVar' => __('Enable %s'),
/* l10n: Disable in this context means setting a status variable to OFF */
'strDisableVar' => __('Disable %s'),
/* l10n: %d seconds */
'setSetLongQueryTime' => __('Set long_query_time to %d seconds.'),
'strNoSuperUser' => __(
'You can\'t change these variables. Please log in as root or contact'
. ' your database administrator.'
),
'strChangeSettings' => __('Change settings'),
'strCurrentSettings' => __('Current settings'),
'strChartTitle' => __('Chart title'),
/* l10n: As in differential values */
'strDifferential' => __('Differential'),
'strDividedBy' => __('Divided by %s'),
'strUnit' => __('Unit'),
'strFromSlowLog' => __('From slow log'),
'strFromGeneralLog' => __('From general log'),
'strServerLogError' => __(
'The database name is not known for this query in the server\'s logs.'
),
'strAnalysingLogsTitle' => __('Analysing logs'),
'strAnalysingLogs' => __('Analysing & loading logs. This may take a while.'),
'strCancelRequest' => __('Cancel request'),
'strCountColumnExplanation' => __(
'This column shows the amount of identical queries that are grouped together. '
. 'However only the SQL query itself has been used as a grouping criteria, so '
. 'the other attributes of queries, such as start time, may differ.'
),
'strMoreCountColumnExplanation' => __(
'Since grouping of INSERTs queries has been selected, INSERT queries into the '
. 'same table are also being grouped together, disregarding of the inserted '
. 'data.'
),
'strLogDataLoaded' => __('Log data loaded. Queries executed in this time span:'),
'strJumpToTable' => __('Jump to Log table'),
'strNoDataFoundTitle' => __('No data found'),
'strNoDataFound' => __('Log analysed, but no data found in this time span.'),
'strAnalyzing' => __('Analyzing…'),
'strExplainOutput' => __('Explain output'),
'strStatus' => __('Status'),
'strTime' => __('Time'),
'strTotalTime' => __('Total time:'),
'strProfilingResults' => __('Profiling results'),
'strTable' => _pgettext('Display format', 'Table'),
'strChart' => __('Chart'),
'strAliasDatabase' => _pgettext('Alias', 'Database'),
'strAliasTable' => _pgettext('Alias', 'Table'),
'strAliasColumn' => _pgettext('Alias', 'Column'),
/* l10n: A collection of available filters */
'strFiltersForLogTable' => __('Log table filter options'),
/* l10n: Filter as in "Start Filtering" */
'strFilter' => __('Filter'),
'strFilterByWordRegexp' => __('Filter queries by word/regexp:'),
'strIgnoreWhereAndGroup' => __('Group queries, ignoring variable data in WHERE clauses'),
'strSumRows' => __('Sum of grouped rows:'),
'strTotal' => __('Total:'),
'strLoadingLogs' => __('Loading logs'),
'strRefreshFailed' => __('Monitor refresh failed'),
'strInvalidResponseExplanation' => __(
'While requesting new chart data the server returned an invalid response. This '
. 'is most likely because your session expired. Reloading the page and '
. 'reentering your credentials should help.'
),
'strReloadPage' => __('Reload page'),
'strAffectedRows' => __('Affected rows:'),
'strFailedParsingConfig' => __(
'Failed parsing config file. It doesn\'t seem to be valid JSON code.'
),
'strFailedBuildingGrid' => __(
'Failed building chart grid with imported config. Resetting to default config…'
),
'strImport' => __('Import'),
'strImportDialogTitle' => __('Import monitor configuration'),
'strImportDialogMessage' => __('Please select the file you want to import.'),
'strTableNameDialogMessage' => __('Please enter a valid table name.'),
'strDBNameDialogMessage' => __('Please enter a valid database name.'),
'strNoImportFile' => __('No files available on server for import!'),
'strAnalyzeQuery' => __('Analyse query'),
/* For query editor */
'strFormatting' => __('Formatting SQL…'),
'strNoParam' => __('No parameters found!'),
/* For inline query editing */
'strGo' => __('Go'),
'strCancel' => __('Cancel'),
/* For page-related settings */
'strPageSettings' => __('Page-related settings'),
'strApply' => __('Apply'),
/* For Ajax Notifications */
'strLoading' => __('Loading…'),
'strAbortedRequest' => __('Request aborted!!'),
'strProcessingRequest' => __('Processing request'),
'strRequestFailed' => __('Request failed!!'),
'strErrorProcessingRequest' => __('Error in processing request'),
'strErrorCode' => __('Error code: %s'),
'strErrorText' => __('Error text: %s'),
'strErrorConnection' => __(
'It seems that the connection to server has been lost. Please check your ' .
'network connectivity and server status.'
),
'strNoDatabasesSelected' => __('No databases selected.'),
'strNoAccountSelected' => __('No accounts selected.'),
'strDroppingColumn' => __('Dropping column'),
'strAddingPrimaryKey' => __('Adding primary key'),
'strOK' => __('OK'),
'strDismiss' => __('Click to dismiss this notification'),
/* For database/operations.js */
'strRenamingDatabases' => __('Renaming databases'),
'strCopyingDatabase' => __('Copying database'),
'strChangingCharset' => __('Changing charset'),
'strNo' => __('No'),
/* For Foreign key checks */
'strForeignKeyCheck' => __('Enable foreign key checks'),
/* For database/structure.js */
'strErrorRealRowCount' => __('Failed to get real row count.'),
/* For database/search.js */
'strSearching' => __('Searching'),
'strHideSearchResults' => __('Hide search results'),
'strShowSearchResults' => __('Show search results'),
'strBrowsing' => __('Browsing'),
'strDeleting' => __('Deleting'),
'strConfirmDeleteResults' => __('Delete the matches for the %s table?'),
/* For rte.js */
'MissingReturn' => __('The definition of a stored function must contain a RETURN statement!'),
'strExport' => __('Export'),
'NoExportable' => __('No routine is exportable. Required privileges may be lacking.'),
/* For ENUM/SET editor*/
'enum_editor' => __('ENUM/SET editor'),
'enum_columnVals' => __('Values for column %s'),
'enum_newColumnVals' => __('Values for a new column'),
'enum_hint' => __('Enter each value in a separate field.'),
'enum_addValue' => __('Add %d value(s)'),
/* For import.js */
'strImportCSV' => __(
'Note: If the file contains multiple tables, they will be combined into one.'
),
/* For sql.js */
'strHideQueryBox' => __('Hide query box'),
'strShowQueryBox' => __('Show query box'),
'strEdit' => __('Edit'),
'strDelete' => __('Delete'),
'strNotValidRowNumber' => __('%d is not valid row number.'),
'strBrowseForeignValues' => __('Browse foreign values'),
'strNoAutoSavedQuery' => __('No previously auto-saved query is available. Loading default query.'),
'strPreviousSaveQuery' => __(
'You have a previously saved query. Click Get auto-saved query to load the query.'
),
'strBookmarkVariable' => __('Variable %d:'),
/* For Central list of columns */
'pickColumn' => __('Pick'),
'pickColumnTitle' => __('Column selector'),
'searchList' => __('Search this list'),
'strEmptyCentralList' => __(
'No columns in the central list. Make sure the Central columns list for '
. 'database %s has columns that are not present in the current table.'
),
'seeMore' => __('See more'),
'confirmTitle' => __('Are you sure?'),
'makeConsistentMessage' => __(
'This action may change some of the columns definition.<br>Are you sure you '
. 'want to continue?'
),
'strContinue' => __('Continue'),
/** For normalization */
'strAddPrimaryKey' => __('Add primary key'),
'strPrimaryKeyAdded' => __('Primary key added.'),
'strToNextStep' => __('Taking you to next step…'),
'strFinishMsg' => __("The first step of normalization is complete for table '%s'."),
'strEndStep' => __('End of step'),
'str2NFNormalization' => __('Second step of normalization (2NF)'),
'strDone' => __('Done'),
'strConfirmPd' => __('Confirm partial dependencies'),
'strSelectedPd' => __('Selected partial dependencies are as follows:'),
'strPdHintNote' => __(
'Note: a, b -> d,f implies values of columns a and b combined together can '
. 'determine values of column d and column f.'
),
'strNoPdSelected' => __('No partial dependencies selected!'),
'strBack' => __('Back'),
'strShowPossiblePd' => __('Show me the possible partial dependencies based on data in the table'),
'strHidePd' => __('Hide partial dependencies list'),
'strWaitForPd' => __(
'Sit tight! It may take few seconds depending on data size and column count of '
. 'the table.'
),
'strStep' => __('Step'),
'strMoveRepeatingGroup' => '<ol><b>' . __('The following actions will be performed:') . '</b>'
. '<li>' . __('DROP columns %s from the table %s') . '</li>'
. '<li>' . __('Create the following table') . '</li>',
'strNewTablePlaceholder' => 'Enter new table name',
'strNewColumnPlaceholder' => 'Enter column name',
'str3NFNormalization' => __('Third step of normalization (3NF)'),
'strConfirmTd' => __('Confirm transitive dependencies'),
'strSelectedTd' => __('Selected dependencies are as follows:'),
'strNoTdSelected' => __('No dependencies selected!'),
/* For server/variables.js */
'strSave' => __('Save'),
/* For table/select.js */
'strHideSearchCriteria' => __('Hide search criteria'),
'strShowSearchCriteria' => __('Show search criteria'),
'strRangeSearch' => __('Range search'),
'strColumnMax' => __('Column maximum:'),
'strColumnMin' => __('Column minimum:'),
'strMinValue' => __('Minimum value:'),
'strMaxValue' => __('Maximum value:'),
/* For table/find_replace.js */
'strHideFindNReplaceCriteria' => __('Hide find and replace criteria'),
'strShowFindNReplaceCriteria' => __('Show find and replace criteria'),
/* For table/zoom_plot_jqplot.js */
'strDisplayHelp' => '<ul><li>'
. __('Each point represents a data row.')
. '</li><li>'
. __('Hovering over a point will show its label.')
. '</li><li>'
. __('To zoom in, select a section of the plot with the mouse.')
. '</li><li>'
. __('Click reset zoom button to come back to original state.')
. '</li><li>'
. __('Click a data point to view and possibly edit the data row.')
. '</li><li>'
. __('The plot can be resized by dragging it along the bottom right corner.')
. '</li></ul>',
'strHelpTitle' => 'Zoom search instructions',
'strInputNull' => '<strong>' . __('Select two columns') . '</strong>',
'strSameInputs' => '<strong>'
. __('Select two different columns')
. '</strong>',
'strDataPointContent' => __('Data point content'),
/* For table/change.js */
'strIgnore' => __('Ignore'),
'strCopy' => __('Copy'),
'strX' => __('X'),
'strY' => __('Y'),
'strPoint' => __('Point'),
'strPointN' => __('Point %d'),
'strLineString' => __('Linestring'),
'strPolygon' => __('Polygon'),
'strGeometry' => __('Geometry'),
'strInnerRing' => __('Inner ring'),
'strOuterRing' => __('Outer ring'),
'strAddPoint' => __('Add a point'),
'strAddInnerRing' => __('Add an inner ring'),
'strYes' => __('Yes'),
'strCopyEncryptionKey' => __('Do you want to copy encryption key?'),
'strEncryptionKey' => __('Encryption key'),
/* l10n: Tip for HEX conversion of Integers */
'HexConversionInfo' => __(
'The HEX function will treat the integer as a string while calculating the hexadecimal value'
),
/* For Tip to be shown on Time field */
'strMysqlAllowedValuesTipTime' => __(
'MySQL accepts additional values not selectable by the slider;'
. ' key in those values directly if desired'
),
/* For Tip to be shown on Date field */
'strMysqlAllowedValuesTipDate' => __(
'MySQL accepts additional values not selectable by the datepicker;'
. ' key in those values directly if desired'
),
/* For Lock symbol Tooltip */
'strLockToolTip' => __(
'Indicates that you have made changes to this page;'
. ' you will be prompted for confirmation before abandoning changes'
),
/* Designer (js/designer/move.js) */
'strSelectReferencedKey' => __('Select referenced key'),
'strSelectForeignKey' => __('Select Foreign Key'),
'strPleaseSelectPrimaryOrUniqueKey' => __('Please select the primary key or a unique key!'),
'strChangeDisplay' => __('Choose column to display'),
'strLeavingDesigner' => __(
'You haven\'t saved the changes in the layout. They will be lost if you'
. ' don\'t save them. Do you want to continue?'
),
'strQueryEmpty' => __('value/subQuery is empty'),
'strAddTables' => __('Add tables from other databases'),
'strPageName' => __('Page name'),
'strSavePage' => __('Save page'),
'strSavePageAs' => __('Save page as'),
'strOpenPage' => __('Open page'),
'strDeletePage' => __('Delete page'),
/* l10n: When the user opens a page saved in the Designer */
'strSavedPageTableMissing' => __('Some tables saved in this page might have been renamed or deleted.'),
'strUntitled' => __('Untitled'),
'strSelectPage' => __('Please select a page to continue'),
'strEnterValidPageName' => __('Please enter a valid page name'),
'strLeavingPage' => __('Do you want to save the changes to the current page?'),
'strSuccessfulPageDelete' => __('Successfully deleted the page'),
'strExportRelationalSchema' => __('Export relational schema'),
'strModificationSaved' => __('Modifications have been saved'),
/* Visual query builder (js/designer/move.js) */
'strObjectsCreated' => __('%d object(s) created.'),
'strColumnName' => __('Column name'),
'strSubmit' => __('Submit'),
/* For makegrid.js (column reordering, show/hide column, grid editing) */
'strCellEditHint' => __('Press escape to cancel editing.'),
'strSaveCellWarning' => __(
'You have edited some data and they have not been saved. Are you sure you want '
. 'to leave this page before saving the data?'
),
'strColOrderHint' => __('Drag to reorder.'),
'strSortHint' => __('Click to sort results by this column.'),
'strMultiSortHint' => __(
'Shift+Click to add this column to ORDER BY clause or to toggle ASC/DESC.'
. '<br>- Ctrl+Click or Alt+Click (Mac: Shift+Option+Click) to remove column '
. 'from ORDER BY clause'
),
'strColMarkHint' => __('Click to mark/unmark.'),
'strColNameCopyHint' => __('Double-click to copy column name.'),
'strColVisibHint' => __(
'Click the drop-down arrow<br>to toggle column\'s visibility.'
),
'strShowAllCol' => __('Show all'),
'strAlertNonUnique' => __(
'This table does not contain a unique column. Features related to the grid '
. 'edit, checkbox, Edit, Copy and Delete links may not work after saving.'
),
'strEnterValidHex' => __('Please enter a valid hexadecimal string. Valid characters are 0-9, A-F.'),
'strShowAllRowsWarning' => __(
'Do you really want to see all of the rows? For a big table this could crash '
. 'the browser.'
),
'strOriginalLength' => __('Original length'),
/** Drag & Drop sql import messages */
'dropImportMessageCancel' => __('cancel'),
'dropImportMessageAborted' => __('Aborted'),
'dropImportMessageFailed' => __('Failed'),
'dropImportMessageSuccess' => __('Success'),
'dropImportImportResultHeader' => __('Import status'),
'dropImportDropFiles' => __('Drop files here'),
'dropImportSelectDB' => __('Select database first'),
/* For Print view */
'print' => __('Print'),
'back' => __('Back'),
// this approach does not work when the parameter is changed via user prefs
'strGridEditFeatureHint' => $cfg['GridEditing'] === 'double-click'
? __('You can also edit most values<br>by double-clicking directly on them.')
: ($cfg['GridEditing'] === 'click'
? __('You can also edit most values<br>by clicking directly on them.')
: ''),
'strGoToLink' => __('Go to link:'),
'strColNameCopyTitle' => __('Copy column name.'),
'strColNameCopyText' => __('Right-click the column name to copy it to your clipboard.'),
/* password generation */
'strGeneratePassword' => __('Generate password'),
'strGenerate' => __('Generate'),
'strChangePassword' => __('Change password'),
/* navigation tabs */
'strMore' => __('More'),
/* navigation panel */
'strShowPanel' => __('Show panel'),
'strHidePanel' => __('Hide panel'),
'strUnhideNavItem' => __('Show hidden navigation tree items.'),
'linkWithMain' => __('Link with main panel'),
'unlinkWithMain' => __('Unlink from main panel'),
/* microhistory */
'strInvalidPage' => __('The requested page was not found in the history, it may have expired.'),
/* update */
'strNewerVersion' => __(
'A newer version of phpMyAdmin is available and you should consider upgrading. '
. 'The newest version is %s, released on %s.'
),
/* l10n: Latest available phpMyAdmin version */
'strLatestAvailable' => __(', latest stable version:'),
'strUpToDate' => __('up to date'),
'strCreateView' => __('Create view'),
/* Error Reporting */
'strSendErrorReport' => __('Send error report'),
'strSubmitErrorReport' => __('Submit error report'),
'strErrorOccurred' => __(
'A fatal JavaScript error has occurred. Would you like to send an error report?'
),
'strChangeReportSettings' => __('Change report settings'),
'strShowReportDetails' => __('Show report details'),
'strTimeOutError' => __(
'Your export is incomplete, due to a low execution time limit at the PHP level!'
),
'strTooManyInputs' => __(
'Warning: a form on this page has more than %d fields. On submission, '
. "some of the fields might be ignored, due to PHP's "
. 'max_input_vars configuration.'
),
'phpErrorsFound' => '<div class="alert alert-danger" role="alert">'
. __('Some errors have been detected on the server!')
. '<br>'
. __('Please look at the bottom of this window.')
. '<div>'
. '<input id="pma_ignore_errors_popup" type="submit" value="'
. __('Ignore')
. '" class="btn btn-secondary floatright message_errors_found">'
. '<input id="pma_ignore_all_errors_popup" type="submit" value="'
. __('Ignore All')
. '" class="btn btn-secondary floatright message_errors_found">'
. '</div></div>',
'phpErrorsBeingSubmitted' => '<div class="alert alert-danger" role="alert">'
. __('Some errors have been detected on the server!')
. '<br>'
. __(
'As per your settings, they are being submitted currently, please be '
. 'patient.'
)
. '<br>'
. '<img src="'
. $ajaxClockSmallGifPath
. '" width="16" height="16" alt="ajax clock">'
. '</div>',
'strCopyQueryButtonSuccess' => __('Successfully copied!'),
'strCopyQueryButtonFailure' => __('Copying failed!'),
// For console
'strConsoleRequeryConfirm' => __('Execute this query again?'),
'strConsoleDeleteBookmarkConfirm' => __('Do you really want to delete this bookmark?'),
'strConsoleDebugError' => __('Some error occurred while getting SQL debug info.'),
'strConsoleDebugSummary' => __('%s queries executed %s times in %s seconds.'),
'strConsoleDebugArgsSummary' => __('%s argument(s) passed'),
'strConsoleDebugShowArgs' => __('Show arguments'),
'strConsoleDebugHideArgs' => __('Hide arguments'),
'strConsoleDebugTimeTaken' => __('Time taken:'),
'strNoLocalStorage' => __(
'There was a problem accessing your browser storage, some features may not'
. ' work properly for you. It is likely that the browser doesn\'t support storage'
. ' or the quota limit has been reached. In Firefox, corrupted storage can also'
. ' cause such a problem, clearing your "Offline Website Data" might help. In Safari,'
. ' such problem is commonly caused by "Private Mode Browsing".'
),
// For modals in /database/structure
'strCopyTablesTo' => __('Copy tables to'),
'strAddPrefix' => __('Add table prefix'),
'strReplacePrefix' => __('Replace table with prefix'),
'strCopyPrefix' => __('Copy table with prefix'),
/* For password strength simulation */
'strExtrWeak' => __('Extremely weak'),
'strVeryWeak' => __('Very weak'),
'strWeak' => __('Weak'),
'strGood' => __('Good'),
'strStrong' => __('Strong'),
/* U2F errors */
// l10n: error code 5 (from U2F API)
'strU2FTimeout' => _pgettext('U2F error', 'Timed out waiting for security key activation.'),
// l10n: error code 2 (from U2F API)
'strU2FBadRequest' => _pgettext('U2F error', 'Invalid request sent to security key.'),
// l10n: unknown error code (from U2F API)
'strU2FUnknown' => _pgettext('U2F error', 'Unknown security key error.'),
// l10n: error code 3 (from U2F API)
'strU2FInvalidClient' => _pgettext('U2F error', 'Client does not support security key.'),
// l10n: error code 4 (from U2F API) on register
'strU2FErrorRegister' => _pgettext('U2F error', 'Failed security key activation.'),
// l10n: error code 4 (from U2F API) on authanticate
'strU2FErrorAuthenticate' => _pgettext('U2F error', 'Invalid security key.'),
/* Designer */
'strTableAlreadyExists' => _pgettext(
'The table already exists in the designer and can not be added once more.',
'Table %s already exists!'
),
'strHide' => __('Hide'),
'strShow' => __('Show'),
'strStructure' => __('Structure'),
];
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* Simple script to set correct charset for the license
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use function is_readable;
use function printf;
use function readfile;
/**
* Simple script to set correct charset for the license
*/
class LicenseController extends AbstractController
{
public function index(): void
{
$this->response->disable();
$this->response->header('Content-type: text/plain; charset=utf-8');
$filename = LICENSE_FILE;
// Check if the file is available, some distributions remove these.
if (@is_readable($filename)) {
readfile($filename);
} else {
printf(
__(
'The %s file is not available on this system, please visit ' .
'%s for more information.'
),
$filename,
'https://www.phpmyadmin.net/'
);
}
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* Represents the interface between the linter and the query editor.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Linter;
use function json_encode;
/**
* Represents the interface between the linter and the query editor.
*/
class LintController extends AbstractController
{
public function index(): void
{
$params = [
'sql_query' => $_POST['sql_query'] ?? null,
'options' => $_POST['options'] ?? null,
];
/**
* The SQL query to be analyzed.
*
* This does not need to be checked again XSS or MySQL injections because it is
* never executed, just parsed.
*
* The client, which will receive the JSON response will decode the message and
* and any HTML fragments that are displayed to the user will be encoded anyway.
*
* @var string
*/
$sqlQuery = ! empty($params['sql_query']) ? $params['sql_query'] : '';
$this->response->setAjax(true);
// Disabling standard response.
$this->response->disable();
Core::headerJSON();
if (! empty($params['options'])) {
$options = $params['options'];
if (! empty($options['routine_editor'])) {
$sqlQuery = 'CREATE PROCEDURE `a`() ' . $sqlQuery;
} elseif (! empty($options['trigger_editor'])) {
$sqlQuery = 'CREATE TRIGGER `a` AFTER INSERT ON `b` FOR EACH ROW ' . $sqlQuery;
} elseif (! empty($options['event_editor'])) {
$sqlQuery = 'CREATE EVENT `a` ON SCHEDULE EVERY MINUTE DO ' . $sqlQuery;
}
}
echo json_encode(Linter::lint($sqlQuery));
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
class LogoutController
{
public function index(): void
{
global $auth_plugin, $token_mismatch;
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || $token_mismatch) {
Core::sendHeaderLocation('./index.php?route=/');
return;
}
$auth_plugin->logOut();
}
}

View file

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Config\PageSettings;
use PhpMyAdmin\Message;
use PhpMyAdmin\Navigation\Navigation;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Utils\SessionCache;
/**
* The navigation panel
*
* Displays server, database and table selection tree.
*/
class NavigationController extends AbstractController
{
/** @var Navigation */
private $navigation;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
Navigation $navigation,
Relation $relation
) {
parent::__construct($response, $template);
$this->navigation = $navigation;
$this->relation = $relation;
}
public function index(): void
{
if (! $this->response->isAjax()) {
$this->response->addHTML(
Message::error(
__('Fatal error: The navigation can only be accessed via AJAX')
)->getDisplay()
);
return;
}
if (isset($_POST['getNaviSettings']) && $_POST['getNaviSettings']) {
$pageSettings = new PageSettings('Navi', 'pma_navigation_settings');
$this->response->addHTML($pageSettings->getErrorHTML());
$this->response->addJSON('message', $pageSettings->getHTML());
return;
}
if (isset($_POST['reload'])) {
SessionCache::set('dbs_to_test', false);// Empty database list cache, see #14252
}
$cfgRelation = $this->relation->getRelationsParam();
if ($cfgRelation['navwork']) {
if (isset($_POST['hideNavItem'])) {
if (! empty($_POST['itemName'])
&& ! empty($_POST['itemType'])
&& ! empty($_POST['dbName'])
) {
$this->navigation->hideNavigationItem(
$_POST['itemName'],
$_POST['itemType'],
$_POST['dbName'],
(! empty($_POST['tableName']) ? $_POST['tableName'] : null)
);
}
return;
}
if (isset($_POST['unhideNavItem'])) {
if (! empty($_POST['itemName'])
&& ! empty($_POST['itemType'])
&& ! empty($_POST['dbName'])
) {
$this->navigation->unhideNavigationItem(
$_POST['itemName'],
$_POST['itemType'],
$_POST['dbName'],
(! empty($_POST['tableName']) ? $_POST['tableName'] : null)
);
}
return;
}
if (isset($_POST['showUnhideDialog'])) {
if (! empty($_POST['dbName'])) {
$this->response->addJSON(
'message',
$this->navigation->getItemUnhideDialog($_POST['dbName'])
);
}
return;
}
}
$this->response->addJSON('message', $this->navigation->getDisplay());
}
}

View file

@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use PhpMyAdmin\Core;
use PhpMyAdmin\Normalization;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use function intval;
use function json_decode;
use function json_encode;
use function min;
/**
* Normalization process (temporarily specific to 1NF).
*/
class NormalizationController extends AbstractController
{
/** @var Normalization */
private $normalization;
/**
* @param Response $response
*/
public function __construct($response, Template $template, Normalization $normalization)
{
parent::__construct($response, $template);
$this->normalization = $normalization;
}
public function index(): void
{
global $db, $table;
if (isset($_POST['getColumns'])) {
$html = '<option selected disabled>' . __('Select one…') . '</option>'
. '<option value="no_such_col">' . __('No such column') . '</option>';
//get column whose datatype falls under string category
$html .= $this->normalization->getHtmlForColumnsList(
$db,
$table,
_pgettext('string types', 'String')
);
echo $html;
return;
}
if (isset($_POST['splitColumn'])) {
$num_fields = min(4096, intval($_POST['numFields']));
$html = $this->normalization->getHtmlForCreateNewColumn($num_fields, $db, $table);
$html .= Url::getHiddenInputs($db, $table);
echo $html;
return;
}
if (isset($_POST['addNewPrimary'])) {
$num_fields = 1;
$columnMeta = [
'Field' => $table . '_id',
'Extra' => 'auto_increment',
];
$html = $this->normalization->getHtmlForCreateNewColumn(
$num_fields,
$db,
$table,
$columnMeta
);
$html .= Url::getHiddenInputs($db, $table);
echo $html;
return;
}
if (isset($_POST['findPdl'])) {
$html = $this->normalization->findPartialDependencies($table, $db);
echo $html;
return;
}
if (isset($_POST['getNewTables2NF'])) {
$partialDependencies = json_decode($_POST['pd'], true);
$html = $this->normalization->getHtmlForNewTables2NF($partialDependencies, $table);
echo $html;
return;
}
if (isset($_POST['getNewTables3NF'])) {
$dependencies = json_decode($_POST['pd']);
$tables = json_decode($_POST['tables'], true);
$newTables = $this->normalization->getHtmlForNewTables3NF($dependencies, $tables, $db);
$this->response->disable();
Core::headerJSON();
echo json_encode($newTables);
return;
}
$this->addScriptFiles(['normalization.js', 'vendor/jquery/jquery.uitablefilter.js']);
$normalForm = '1nf';
if (Core::isValid($_POST['normalizeTo'], ['1nf', '2nf', '3nf'])) {
$normalForm = $_POST['normalizeTo'];
}
if (isset($_POST['createNewTables2NF'])) {
$partialDependencies = json_decode($_POST['pd'], true);
$tablesName = json_decode($_POST['newTablesName']);
$res = $this->normalization->createNewTablesFor2NF($partialDependencies, $tablesName, $table, $db);
$this->response->addJSON($res);
return;
}
if (isset($_POST['createNewTables3NF'])) {
$newtables = json_decode($_POST['newTables'], true);
$res = $this->normalization->createNewTablesFor3NF($newtables, $db);
$this->response->addJSON($res);
return;
}
if (isset($_POST['repeatingColumns'])) {
$repeatingColumns = $_POST['repeatingColumns'];
$newTable = $_POST['newTable'];
$newColumn = $_POST['newColumn'];
$primary_columns = $_POST['primary_columns'];
$res = $this->normalization->moveRepeatingGroup(
$repeatingColumns,
$primary_columns,
$newTable,
$newColumn,
$table,
$db
);
$this->response->addJSON($res);
return;
}
if (isset($_POST['step1'])) {
$html = $this->normalization->getHtmlFor1NFStep1($db, $table, $normalForm);
$this->response->addHTML($html);
} elseif (isset($_POST['step2'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep2($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step3'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep3($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step4'])) {
$res = $this->normalization->getHtmlContentsFor1NFStep4($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step']) && $_POST['step'] == '2.1') {
$res = $this->normalization->getHtmlFor2NFstep1($db, $table);
$this->response->addJSON($res);
} elseif (isset($_POST['step']) && $_POST['step'] == '3.1') {
$tables = $_POST['tables'];
$res = $this->normalization->getHtmlFor3NFstep1($db, $tables);
$this->response->addJSON($res);
} else {
$this->response->addHTML($this->normalization->getHtmlForNormalizeTable());
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* phpinfo() wrapper to allow displaying only when configured to do so.
*/
declare(strict_types=1);
namespace PhpMyAdmin\Controllers;
use const INFO_CONFIGURATION;
use const INFO_GENERAL;
use const INFO_MODULES;
use function phpinfo;
/**
* phpinfo() wrapper to allow displaying only when configured to do so.
*/
class PhpInfoController extends AbstractController
{
public function index(): void
{
global $cfg;
$this->response->disable();
$this->response->getHeader()->sendHttpHeaders();
if (! $cfg['ShowPhpInfo']) {
return;
}
phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES);
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\ExportForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class ExportController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new ExportForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/export');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/export',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/export'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\FeaturesForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class FeaturesController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new FeaturesForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/features');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/features',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/features'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\ImportForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class ImportController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new ImportForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/import');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/import',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/import'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Preferences;
use PhpMyAdmin\Config\ConfigFile;
use PhpMyAdmin\Config\Forms\User\MainForm;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Relation;
use PhpMyAdmin\Response;
use PhpMyAdmin\Template;
use PhpMyAdmin\TwoFactor;
use PhpMyAdmin\Url;
use PhpMyAdmin\UserPreferences;
use function define;
use function ltrim;
class MainPanelController extends AbstractController
{
/** @var UserPreferences */
private $userPreferences;
/** @var Relation */
private $relation;
/**
* @param Response $response
*/
public function __construct(
$response,
Template $template,
UserPreferences $userPreferences,
Relation $relation
) {
parent::__construct($response, $template);
$this->userPreferences = $userPreferences;
$this->relation = $relation;
}
public function index(): void
{
global $cfg, $cf, $error, $tabHash, $hash;
global $server, $PMA_Config, $route;
$cf = new ConfigFile($PMA_Config->baseSettings);
$this->userPreferences->pageInit($cf);
$formDisplay = new MainForm($cf, 1);
if (isset($_POST['revert'])) {
// revert erroneous fields to their default values
$formDisplay->fixErrors();
Core::sendHeaderLocation('./index.php?route=/preferences/main-panel');
return;
}
$error = null;
if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) {
// Load 2FA settings
$twoFactor = new TwoFactor($cfg['Server']['user']);
// save settings
$result = $this->userPreferences->save($cf->getConfigArray());
// save back the 2FA setting only
$twoFactor->save();
if ($result === true) {
// reload config
$PMA_Config->loadUserPreferences();
$tabHash = $_POST['tab_hash'] ?? null;
$hash = ltrim($tabHash, '#');
$this->userPreferences->redirect(
'index.php?route=/preferences/main-panel',
null,
$hash
);
return;
}
$error = $result;
}
$this->addScriptFiles(['config.js']);
$cfgRelation = $this->relation->getRelationsParam();
$this->render('preferences/header', [
'route' => $route,
'is_saved' => ! empty($_GET['saved']),
'has_config_storage' => $cfgRelation['userconfigwork'],
]);
if ($formDisplay->hasErrors()) {
$formErrors = $formDisplay->displayErrors();
}
$this->render('preferences/forms/main', [
'error' => $error ? $error->getDisplay() : '',
'has_errors' => $formDisplay->hasErrors(),
'errors' => $formErrors ?? null,
'form' => $formDisplay->getDisplay(
true,
true,
true,
Url::getFromRoute('/preferences/main-panel'),
['server' => $server]
),
]);
if ($this->response->isAjax()) {
$this->response->addJSON('disableNaviSettings', true);
} else {
define('PMA_DISABLE_NAVI_SETTINGS', true);
}
}
}

Some files were not shown because too many files have changed in this diff Show more