diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..acbaa32 --- /dev/null +++ b/.distignore @@ -0,0 +1,16 @@ +.DS_Store +.git +.gitignore +.gitlab-ci.yml +.editorconfig +.travis.yml +behat.yml +circle.yml +bin/ +features/ +utils/ +*.zip +*.tar.gz +*.swp +*.txt +*.log diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e14d024 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[{.jshintrc,*.json,*.yml,*.feature}] +indent_style = space +indent_size = 2 + +[{*.txt}] +end_of_line = crlf + +[composer.json] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b6c3ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules/ +vendor/ +*.zip +*.tar.gz +*.swp +*.txt +*.log +composer.lock +.idea diff --git a/admin-tools-command.php b/admin-tools-command.php new file mode 100644 index 0000000..185d3ee --- /dev/null +++ b/admin-tools-command.php @@ -0,0 +1,20 @@ + + + WordPress Coding Standards for EE + + + + + + + + + + + . + */ci/* + */features/* + */packages/* + */tests/* + */utils/* + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Admin_Tools_Command.php b/src/Admin_Tools_Command.php new file mode 100644 index 0000000..bc5b9fe --- /dev/null +++ b/src/Admin_Tools_Command.php @@ -0,0 +1,331 @@ +fs = new Filesystem(); + define( 'ADMIN_TOOL_DIR', EE_CONF_ROOT . '/admin-tools' ); + } + + /** + * Installs admin-tools for EasyEngine. + */ + public function install() { + + if ( ! $this->is_installed() ) { + EE::log( 'Installing admin-tools. This may take some time.' ); + $this->fs->mkdir( ADMIN_TOOL_DIR ); + } + + $tools_file_info = pathinfo( ADMIN_TOOLS_FILE ); + EE::debug( 'admin-tools file: ' . ADMIN_TOOLS_FILE ); + + if ( 'json' !== $tools_file_info['extension'] ) { + EE::error( 'Invalid admin-tools file found. Aborting.' ); + } + + $tools_file = file_get_contents( ADMIN_TOOLS_FILE ); + if ( empty( $tools_file ) ) { + EE::error( 'admin-tools file is empty. Can\'t proceed further.' ); + } + $tools = json_decode( $tools_file, true ); + $json_error = json_last_error(); + if ( $json_error != JSON_ERROR_NONE ) { + EE::debug( 'Json last error: ' . $json_error ); + EE::error( 'Error decoding admin-tools file.' ); + } + if ( empty( $tools ) ) { + EE::error( 'No data found in admin-tools file. Can\'t proceed further.' ); + } + + foreach ( $tools as $tool => $data ) { + if ( ! $this->is_installed( $tool ) ) { + EE::log( "Installing $tool" ); + $tool_path = ADMIN_TOOL_DIR . '/' . $tool; + if ( method_exists( $this, "install_$tool" ) ) { + call_user_func_array( [ $this, "install_$tool" ], [ $data, $tool_path ] ); + } else { + EE::error( "No method found to install $tool. Aborting." ); + } + EE::success( "Installed $tool successfully." ); + } else { + EE::log( "$tool already installed." ); + } + } + } + + /** + * Enables admin-tools on given site. + * + * ## OPTIONS + * + * [] + * : Name of website to enable admin-tools on. + * + * [--force] + * : Force enabling of admin-tools for a site. + */ + public function up( $args, $assoc_args ) { + + EE\Utils\delem_log( 'admin-tools ' . __FUNCTION__ . ' start' ); + $args = EE\SiteUtils\auto_site_name( $args, $this->command, __FUNCTION__ ); + $force = EE\Utils\get_flag_value( $assoc_args, 'force' ); + $this->db = Site::find( EE\Utils\remove_trailing_slash( $args[0] ) ); + if ( ! $this->db || ! $this->db->site_enabled ) { + EE::error( sprintf( 'Site %s does not exist / is not enabled.', $args[0] ) ); + } + + if ( $this->db->admin_tools && ! $force ) { + EE::error( sprintf( 'admin-tools already seem to be enabled for %s', $this->db->site_url ) ); + } + + chdir( $this->db->site_fs_path ); + + $launch = EE::launch( 'docker-compose config --services' ); + $services = explode( PHP_EOL, trim( $launch->stdout ) ); + $min_req_services = [ 'nginx', 'php' ]; + + if ( count( array_intersect( $services, $min_req_services ) ) !== count( $min_req_services ) ) { + EE::error( sprintf( '%s site-type of %s-command does not support admin-tools.', $this->db->app_sub_type, $this->db->site_type ) ); + } + + if ( ! $this->is_installed() ) { + EE::log( 'It seems admin-tools have not yet been installed.' ); + $this->install(); + } + + $this->move_config_file( 'docker-compose-admin.mustache', $this->db->site_fs_path . '/docker-compose-admin.yml' ); + + if ( EE::exec( 'docker-compose -f docker-compose.yml -f docker-compose-admin.yml up -d nginx' ) ) { + EE::success( sprintf( 'admin-tools enabled for %s site.', $this->db->site_url ) ); + $this->db->admin_tools = 1; + $this->db->save(); + } else { + EE::error( sprintf( 'Error in enabling admin-tools for %s site. Check logs.', $this->db->site_url ) ); + } + + EE\Utils\delem_log( 'admin-tools ' . __FUNCTION__ . ' stop' ); + } + + /** + * Disables admin-tools on given site. + * + * ## OPTIONS + * + * [] + * : Name of website to disable admin-tools on. + * + * [--force] + * : Force disabling of admin-tools for a site. + */ + public function down( $args, $assoc_args ) { + + EE\Utils\delem_log( 'admin-tools ' . __FUNCTION__ . ' start' ); + $args = EE\SiteUtils\auto_site_name( $args, $this->command, __FUNCTION__ ); + $force = EE\Utils\get_flag_value( $assoc_args, 'force' ); + $this->db = Site::find( EE\Utils\remove_trailing_slash( $args[0] ) ); + if ( ! $this->db || ! $this->db->site_enabled ) { + EE::error( sprintf( 'Site %s does not exist / is not enabled.', $args[0] ) ); + } + + if ( ! $this->db->admin_tools && ! $force ) { + EE::error( sprintf( 'admin-tools already seem to be enabled for %s', $this->db->site_url ) ); + } + + EE::docker()::docker_compose_up( $this->db->site_fs_path, [ 'nginx', 'php' ] ); + EE::success( sprintf( 'admin-tools disabled for %s site.', $this->db->site_url ) ); + $this->db->admin_tools = 0; + $this->db->save(); + EE\Utils\delem_log( 'admin-tools ' . __FUNCTION__ . ' stop' ); + } + + /** + * Check if a tools directory is installed. + * + * @param string $tool The tool whose directory has to be checked. + * + * @return bool status. + */ + private function is_installed( $tool = '' ) { + + $tool = in_array( $tool, [ 'index', 'phpinfo' ] ) ? $tool . '.php' : $tool; + $tool = 'opcache' === $tool ? $tool . '-gui.php' : $tool; + + return $this->fs->exists( ADMIN_TOOL_DIR . '/' . $tool ); + } + + /** + * Function to download file to a path. + * + * @param string $path Path to download the file on. + * @param string $download_url Url to download the file from. + */ + private function download( $path, $download_url ) { + + $headers = array(); + $options = array( + 'timeout' => 1200, // 20 minutes ought to be enough for everybody. + 'filename' => $path, + ); + EE\Utils\http_request( 'GET', $download_url, null, $headers, $options ); + } + + /** + * Extract zip files. + * + * @param string $zip_file Path to the zip file. + * @param string $path_to_extract Path where zip needs to be extracted to. + * + * @return bool Success of extraction. + */ + private function extract_zip( $zip_file, $path_to_extract ) { + + $zip = new ZipArchive; + $res = $zip->open( $zip_file ); + if ( true === $res ) { + $zip->extractTo( $path_to_extract ); + $zip->close(); + + return true; + } + + return false; + } + + /** + * Place config files from templates to tools. + * + * @param string $config_file Destination Path where the config file needs to go. + * @param string $template_file Source Template file from which the config needs to be created. + */ + private function move_config_file( $template_file, $config_file ) { + + $this->fs->dumpFile( $config_file, file_get_contents( ADMIN_TEMPLATE_ROOT . '/' . $template_file ) ); + } + + /** + * Function to run install composer dependencies in tools that require it. + * + * @param string $tool_path Directory of the tool that contains `composer.json` file. + */ + private function composer_install( $tool_path ) { + + putenv( 'COMPOSER_HOME=' . EE_VENDOR_DIR . '/bin/composer' ); + chdir( $tool_path ); + $input = new ArrayInput( array( 'command' => 'update' ) ); + $application = new Application(); + $application->setAutoExit( false ); + $application->run( $input ); + } + + /** + * Function to install index.php file. + * + * @param array $data Data about url and version from `tools.json`. + * @param string $tool_path Path to where the tool needs to be installed. + */ + private function install_index( $data, $tool_path ) { + + $index_path_data = [ + 'db_path' => DB, + 'ee_admin_path' => '/var/www/htdocs/ee-admin', + ]; + $index_file = EE\Utils\mustache_render( ADMIN_TEMPLATE_ROOT . '/index.mustache', $index_path_data ); + $this->fs->dumpFile( $tool_path . '.php', $index_file ); + } + + /** + * Function to install phpinfo.php file. + * + * @param array $data Data about url and version from `tools.json`. + * @param string $tool_path Path to where the tool needs to be installed. + */ + private function install_phpinfo( $data, $tool_path ) { + + $this->move_config_file( 'phpinfo.mustache', $tool_path . '.php' ); + } + + /** + * Function to install phpMyAdmin. + * + * @param array $data Data about url and version from `tools.json`. + * @param string $tool_path Path to where the tool needs to be installed. + */ + private function install_pma( $data, $tool_path ) { + + $temp_dir = EE\Utils\get_temp_dir(); + $download_path = $temp_dir . 'pma.zip'; + $version = str_replace( '.', '_', $data['version'] ); + $download_url = str_replace( '{version}', $version, $data['url'] ); + $this->download( $download_path, $download_url ); + $this->extract_zip( $download_path, $temp_dir ); + $this->fs->rename( $temp_dir . 'phpmyadmin-RELEASE_' . $version, $tool_path ); + $this->move_config_file( 'pma.config.mustache', $tool_path . '/config.inc.php' ); + $this->composer_install( $tool_path ); + } + + /** + * Function to install phpRedisAdmin. + * + * @param array $data Data about url and version from `tools.json`. + * @param string $tool_path Path to where the tool needs to be installed. + */ + private function install_pra( $data, $tool_path ) { + + $temp_dir = EE\Utils\get_temp_dir(); + $download_path = $temp_dir . 'pra.zip'; + $download_url = str_replace( '{version}', $data['version'], $data['url'] ); + $this->download( $download_path, $download_url ); + $this->extract_zip( $download_path, $temp_dir ); + $this->fs->rename( $temp_dir . 'phpRedisAdmin-' . $data['version'], $tool_path ); + $this->move_config_file( 'pra.config.mustache', $tool_path . '/includes/config.inc.php' ); + $this->composer_install( $tool_path ); + } + + /** + * Function to install opcache gui. + * + * @param array $data Data about url and version from `tools.json`. + * @param string $tool_path Path to where the tool needs to be installed. + */ + private function install_opcache( $data, $tool_path ) { + + $temp_dir = EE\Utils\get_temp_dir(); + $download_path = $temp_dir . 'opcache-gui.php'; + $this->download( $download_path, $data['url'] ); + $this->fs->rename( $temp_dir . 'opcache-gui.php', $tool_path . '-gui.php' ); + } + +} diff --git a/templates/docker-compose-admin.mustache b/templates/docker-compose-admin.mustache new file mode 100644 index 0000000..7a40d72 --- /dev/null +++ b/templates/docker-compose-admin.mustache @@ -0,0 +1,13 @@ +version: '3' + +services: + + php: + volumes: + - "/opt/easyengine/admin-tools:/var/www/htdocs/ee-admin:ro" + - "/opt/easyengine/ee.sqlite:/opt/easyengine/ee.sqlite:ro" + + nginx: + volumes: + - "/opt/easyengine/admin-tools:/var/www/htdocs/ee-admin:ro" + - "/opt/easyengine/ee.sqlite:/opt/easyengine/ee.sqlite:ro" diff --git a/templates/index.mustache b/templates/index.mustache new file mode 100644 index 0000000..a406200 --- /dev/null +++ b/templates/index.mustache @@ -0,0 +1,86 @@ + $site_name ), 'sites', 1 ); + + return $db_select[0]; + +} + +/** + * Select data from the database. + * + * @param array $columns + * @param array $where + * @param string $table_name + * @param int|null $limit + * + * @return array|bool + */ +function select( $columns = array(), $where = array(), $table_name = 'services', $limit = null ) { + + $db_path = '{{db_path}}'; + $db = new SQLite3( $db_path ); + + $conditions = array(); + if ( empty( $columns ) ) { + $columns = '*'; + } else { + $columns = implode( ',', $columns ); + } + + foreach ( $where as $key => $value ) { + $conditions[] = "`$key`='" . $value . "'"; + } + + $conditions = implode( ' AND ', $conditions ); + + $select_data_query = "SELECT {$columns} FROM `$table_name`"; + + if ( ! empty( $conditions ) ) { + $select_data_query .= " WHERE $conditions"; + } + + if ( ! empty( $limit ) ) { + $select_data_query .= " LIMIT $limit"; + } + + $select_data_exec = $db->query( $select_data_query ); + $select_data = array(); + if ( $select_data_exec ) { + while ( $row = $select_data_exec->fetchArray( SQLITE3_ASSOC ) ) { + $select_data[] = $row; + } + } + if ( empty( $select_data ) ) { + return false; + } + + return $select_data; +} + +$services = populate_site_info(); +unset( $services['id'] ); +unset( $services['site_url'] ); + + +$scan = scandir( '{{ee_admin_path}}' ); +$tools = array_slice( $scan, 2 ); + +$tools = array_diff( $tools, [ 'index.php' ] ); + +if ( ! $services['cache_nginx_fullpage'] ) { + $tools = array_diff( $tools, [ 'pra' ] ); +} + +foreach ( $tools as $tool ) { + echo '' . $tool . '
'; +} diff --git a/templates/phpinfo.mustache b/templates/phpinfo.mustache new file mode 100644 index 0000000..2fa5939 --- /dev/null +++ b/templates/phpinfo.mustache @@ -0,0 +1,2 @@ +. + * + * @package PhpMyAdmin + */ + +/** + * This is needed for cookie based authentication to encrypt password in + * cookie. Needs to be 32 chars long. + */ +$cfg['blowfish_secret'] = ''; /* YOU MUST FILL IN THIS FOR COOKIE AUTH! */ + +/** + * Servers configuration + */ +$i = 0; + +$cfg['AllowArbitraryServer'] = true; + +/** + * First server + */ +$i++; +/* Authentication type */ +$cfg['Servers'][$i]['auth_type'] = 'cookie'; +/* Server parameters */ +$cfg['Servers'][$i]['host'] = 'db'; +$cfg['Servers'][$i]['compress'] = false; +$cfg['Servers'][$i]['AllowNoPassword'] = false; + +/** + * phpMyAdmin configuration storage settings. + */ + +/* User used to manipulate with storage */ +// $cfg['Servers'][$i]['controlhost'] = ''; +// $cfg['Servers'][$i]['controlport'] = ''; +// $cfg['Servers'][$i]['controluser'] = 'pma'; +// $cfg['Servers'][$i]['controlpass'] = 'pmapass'; + +/* Storage database and tables */ +// $cfg['Servers'][$i]['pmadb'] = 'phpmyadmin'; +// $cfg['Servers'][$i]['bookmarktable'] = 'pma__bookmark'; +// $cfg['Servers'][$i]['relation'] = 'pma__relation'; +// $cfg['Servers'][$i]['table_info'] = 'pma__table_info'; +// $cfg['Servers'][$i]['table_coords'] = 'pma__table_coords'; +// $cfg['Servers'][$i]['pdf_pages'] = 'pma__pdf_pages'; +// $cfg['Servers'][$i]['column_info'] = 'pma__column_info'; +// $cfg['Servers'][$i]['history'] = 'pma__history'; +// $cfg['Servers'][$i]['table_uiprefs'] = 'pma__table_uiprefs'; +// $cfg['Servers'][$i]['tracking'] = 'pma__tracking'; +// $cfg['Servers'][$i]['userconfig'] = 'pma__userconfig'; +// $cfg['Servers'][$i]['recent'] = 'pma__recent'; +// $cfg['Servers'][$i]['favorite'] = 'pma__favorite'; +// $cfg['Servers'][$i]['users'] = 'pma__users'; +// $cfg['Servers'][$i]['usergroups'] = 'pma__usergroups'; +// $cfg['Servers'][$i]['navigationhiding'] = 'pma__navigationhiding'; +// $cfg['Servers'][$i]['savedsearches'] = 'pma__savedsearches'; +// $cfg['Servers'][$i]['central_columns'] = 'pma__central_columns'; +// $cfg['Servers'][$i]['designer_settings'] = 'pma__designer_settings'; +// $cfg['Servers'][$i]['export_templates'] = 'pma__export_templates'; + +/** + * End of servers configuration + */ + +/** + * Directories for saving/loading files from server + */ +$cfg['UploadDir'] = ''; +$cfg['SaveDir'] = ''; + +/** + * Whether to display icons or text or both icons and text in table row + * action segment. Value can be either of 'icons', 'text' or 'both'. + * default = 'both' + */ +//$cfg['RowActionType'] = 'icons'; + +/** + * Defines whether a user should be displayed a "show all (records)" + * button in browse mode or not. + * default = false + */ +//$cfg['ShowAll'] = true; + +/** + * Number of rows displayed when browsing a result set. If the result + * set contains more rows, "Previous" and "Next". + * Possible values: 25, 50, 100, 250, 500 + * default = 25 + */ +//$cfg['MaxRows'] = 50; + +/** + * Disallow editing of binary fields + * valid values are: + * false allow editing + * 'blob' allow editing except for BLOB fields + * 'noblob' disallow editing except for BLOB fields + * 'all' disallow editing + * default = 'blob' + */ +//$cfg['ProtectBinary'] = false; + +/** + * Default language to use, if not browser-defined or user-defined + * (you find all languages in the locale folder) + * uncomment the desired line: + * default = 'en' + */ +//$cfg['DefaultLang'] = 'en'; +//$cfg['DefaultLang'] = 'de'; + +/** + * How many columns should be used for table display of a database? + * (a value larger than 1 results in some information being hidden) + * default = 1 + */ +//$cfg['PropertiesNumColumns'] = 2; + +/** + * Set to true if you want DB-based query history.If false, this utilizes + * JS-routines to display query history (lost by window close) + * + * This requires configuration storage enabled, see above. + * default = false + */ +//$cfg['QueryHistoryDB'] = true; + +/** + * When using DB-based query history, how many entries should be kept? + * default = 25 + */ +//$cfg['QueryHistoryMax'] = 100; + +/** + * Whether or not to query the user before sending the error report to + * the phpMyAdmin team when a JavaScript error occurs + * + * Available options + * ('ask' | 'always' | 'never') + * default = 'ask' + */ +//$cfg['SendErrorReports'] = 'always'; + +/** + * You can find more configuration options in the documentation + * in the doc/ folder or at . + */ diff --git a/templates/pra.config.mustache b/templates/pra.config.mustache new file mode 100644 index 0000000..1f47fd6 --- /dev/null +++ b/templates/pra.config.mustache @@ -0,0 +1,84 @@ + array( + array( + 'name' => getenv('VIRTUAL_HOST'), // Optional name. + 'host' => 'redis', + 'port' => 6379, + 'filter' => '*', + 'scheme' => 'tcp', // Optional. Connection scheme. 'tcp' - for TCP connection, 'unix' - for connection by unix domain socket + 'path' => '' // Optional. Path to unix domain socket. Uses only if 'scheme' => 'unix'. Example: '/var/run/redis/redis.sock' + + // Optional Redis authentication. + //'auth' => 'redispasswordhere' // Warning: The password is sent in plain-text to the Redis server. + ), + + /*array( + 'host' => 'localhost', + 'port' => 6380 + ),*/ + + /*array( + 'name' => 'local db 2', + 'host' => 'localhost', + 'port' => 6379, + 'db' => 1, // Optional database number, see http://redis.io/commands/select + 'databases' => 1, // Optional number of databases (prevents use of CONFIG command). + 'filter' => 'something:*', // Show only parts of database for speed or security reasons. + 'seperator' => '/', // Use a different seperator on this database (default uses config default). + 'flush' => false, // Set to true to enable the flushdb button for this instance. + 'charset' => 'cp1251', // Keys and values are stored in redis using this encoding (default utf-8). + 'keys' => false, // Use the old KEYS command instead of SCAN to fetch all keys for this server (default uses config default). + 'scansize' => 1000 // How many entries to fetch using each SCAN command for this server (default uses config default). + ),*/ + ), + + + 'seperator' => ':', + + + // Uncomment to show less information and make phpRedisAdmin fire less commands to the Redis server. Recommended for a really busy Redis server. + //'faster' => true, + + + // Uncomment to enable HTTP authentication + /*'login' => array( + // Username => Password + // Multiple combinations can be used + 'admin' => array( + 'password' => 'adminpassword', + ), + 'guest' => array( + 'password' => '', + 'servers' => array(1) // Optional list of servers this user can access. + ) + ),*/ + + // Use HTML form/cookie-based auth instead of HTTP Basic/Digest auth + 'cookie_auth' => false, + + + /*'serialization' => array( + 'foo*' => array( // Match like KEYS + // Function called when saving to redis. + 'save' => function($data) { return json_encode(json_decode($data)); }, + // Function called when loading from redis. + 'load' => function($data) { return json_encode(json_decode($data), JSON_PRETTY_PRINT); }, + ), + ),*/ + + + // You can ignore settings below this point. + + 'maxkeylen' => 100, + 'count_elements_page' => 100, + + // Use the old KEYS command instead of SCAN to fetch all keys. + 'keys' => false, + + // How many entries to fetch using each SCAN command. + 'scansize' => 1000 +); + +?> diff --git a/tools.json b/tools.json new file mode 100644 index 0000000..1b00bce --- /dev/null +++ b/tools.json @@ -0,0 +1,22 @@ +{ + "index": { + "url": "", + "version": "" + }, + "phpinfo": { + "url": "", + "version": "" + }, + "pma": { + "url": "https://github.com/phpmyadmin/phpmyadmin/archive/RELEASE_{version}.zip", + "version": "4.8.2" + }, + "pra": { + "url": "https://github.com/erikdubbelboer/phpRedisAdmin/archive/v{version}.zip", + "version": "1.10.2" + }, + "opcache": { + "url": "https://raw.githubusercontent.com/amnuts/opcache-gui/master/index.php", + "version": "" + } +}