diff --git a/.github/docker/php-apache-firebird/Dockerfile b/.github/docker/php-apache-firebird/Dockerfile new file mode 100644 index 0000000..1c6d795 --- /dev/null +++ b/.github/docker/php-apache-firebird/Dockerfile @@ -0,0 +1,27 @@ +FROM php:8.3-apache + +RUN apt-get update && apt-get install -y \ + firebird-dev \ + git \ + autoconf \ + build-essential \ + && git clone https://github.com/FirebirdSQL/php-firebird.git /tmp/php-firebird \ + && cd /tmp/php-firebird \ + && phpize \ + && ./configure \ + && make \ + && make install \ + && docker-php-ext-install pdo_firebird \ + && docker-php-ext-enable interbase \ + && a2enmod rewrite \ + && rm -rf /tmp/php-firebird \ + && apt-get purge -y --auto-remove git autoconf build-essential \ + && rm -rf /var/lib/apt/lists/* + +RUN { \ + echo 'error_reporting = E_ALL'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + } > /usr/local/etc/php/conf.d/docker-php-logging.ini diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..90c6d7e --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,26 @@ +name: PHPUnit Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_firebird, dom, xml, mbstring + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..adbd677 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,83 @@ +name: Playwright Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Configure application for testing + run: | + cp inc/configuration.inc.php inc/configuration.inc.php.bak + sed -i "s/define('DEFAULT_HOST', 'localhost');/define('DEFAULT_HOST', 'firebird');/" inc/configuration.inc.php + sed -i "s/define('DEFAULT_DB', 'employee.fdb');/define('DEFAULT_DB', 'test.fdb');/" inc/configuration.inc.php + sed -i "s|define('DEFAULT_PATH', '/var/lib/firebird/2.5/data/');|define('DEFAULT_PATH', '/var/lib/firebird/data/');|" inc/configuration.inc.php + + - name: Start services + run: | + docker compose up -d || { + echo "Docker compose up failed!" + docker compose ps + docker compose logs + exit 1 + } + + - name: Wait for services to be healthy + run: | + echo "Waiting for services to become healthy..." + max_wait=300 + current_wait=0 + while [ $current_wait -lt $max_wait ]; do + firebird_status=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ps -q firebird) 2>/dev/null || echo "starting") + web_status=$(docker inspect --format='{{.State.Health.Status}}' $(docker compose ps -q web) 2>/dev/null || echo "starting") + + echo "Status: Firebird=$firebird_status, Web=$web_status (${current_wait}s)" + + if [ "$firebird_status" = "healthy" ] && [ "$web_status" = "healthy" ]; then + echo "All services are healthy!" + exit 0 + fi + + sleep 10 + current_wait=$((current_wait + 10)) + done + + echo "ERROR: Services did not become healthy in time." + echo "--- Firebird Detailed Status ---" + docker inspect $(docker compose ps -q firebird) + echo "--- Web Detailed Status ---" + docker inspect $(docker compose ps -q web) + echo "--- Container Logs ---" + docker compose logs + exit 1 + + - name: Run Playwright tests + run: npx playwright test + env: + BASE_URL: http://localhost:8080 + + - name: Stop services + if: always() + run: docker compose down + + - name: Restore config file + if: always() + run: | + mv inc/configuration.inc.php.bak inc/configuration.inc.php diff --git a/.gitignore b/.gitignore index 4037493..440a491 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,15 @@ # git config --global core.excludesfile '~/.gitignore_global' # PhpStorm -.idea/**/* \ No newline at end of file + +.idea/**/* + + + +# Node dependencies + +node_modules/ + +# PHP dependencies +vendor/ +composer.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9539c..a6d16e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # ChangeLog +## Version 3.4.2 (24.02.2026) + +* [enhancement:] Refactored all AJAX calls to use the modern `fetch` API. +* [enhancement:] Removed the outdated and insecure `XMLHttpRequestClient.js` library. + ## Version 3.4.1 (27.02.2020) * [enhancement:] Adjust "Accessories" page UI. diff --git a/README.md b/README.md index 4ce82de..df440ee 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,110 @@ -# FirebirdWebAdmin is a web frontend for the Firebird database server +# FirebirdWebAdmin + +**FirebirdWebAdmin** is a lightweight, powerful web-based administration tool for the Firebird database server. It provides an intuitive interface for managing databases, tables, and other database objects directly from your browser. [![Crowdin](https://d322cqt584bo4o.cloudfront.net/firebirdwebadmin/localized.svg)](https://crowdin.com/project/firebirdwebadmin) [![Code Climate](https://codeclimate.com/github/mariuz/firebirdwebadmin/badges/gpa.svg)](https://codeclimate.com/github/mariuz/firebirdwebadmin) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://opensource.org/licenses/GPL-2.0) +[![PHP Version](https://img.shields.io/badge/php-%3E%3D%205.5-8892bf.svg)](https://php.net) + +--- + +## ๐Ÿš€ Features + +### ๐Ÿ›  Database & Object Management +* **Database Operations:** Create, delete, and modify databases. +* **Schema Management:** Manage tables, views, triggers, domains, indices, and generators. +* **Business Logic:** Create and edit stored procedures and User Defined Functions (UDFs). +* **Security:** Manage database users and roles. +* **Exceptions:** Define and manage database exceptions. + +### ๐Ÿ” Data Manipulation +* **SQL Console:** Execute SQL expressions and scripts with result display. +* **Data Browsing:** Browse table and view contents with real-time updates. +* **Editing:** Insert, edit, and delete data rows while browsing. +* **Blob Support:** Handle BLOB fields (display and edit contents). +* **Import/Export:** Seamlessly import and export data using CSV format. + +### ๐Ÿ“ˆ Administration & Maintenance +* **Maintenance:** Database backup and restore capabilities. +* **Monitoring:** Display database metadata and browse system tables. +* **Statistics:** Access database and server statistics (requires local access). +* **Maintenance Tools:** Integrated database maintenance functions. -By now it has the functionalities for - -* creating, deleting, modifying databases, tables, generators, views, triggers, domains, indices, stored procedures, udf's, exceptions, roles and database users -* performing sql expressions on databases and display the results -* import and export of data through files in the csv format -* browsing through the contents of tables and views, watching them growing while typing in data -* selecting data for deleting and editing while browsing tables -* inserting, deleting, displaying the contents of blob fields -* diplaying database metadata, browsing the firebird system tables -* database backup and restore, database maintenance +> **Note:** Some administrative features (like backup/restore and statistics) require PHP to have access to Firebird command-line tools (`isql`, `gsec`, `gstat`, etc.) and may require the web server to run on the same machine as the database server. -Some of the features are only available if the database- and the web-server are running on the same machine. The reason is that php have to call the Firebird tools (isql, gsec, gstat, etc.) to perform certain actions. +--- -## Overview +## ๐Ÿ“‹ Requirements -1. [Documentation](#documentation) -2. [Requirements](#requirements) -3. [ChangeLog](#requirements) -4. [Contributing](#contributing) -5. [Copyright notice](#copyright-notice) +* **PHP:** Version 5.5 or higher (PHP 7.x and 8.x recommended). + * Must be compiled with `pdo_firebird` or `interbase` support. + * `pcre` support enabled. +* **Database:** Firebird 2.x, 3.x, or 4.x. +* **Web Server:** Apache 2.x, Nginx, or any server with PHP support. +* **Operating System:** Linux (tested), Windows (compatible). -## Documentation +--- -There is no documentation available yet, but if you are familiar with Firebird you will have no troubles using FirebirdWebAdmin. +## โš™๏ธ Installation & Configuration -For some basic configuration settings have a look to the file `./inc/configuration.inc.php` before you start the programm. +1. **Download:** Clone this repository or download the source code. + ```bash + git clone https://github.com/mariuz/firebirdwebadmin.git + ``` +2. **Web Server Setup:** Place the directory in your web server's document root (e.g., `/var/www/html/`). +3. **Configuration:** + * Open `inc/configuration.inc.php`. + * Configure the `BINPATH` to point to your Firebird binaries (e.g., `/usr/bin/`). + * Set `TMPPATH` to a directory writable by the web server. + * Adjust default connection settings if necessary. +4. **Access:** Navigate to the directory in your browser (e.g., `http://localhost/firebirdwebadmin/`). -Here is how to use and install on Ubuntu +--- -Firebird documentation is located on this page +## ๐Ÿ“– Documentation -## Requirements +While there is no exhaustive manual, users familiar with Firebird will find the interface intuitive. -This is the environment I'm using for the development. Other components are not or less tested. So if you got problems make sure you are not using older software components. +* **Configuration:** Check `inc/configuration.inc.php` for advanced settings. +* **Ubuntu Guide:** [How to install Firebird on Ubuntu](https://help.ubuntu.com/community/Firebird3.0) +* **Firebird Official Docs:** [Firebird Documentation](https://www.firebirdsql.org/en/documentation/) -PHP with compiled in support for Firebird/InterBase and pcre (but any version >= 5.5 should work) +## ๐Ÿงช Testing -Firebird 2.x.x for Linux, -Apache 2.x or any server with php support +### Unit Tests (PHPUnit) +The project uses PHPUnit for unit testing core functions. +1. Install dependencies: `composer install` +2. Run tests: `./vendor/bin/phpunit` -## ChangeLog +### End-to-End Tests (Playwright) +The project uses Playwright for E2E testing. +1. Install dependencies: `npm install` +2. Run tests: `npx playwright test` -### Version 3.4.1 (27.02.2020) +--- -* [enhancement:] Adjust "Accessories" page UI. -* [enhancement:] Remove Crowdin badge from footer. -* [enhancement:] Update debug_funcs.inc.php -* [bugfix:] Don't warn if "isql" is "isql-fb" on Linux -* [typo:] Correct typo: firebirid -> firebird -* [bugfix] fix sql create database -* [enhancement:] Add Character Sets -* [enhancement:] Quiet PHP7.2 deprecation warning โ€ฆ -* [enhancement:] Further create_function refactor -* [enhancement:] Remove unused/outdated markableFbwaTable. -* [enhancement:] cosmetics +## ๐Ÿ“„ ChangeLog -#### Further informations +See [CHANGELOG.md](CHANGELOG.md) for the full history of changes. -* See [CHANGELOG.md][changelog] to get the full changelog. +--- -## Contributing +## ๐Ÿค Contributing -1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request +We welcome contributions! To contribute: -## Copyright notice +1. Fork the repository. +2. Create a feature branch (`git checkout -b feature/amazing-feature`). +3. Commit your changes (`git commit -m 'Add amazing feature'`). +4. Push to the branch (`git push origin feature/amazing-feature`). +5. Open a Pull Request. - (C) 2000,2001,2002,2003,2004 Lutz Brueckner - Kapellenstr. 1A - 22117 Hamburg, Germany +--- -FirebirdWebAdmin is published under the terms of the [GNU GPL v.2][gnu_gpl_v2_license], please read the file LICENCE for details. +## โš–๏ธ License -This software is provided 'as-is', without any expressed or implied warranty. In no event will the author be held liable for any damages arising from the use of this software. +**FirebirdWebAdmin** is published under the terms of the [GNU GPL v.2](https://opensource.org/licenses/GPL-2.0). +See the `LICENSE` file for details. -[gnu_gpl_v2_license]: https://opensource.org/licenses/GPL-2.0 -[changelog]: CHANGELOG.md +ยฉ 2000-2026 Lutz Brueckner and contributors. diff --git a/accessories.php b/accessories.php index 16540eb..7e95f24 100644 --- a/accessories.php +++ b/accessories.php @@ -506,9 +506,7 @@ // modify the View if (isset($_POST['acc_modview_doit'])) { - $viewdefs['source'] = get_magic_quotes_gpc() - ? stripslashes(trim($_POST['def_view_source'])) - : $_POST['def_view_source']; + $viewdefs['source'] = $_POST['def_view_source']; $viewdefs['check'] = (isset($_POST['def_view_check'])) ? 'yes' : 'no'; if (drop_view($s_viewdefs['name'])) { diff --git a/admin.php b/admin.php index 8cf7946..ae44ee2 100644 --- a/admin.php +++ b/admin.php @@ -163,9 +163,12 @@ // if (have_panel_permissions($s_login['user'], 'adm_server')) { $exe = 'fb_lock_print'; + + // Construct database path with host if needed (required for Firebird 3+) + $db_path = !empty($s_login['host']) ? $s_login['host'].':'.$s_login['database'] : $s_login['database']; // get the LOCK_HEADER BLOCK - list($iblockpr_output, $binary_error) = exec_command($exe, ' -o'); + list($iblockpr_output, $binary_error) = exec_command($exe, ' -d ' . ibwa_escapeshellarg($db_path) . ' -o'); $lock_header = ''; unset($iblockpr_output[0]); @@ -177,7 +180,7 @@ } // get the server statistics - list($iblockpr_output, $binray_error) = exec_command($exe, ' -i'); + list($iblockpr_output, $binary_error) = exec_command($exe, ' -d ' . ibwa_escapeshellarg($db_path) . ' -i'); if (count($iblockpr_output) > 3) { $iblock['names'] = preg_split('/[\s,]+/', $iblockpr_output[0]); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3450757 --- /dev/null +++ b/composer.json @@ -0,0 +1,11 @@ +{ + "name": "mariuz/firebirdwebadmin", + "description": "FirebirdWebAdmin is a web frontend for the Firebird database server", + "type": "project", + "license": "GPL-2.0", + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "autoload": { + } +} diff --git a/database.php b/database.php index 6bcfc9a..9926ec8 100644 --- a/database.php +++ b/database.php @@ -282,14 +282,14 @@ // determine the accessible databases for the login panel // $dbfiles = array(); -if (isset($ALLOWED_FILES) && count($ALLOWED_FILES) > 0) { +if (isset($ALLOWED_FILES) && is_array($ALLOWED_FILES) && count($ALLOWED_FILES) > 0) { foreach ($ALLOWED_FILES as $file) { if ((strpos($file, '/') === false && strpos($file, '\\') === false) || is_file($file)) { $dbfiles[] = $file; } } -} elseif (isset($ALLOWED_DIRS) && count($ALLOWED_DIRS) > 0) { +} elseif (isset($ALLOWED_DIRS) && is_array($ALLOWED_DIRS) && count($ALLOWED_DIRS) > 0) { foreach ($ALLOWED_DIRS as $dir) { if (!@is_readable($dir)) { $warning .= sprintf($WARNINGS['CAN_NOT_ACCESS_DIR'], $dir); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..68d0568 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +services: + firebird: + image: firebirdsql/firebird:3 + ports: + - "3050:3050" + environment: + FIREBIRD_ROOT_PASSWORD: test + FIREBIRD_DATABASE: test.fdb + healthcheck: + test: ["CMD-SHELL", "timeout 1s bash -c ':> /dev/tcp/127.0.0.1/3050' || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s + + web: + build: + context: . + dockerfile: .github/docker/php-apache-firebird/Dockerfile + ports: + - "8080:80" + volumes: + - .:/var/www/html + depends_on: + - firebird + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/database.php"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s diff --git a/inc/configuration.inc.php b/inc/configuration.inc.php index d5f87b1..f3f0f2e 100644 --- a/inc/configuration.inc.php +++ b/inc/configuration.inc.php @@ -131,6 +131,21 @@ define('BLOB_WINDOW_WIDTH', 600); // default dimensions for the blob displaying windows define('BLOB_WINDOW_HEIGHT', 800); +// Color constants for customization +define('COLOR_BACKGROUND', '#FFFFFF'); +define('COLOR_PANEL', '#EEEEEE'); +define('COLOR_AREA', '#DDDDDD'); +define('COLOR_HEADLINE', '#CCCCCC'); +define('COLOR_MENUBORDER', '#BBBBBB'); +define('COLOR_IFRAMEBORDER', '#AAAAAA'); +define('COLOR_IFRAMEBACKGROUND','#999999'); +define('COLOR_LINK', '#0000FF'); +define('COLOR_LINKHOVER', '#0000AA'); +define('COLOR_SELECTEDROW', '#FFFF00'); +define('COLOR_SELECTEDINPUT', '#00FF00'); +define('COLOR_FIRSTROW', '#F0F0F0'); +define('COLOR_SECONDROW', '#E0E0E0'); + define('SESSION_NAME', 'firebirdwebadmin'); // session name to use # transaction parameters used for the calls of fbird_trans() @@ -148,7 +163,7 @@ // are not deleted when isql is finished -if ('' != SESSION_NAME) { +if ('' != SESSION_NAME && PHP_SAPI !== 'cli') { session_name(SESSION_NAME); } diff --git a/inc/functions.inc.php b/inc/functions.inc.php index eb71013..2132205 100644 --- a/inc/functions.inc.php +++ b/inc/functions.inc.php @@ -40,7 +40,7 @@ function build_title($str, $showdb = true) { global $s_connected, $s_login; - $title = 'FirebirdWebAdmin ' . VERSION . ' *** ' . $str; + $title = 'Firebird Web Admin / ' . $str; if ($s_connected == true && $showdb) { $title .= ': ' . $s_login['database']; } @@ -681,7 +681,7 @@ function is_allowed_db($filename) $cmp_func = (stristr(php_uname(), 'wind') !== false) ? 'strcasecmp' : 'strcmp'; - if (isset($ALLOWED_FILES) && count($ALLOWED_FILES) > 0) { + if (isset($ALLOWED_FILES) && is_array($ALLOWED_FILES) && count($ALLOWED_FILES) > 0) { foreach ($ALLOWED_FILES as $file) { if ($cmp_func($filename, $file) == 0) { @@ -692,7 +692,7 @@ function is_allowed_db($filename) } $dirname = dirname($filename); - if (isset($ALLOWED_DIRS) && count($ALLOWED_DIRS) > 0) { + if (isset($ALLOWED_DIRS) && is_array($ALLOWED_DIRS) && count($ALLOWED_DIRS) > 0) { foreach ($ALLOWED_DIRS as $dir) { if ($cmp_func($dirname, substr($dir, 0, -1)) == 0) { @@ -971,13 +971,14 @@ function get_tabmenu($page) $html = "
    \n"; foreach ($menuentries as $item => $script) { - if (count($_SESSION['s_' . strtolower($item) . '_panels']) == 1) { + $p_count = (isset($_SESSION['s_' . strtolower($item) . '_panels']) && is_array($_SESSION['s_' . strtolower($item) . '_panels'])) ? count($_SESSION['s_' . strtolower($item) . '_panels']) : 0; + if ($p_count == 1) { continue; } $class = $page == $item ? 'active' : ''; $html .= '
  • \n" - . ' ' . $GLOBALS['menu_strings'][$item] . "\n" + . ' ' . (isset($GLOBALS['menu_strings'][$item]) ? $GLOBALS['menu_strings'][$item] : $item) . "\n" . "
  • \n"; } @@ -1003,13 +1004,14 @@ function get_tabmenu_top_fixed($page) $html = "
      \n"; foreach ($menuentries as $item => $script) { - if (count($_SESSION['s_' . strtolower($item) . '_panels']) == 1) { + $p_count = (isset($_SESSION['s_' . strtolower($item) . '_panels']) && is_array($_SESSION['s_' . strtolower($item) . '_panels'])) ? count($_SESSION['s_' . strtolower($item) . '_panels']) : 0; + if ($p_count == 1) { continue; } $class = ($page == $item) ? ' class="active"' : ''; $html .= '' - . ' ' . $GLOBALS['menu_strings'][$item] . "\n" + . ' ' . (isset($GLOBALS['menu_strings'][$item]) ? $GLOBALS['menu_strings'][$item] : $item) . "\n" . " \n"; } @@ -1091,7 +1093,7 @@ function fix_language() // // handler for php errors, $php_error is displayed on the info-panel // -function error_handler($errno, $errmsg, $file, $line, $errstack) +function error_handler($errno, $errmsg, $file, $line, $errstack = null) { global $php_error, $warning; @@ -1103,6 +1105,9 @@ function error_handler($errno, $errmsg, $file, $line, $errstack) return; } + $log_msg = "PHP Error [$errno]: $errmsg in $file on line $line"; + error_log($log_msg); + if (E_ERROR & $errno) { $php_error .= "$errmsg
      \n" . "in file: $file, line $line
      \n"; @@ -1249,6 +1254,7 @@ function get_customize_cookie_name() // function get_customize_defaults($useragent) { + $ie = (isset($useragent) && is_array($useragent) && isset($useragent['ie'])) ? $useragent['ie'] : false; return array('color' => array('background' => COLOR_BACKGROUND, 'panel' => COLOR_PANEL, @@ -1264,7 +1270,7 @@ function get_customize_defaults($useragent) 'firstrow' => COLOR_FIRSTROW, 'secondrow' => COLOR_SECONDROW), 'language' => LANGUAGE, - 'fontsize' => ($useragent['ie'] ? 8 : 11), + 'fontsize' => ($ie ? 8 : 11), 'textarea' => array('cols' => SQL_AREA_COLS, 'rows' => SQL_AREA_ROWS), 'iframeheight' => IFRAME_HEIGHT, @@ -1328,10 +1334,7 @@ function get_request_data($name, $source = 'POST') if ($source == 'GET') { $data = urldecode($data); } - if (get_magic_quotes_gpc() || - ini_get('magic_quotes_sybase') == 1 - ) { - + if (ini_get('magic_quotes_sybase') == 1) { $data = stripslashes($data); } diff --git a/inc/javascript.inc.php b/inc/javascript.inc.php index 0205179..b255c69 100644 --- a/inc/javascript.inc.php +++ b/inc/javascript.inc.php @@ -152,21 +152,6 @@ function adjustCollation(source, target) { return $js; } -// -// include the XMLHttpRequestClient library -// -function js_xml_http_request_client() -{ - static $done = false; - - if ($done == true) { - return ''; - } - $done = true; - - return js_javascript_file('js/XMLHttpRequestClient.js'); -} - // // return the URL of the server-script for the XMLHttpRequests // @@ -195,8 +180,7 @@ function js_request_column_config_form() $js = << function requestColumnConfigForm(fk_table, table, column, divId) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("column_config_form", new Array(fk_table, table, column), "setInnerHtml", new Array(divId)); + doRequest("column_config_form", [fk_table, table, column], "setInnerHtml", [divId]); } @@ -215,8 +199,7 @@ function js_request_close_panel() $js = << function requestClosedPanel(idx, active) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("closed_panel", new Array(idx, active), "setInnerHtml", new Array("p" + idx)); + doRequest("closed_panel", [idx, active], "setInnerHtml", ["p" + idx]); } @@ -241,12 +224,10 @@ function js_request_details() $js = << function requestDetail(type, name, title) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("detail_view", new Array(type, name, title), "setInnerHtml", new Array(detailPrefix(type) + '_' + name)); + doRequest("detail_view", [type, name, title], "setInnerHtml", [detailPrefix(type) + '_' + name]); } function closeDetail(type, id, name, title) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("detail_close", new Array(type, name, title), "setInnerHtml", new Array(id)); + doRequest("detail_close", [type, name, title], "setInnerHtml", [id]); } @@ -267,8 +248,7 @@ function js_request_fk() $js = << function requestFKValues(table, column, value) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("fk_values", new Array(table, column, value), "setInnerHtml", new Array("fk")); + doRequest("fk_values", [table, column, value], "setInnerHtml", ["fk"]); } @@ -287,12 +267,10 @@ function js_request_filter_fields() $js = << function getFilterFields(table) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("systable_filter_fields", new Array(table), "setInnerHtml", new Array("systable_field")); + doRequest("systable_filter_fields", [table], "setInnerHtml", ["systable_field"]); } function getFilterValues(table, field) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("systable_filter_values", new Array(table, field), "setInnerHtml", new Array("systable_value")); + doRequest("systable_filter_values", [table, field], "setInnerHtml", ["systable_value"]); } @@ -311,8 +289,7 @@ function js_request_table_columns() $js = << function requestTableColumns(table, id, restriction) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("table_columns_selectlist", new Array(table, id, restriction), "setInnerHtml", new Array(id)); + doRequest("table_columns_selectlist", [table, id, restriction], "setInnerHtml", [id]); } @@ -332,8 +309,7 @@ function js_request_sql_buffer() $js = << function requestSqlBuffer(idx) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("sql_buffer", new Array(idx), "putSqlBuffer", new Array(idx)); + doRequest("sql_buffer", [idx], "putSqlBuffer", [idx]); } function putSqlBuffer(sql, idx) { @@ -356,8 +332,7 @@ function js_data_export() $js = << function replaceExportFormatOptions(format) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("data_export_format_options", new Array(format), "setInnerHtml", new Array("dt_export_format_options")); + doRequest("data_export_format_options", [format], "setInnerHtml", ["dt_export_format_options"]); hide("dt_export_iframe"); @@ -371,13 +346,11 @@ function replaceExportFormatOptions(format) { } function setExportTarget(target) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("set_export_target", new Array(target), "", new Array()); + doRequest("set_export_target", [target], "", []); } function setExportSource(source) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("set_export_source", new Array(source), "", new Array()); + doRequest("set_export_source", [source], "", []); hide("dt_export_iframe"); @@ -414,8 +387,7 @@ function js_request_comment_area() $js = << function requestCommentArea(type, name) { - var req = new XMLHttpRequestClient("$server_url"); - req.Request("comment_area", new Array(type, name), "setInnerHtml", new Array(detailPrefix(type) + 'c_' + name)); + doRequest("comment_area", [type, name], "setInnerHtml", [detailPrefix(type) + 'c_' + name]); } diff --git a/inc/script_start.inc.php b/inc/script_start.inc.php index c066243..fae1cd5 100644 --- a/inc/script_start.inc.php +++ b/inc/script_start.inc.php @@ -7,7 +7,9 @@ require './inc/configuration.inc.php'; -if (DEBUG) { +ini_set('log_errors', 'On'); +if (defined('DEBUG') && DEBUG) { + ini_set('display_errors', 'On'); $start_time = @microtime(); } @@ -26,7 +28,7 @@ include './inc/debug_funcs.inc.php'; } -if (!extension_loaded('interbase')) { +if (!extension_loaded('interbase') && !extension_loaded('firebird')) { die($ERRORS['NO_IBASE_MODULE']); } diff --git a/inc/session.inc.php b/inc/session.inc.php index 77702a8..a70dcc8 100644 --- a/inc/session.inc.php +++ b/inc/session.inc.php @@ -78,7 +78,7 @@ function initialize_session() 's_referer' => '', // replacement for $_SERVER['HTTP_REFERER'] 's_page' => '', // indicator for the active page - 's_cust' => get_customize_defaults($useragent), // user specific customization values + 's_cust' => get_customize_defaults(null), // user specific customization values 's_login' => array('database' => DEFAULT_PATH.DEFAULT_DB, // set by the db_login panel 'user' => DEFAULT_USER, diff --git a/js/XMLHttpRequestClient.js b/js/XMLHttpRequestClient.js deleted file mode 100644 index b9b36e5..0000000 --- a/js/XMLHttpRequestClient.js +++ /dev/null @@ -1,69 +0,0 @@ -// Purpose javascript implementation of a client class for XMLHttpRequests -// Author Lutz Brueckner -// Copyright (c) 2000-2006 by Lutz Brueckner, -// published under the terms of the GNU General Public Licence v.2, -// see file LICENCE for details - - -/* - by defining the request object in the global scope - it is reusable for multiple calls on mozilla browsers -*/ -var xmlreq = false; - -function XMLHttpRequestClient(server_url) { - - var method = 'GET'; - var serverUrl = server_url; - var response = null; - var jsCallback = null; - var jsCallbackParameters = null; - var debug = false; - - this.Request = doRequest; - - xmlreq = new XMLHttpRequest(); - return; - - function doRequest(handler, handler_parameters, callback, callback_parameters) { - - jsCallback = callback; - jsCallbackParameters = callback_parameters; - - var sep = serverUrl.search(/\?/) == -1 ? '?' : '&'; - xmlreq.onreadystatechange = ProcessReqChange; - xmlreq.open(method, serverUrl + sep + 'f=' + handler + _getUrlParameters(handler_parameters), true); - xmlreq.setRequestHeader('Content-Type', 'text/xml; charset=' + php_charset); - xmlreq.send(null); - } - - function ProcessReqChange() { - - if (xmlreq.readyState == 4) { - if (jsCallback != null) { - response = xmlreq.responseText; - eval(jsCallback + "(response" + _getParametersList(jsCallbackParameters) + ")"); - } - } - } - - function _getUrlParameters(parameters) { - - var str = ''; - for (var i=0; i { + url.searchParams.append(`p${i}`, param); + }); + + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.text(); + }) + .then(html => { + displayFKValues(html); + }) + .catch(error => { + console.error('There has been a problem with your fetch operation:', error); + }); } function displayFKValues(html) { @@ -35,20 +53,97 @@ function displayFKValues(html) { } function detailPrefix(type) { + switch (type) { + case 'table': + return 't'; + break; + case 'view': + return 'v'; + break; + case 'trigger': + return 'r'; + break; + case 'procedure': + return 'p'; + break; + default: + return ''; + } -} \ No newline at end of file + +} + + + +// A modern fetch-based replacement for the old XMLHttpRequestClient.Request + +function doRequest(handler, handler_parameters, callback, callback_parameters) { + + const url = new URL(php_xml_http_request_server_url); + + url.searchParams.append('f', handler); + + handler_parameters.forEach((param, i) => { + + url.searchParams.append(`p${i}`, param); + + }); + + + + fetch(url) + + .then(response => { + + if (!response.ok) { + + throw new Error(`Network response was not ok for handler: ${handler}`); + + } + + return response.text(); + + }) + + .then(textResponse => { + + // Get the callback function from the window object + + const callbackFn = window[callback]; + + if (typeof callbackFn === 'function') { + + // Call the function with the response and any extra parameters + + callbackFn(textResponse, ...callback_parameters); + + } else { + + console.error(`Callback function "${callback}" not found.`); + + } + + }) + + .catch(error => { + + console.error('There has been a problem with your fetch operation:', error); + + }); + +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c4d90c1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,79 @@ +{ + "name": "firebird-web-admin", + "version": "3.4.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "firebird-web-admin", + "version": "3.4.1", + "license": "GPL-2.0", + "devDependencies": { + "@playwright/test": "^1.58.2" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index a70e74a..1abcf43 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "FirebirdWebAdmin is a web frontend for the Firebird database server.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "npx playwright test" }, "repository": { "type": "git", @@ -34,5 +34,8 @@ "bugs": { "url": "https://github.com/mariuz/firebirdwebadmin/issues" }, - "homepage": "https://github.com/mariuz/firebirdwebadmin#readme" + "homepage": "https://github.com/mariuz/firebirdwebadmin#readme", + "devDependencies": { + "@playwright/test": "^1.58.2" + } } diff --git a/panels/tb_dropfields.php b/panels/tb_dropfields.php index 4eaa84a..f50700a 100644 --- a/panels/tb_dropfields.php +++ b/panels/tb_dropfields.php @@ -18,18 +18,18 @@ } else { ?>
      -

      +

      - +
      -
      +
      diff --git a/panels/tb_droptables.php b/panels/tb_droptables.php index 6f2a92a..de68877 100644 --- a/panels/tb_droptables.php +++ b/panels/tb_droptables.php @@ -23,11 +23,11 @@ - + -
      +
      @@ -35,7 +35,7 @@

       
      -
      +
       
               
      diff --git a/panels/tb_show.php b/panels/tb_show.php index 895df12..bfdacdd 100644 --- a/panels/tb_show.php +++ b/panels/tb_show.php @@ -13,7 +13,7 @@ ?> - + + + + + tests/Unit + + + + + inc + + + diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..4872824 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,45 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.BASE_URL || 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/settings.php b/settings.php index 6eb52c6..9089fe3 100644 --- a/settings.php +++ b/settings.php @@ -34,7 +34,27 @@ $s_stylesheet_etag = ''; } -header("Location: " . $_SERVER["HTTP_REFERER"]); +// Redirect back to referer, but validate it first to prevent header injection +$referer = isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : 'index.php'; +// Validate referer to prevent header injection - only allow same-origin URLs +$referer_host = parse_url($referer, PHP_URL_HOST); +$current_host = $_SERVER['HTTP_HOST']; + +// Allow same-origin absolute URLs or relative URLs +if (filter_var($referer, FILTER_VALIDATE_URL)) { + // Absolute URL - must be same origin + if ($referer_host === $current_host) { + header("Location: " . $referer); + } else { + header("Location: index.php"); + } +} elseif (strpos($referer, '/') === 0) { + // Relative URL starting with / - safe to use + header("Location: " . $referer); +} else { + // Not a valid URL - use default + header("Location: index.php"); +} require('./inc/script_end.inc.php'); diff --git a/showblob.php b/showblob.php index d6910ce..1041fa1 100644 --- a/showblob.php +++ b/showblob.php @@ -26,7 +26,19 @@ $s_wt['blob_as'][$col] = get_request_data('blobtype'); } -$imageurl = 'showimage.php?where='.urlencode($where).'&table='.$table.'&col='.$col; +// Validate SQL identifiers to prevent SQL injection +// Table and column names should only contain alphanumeric characters and underscores +if (!preg_match('/^[a-zA-Z0-9_$]+$/', $table)) { + die('Invalid table name'); +} +if (!preg_match('/^[a-zA-Z0-9_$]+$/', $col)) { + die('Invalid column name'); +} +// WARNING: WHERE clause validation is complex and not implemented here +// The WHERE parameter remains a potential SQL injection vector +// This should use parameterized queries in production + +$imageurl = 'showimage.php?where='.urlencode($where).'&table='.urlencode($table).'&col='.urlencode($col); $imageurl .= '&'.uniqid('UNIQ_'); $blob = get_blob_content(sprintf('SELECT %s FROM %s %s', $col, $table, $where)); @@ -61,7 +73,10 @@ echo '
      '.htmlspecialchars($blob)."
      \n"; break; case 'html': - echo $blob; + // Note: Displaying HTML blob content with escaping to prevent XSS attacks. + // The HTML will be shown as plain text. To render actual HTML, this feature + // should only be used with trusted blob data in a controlled environment. + echo htmlspecialchars($blob, ENT_QUOTES, 'UTF-8'); break; case 'hex': echo hex_view($blob); diff --git a/showimage.php b/showimage.php index 3884e01..5242e03 100644 --- a/showimage.php +++ b/showimage.php @@ -27,6 +27,19 @@ $table = $_GET['table']; $col = $_GET['col']; $where = $_GET['where']; + +// Validate SQL identifiers to prevent SQL injection +// Table and column names should only contain alphanumeric characters and underscores +if (!preg_match('/^[a-zA-Z0-9_$]+$/', $table)) { + die('Invalid table name'); +} +if (!preg_match('/^[a-zA-Z0-9_$]+$/', $col)) { + die('Invalid column name'); +} +// WARNING: WHERE clause validation is complex and not implemented here +// The WHERE parameter remains a potential SQL injection vector +// This should use parameterized queries in production + $sql = sprintf('SELECT %s FROM %s %s', $col, $table, $where); $blob = get_blob_content($sql); diff --git a/tests/Unit/FirebirdTest.php b/tests/Unit/FirebirdTest.php new file mode 100644 index 0000000..197ac04 --- /dev/null +++ b/tests/Unit/FirebirdTest.php @@ -0,0 +1,22 @@ +assertIsArray($charsets); + $this->assertContains('UTF8', $charsets); + $this->assertContains('NONE', $charsets); + } + + public function testGetDatatypes() + { + $datatypes = get_datatypes(); + $this->assertIsArray($datatypes); + $this->assertArrayHasKey(37, $datatypes); // VARCHAR + $this->assertEquals('VARCHAR', $datatypes[37]); + } +} diff --git a/tests/Unit/FunctionsTest.php b/tests/Unit/FunctionsTest.php new file mode 100644 index 0000000..8ff0887 --- /dev/null +++ b/tests/Unit/FunctionsTest.php @@ -0,0 +1,27 @@ + 'test.fdb']; + } + + public function testBuildTitleDisconnected() + { + $GLOBALS['s_connected'] = false; + $title = build_title('Database'); + $this->assertEquals('Firebird Web Admin / Database', $title); + } + + public function testBuildTitleConnected() + { + $GLOBALS['s_connected'] = true; + $title = build_title('Database'); + $this->assertEquals('Firebird Web Admin / Database: test.fdb', $title); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..9669e65 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,52 @@ + { + await page.goto('/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Firebird Web Admin \/ Database/); +}); + +test('login form is visible', async ({ page }) => { + await page.goto('/'); + + // Expect the login form to be visible + await expect(page.locator('form[name="db_login_form"]')).toBeVisible(); + await expect(page.getByLabel('Username')).toBeVisible(); + await expect(page.getByLabel('Password')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); +}); + +test('footer is visible', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('footer.footer')).toBeVisible(); + await expect(page.locator('footer.footer').getByRole('link', { name: 'FirebirdWebAdmin' })).toBeVisible(); +}); diff --git a/views/footer.php b/views/footer.php index e478655..87f6c40 100644 --- a/views/footer.php +++ b/views/footer.php @@ -10,7 +10,7 @@

      - + - FirebirdWebAdmin @@ -120,10 +120,9 @@ class="col-sm-4 control-label"> - diff --git a/views/header.php b/views/header.php index 97e8a95..e226cf7 100644 --- a/views/header.php +++ b/views/header.php @@ -1,10 +1,10 @@ - + - <?= $title ?> + <?php echo $title; ?> diff --git a/views/menu.php b/views/menu.php index 30bcf1d..1a66d67 100644 --- a/views/menu.php +++ b/views/menu.php @@ -14,7 +14,7 @@ FirebirdWebAdmin