diff --git a/.ddev/addon-metadata/phpmyadmin/manifest.yaml b/.ddev/addon-metadata/phpmyadmin/manifest.yaml index ffd67e101..f40f6bfae 100644 --- a/.ddev/addon-metadata/phpmyadmin/manifest.yaml +++ b/.ddev/addon-metadata/phpmyadmin/manifest.yaml @@ -1,7 +1,7 @@ name: phpmyadmin repository: ddev/ddev-phpmyadmin -version: v1.0.0 -install_date: "2025-08-24T12:09:35-07:00" +version: v1.0.3 +install_date: "2026-09-11T12:34:11+01:00" project_files: - docker-compose.phpmyadmin.yaml - docker-compose.phpmyadmin_norouter.yaml diff --git a/.ddev/commands/host/phpmyadmin b/.ddev/commands/host/phpmyadmin index c03ee85d1..5f7d0edce 100755 --- a/.ddev/commands/host/phpmyadmin +++ b/.ddev/commands/host/phpmyadmin @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ## #ddev-generated: If you want to edit and own this file, remove this line. ## Description: Launch a browser with PhpMyAdmin diff --git a/.ddev/config.yaml b/.ddev/config.yaml index f15ac88d7..efe402001 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -1,12 +1,11 @@ -name: fossbilling.dev +name: fossbilling type: php docroot: src php_version: "8.3" nodejs_version: "24" webserver_type: nginx-fpm xdebug_enabled: false -additional_hostnames: [] -additional_fqdns: [] +additional_hostnames: ["fossbilling.dev"] database: type: mariadb version: "11.8" @@ -15,131 +14,17 @@ hooks: - exec: .ddev/post-start.sh upload_dirs: - data/uploads + - ../node_modules use_dns_when_possible: true composer_version: "2" web_environment: - IS_DDEV=true - APP_ENV=dev corepack_enable: false +omit_containers: ["ddev-ssh-agent"] # Key features of DDEV's config.yaml: -# name: # Name of the project, automatically provides -# http://projectname.ddev.site and https://projectname.ddev.site -# If the name is omitted, the project will take the name of the enclosing directory, -# which is useful if you want to have a copy of the project side by side with this one. - -# type: # asterios, backdrop, cakephp, codeigniter, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, drupal12, generic, joomla, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress, wp-bedrock -# See https://docs.ddev.com/en/stable/users/quickstart/ for more -# information on the different project types - -# docroot: # Relative path to the directory containing index.php. - -# php_version: "8.4" # PHP version to use, "5.6" through "8.5" - -# You can explicitly specify the webimage but this -# is not recommended, as the images are often closely tied to DDEV's' behavior, -# so this can break upgrades. - -# webimage: -# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason. -# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.* - -# database: -# type: # mysql, mariadb, postgres -# version: # database version, like "10.11" or "8.0" -# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8 -# MySQL versions can be 5.5-8.0, 8.4 -# PostgreSQL versions can be 9-18 - -# router_http_port: # Port to be used for http (defaults to global configuration, usually 80) -# router_https_port: # Port for https (defaults to global configuration, usually 443) - -# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" -# Note that for most people the commands -# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, -# as leaving Xdebug enabled all the time is a big performance hit. - -# xhgui_http_port: "8143" -# xhgui_https_port: "8142" -# The XHGui ports can be changed from the default 8143 and 8142 -# Very rarely used - -# host_xhgui_port: "8142" -# Can be used to change the host binding port of the XHGui -# application. Rarely used; only when port conflict and -# bind_all_ports is used (normally with router disabled) - -# xhprof_mode: [prepend|xhgui|global] -# Default is "xhgui" - -# webserver_type: nginx-fpm, apache-fpm, generic - -# timezone: Europe/Berlin -# If timezone is unset, DDEV will attempt to derive it from the host system timezone -# using the $TZ environment variable or the /etc/localtime symlink. -# This is the timezone used in the containers and by PHP; -# it can be set to any valid timezone, -# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -# For example Europe/Dublin or MST7MDT - -# composer_root: -# Relative path to the Composer root directory from the project root. This is -# the directory which contains the composer.json and where all Composer related -# commands are executed. - -# composer_version: "2" -# You can set it to "" or "2" (default) for Composer v2 -# to use the latest major version available at the time your container is built. -# It is also possible to use each other Composer version channel. This includes: -# - 2.2 (latest Composer LTS version) -# - stable -# - preview -# - snapshot -# Alternatively, an explicit Composer version may be specified, for example "2.2.18". -# To reinstall Composer after the image was built, run "ddev utility rebuild". - -# nodejs_version: "24" -# change from the default system Node.js version to any other version. -# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information -# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation. - -# corepack_enable: false -# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm - -# additional_hostnames: -# - somename -# - someothername -# would provide http and https URLs for "somename.ddev.site" -# and "someothername.ddev.site". - -# additional_fqdns: -# - example.com -# - sub1.example.com -# would provide http and https URLs for "example.com" and "sub1.example.com" -# Please take care with this because it can cause great confusion. - -# upload_dirs: "custom/upload/dir" -# -# upload_dirs: -# - custom/upload/dir -# - ../private -# -# would set the destination paths for ddev import-files to /custom/upload/dir -# When Mutagen is enabled this path is bind-mounted so that all the files -# in the upload_dirs don't have to be synced into Mutagen. - -# disable_upload_dirs_warning: false -# If true, turns off the normal warning that says -# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" - -# ddev_version_constraint: "" -# Example: -# ddev_version_constraint: ">= 1.24.8" -# This will enforce that the running ddev version is within this constraint. -# See https://github.com/Masterminds/semver#checking-version-constraints for -# supported constraint formats - # working_dir: # web: /var/www/html # db: /home @@ -147,153 +32,7 @@ corepack_enable: false # These values specify the destination directory for ddev ssh and the # directory in which commands passed into ddev exec are run. -# omit_containers: [db, ddev-ssh-agent] -# Currently only these containers are supported. Some containers can also be -# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit -# the "db" container, several standard features of DDEV that access the -# database container will be unusable. In the global configuration it is also -# possible to omit ddev-router, but not here. - -# performance_mode: "global" -# DDEV offers performance optimization strategies to improve the filesystem -# performance depending on your host system. Should be configured globally. -# -# If set, will override the global config. Possible values are: -# - "global": uses the value from the global config. -# - "none": disables performance optimization for this project. -# - "mutagen": enables Mutagen for this project. -# -# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen - -# fail_on_hook_fail: False -# Decide whether 'ddev start' should be interrupted by a failing hook - -# host_https_port: "59002" -# The host port binding for https can be explicitly specified. It is -# dynamic unless otherwise specified. -# This is not used by most people, most people use the *router* instead -# of the localhost port. - -# host_webserver_port: "59001" -# The host port binding for the ddev-webserver can be explicitly specified. It is -# dynamic unless otherwise specified. -# This is not used by most people, most people use the *router* instead -# of the localhost port. - -# host_db_port: "59002" -# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic -# unless explicitly specified. - -# mailpit_http_port: "8025" -# mailpit_https_port: "8026" -# The Mailpit ports can be changed from the default 8025 and 8026 - -# host_mailpit_port: "8025" -# The mailpit port is not normally bound on the host at all, instead being routed -# through ddev-router, but it can be bound directly to localhost if specified here. - -# webimage_extra_packages: ['php${DDEV_PHP_VERSION}-tidy', 'php${DDEV_PHP_VERSION}-yac'] -# Extra Debian packages that are needed in the webimage can be added here - -# dbimage_extra_packages: [netcat, telnet, sudo] -# Extra Debian packages that are needed in the dbimage can be added here - -# use_dns_when_possible: true -# If the host has internet access and the domain configured can -# successfully be looked up, DNS will be used for hostname resolution -# instead of editing /etc/hosts -# Defaults to true - -# project_tld: ddev.site -# The top-level domain used for project URLs -# The default "ddev.site" allows DNS lookup via a wildcard - # share_default_provider: ngrok # The default share provider to use for "ddev share" # Defaults to global configuration, usually "ngrok" # Can be "ngrok" or "cloudflared" or the name of a custom provider from .ddev/share-providers/ - -# share_provider_args: --basic-auth username:pass1234 -# Provide extra flags to the share provider script -# See https://docs.ddev.com/en/stable/users/configuration/config/#share_provider_args - -# disable_settings_management: false -# If true, DDEV will not create CMS-specific settings files like -# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php -# In this case the user must provide all such settings. - -# You can inject environment variables into the web container with: -# web_environment: -# - SOMEENV=somevalue -# - SOMEOTHERENV=someothervalue - -# no_project_mount: false -# (Experimental) If true, DDEV will not mount the project into the web container; -# the user is responsible for mounting it manually or via a script. -# This is to enable experimentation with alternate file mounting strategies. -# For advanced users only! - -# bind_all_interfaces: false -# If true, host ports will be bound on all network interfaces, -# not the localhost interface only. This means that ports -# will be available on the local network if the host firewall -# allows it. - -# default_container_timeout: 120 -# The default time that DDEV waits for all containers to become ready can be increased from -# the default 120. This helps in importing huge databases, for example. - -#web_extra_exposed_ports: -#- name: nodejs -# container_port: 3000 -# http_port: 2999 -# https_port: 3000 -#- name: something -# container_port: 4000 -# https_port: 4000 -# http_port: 3999 -# Allows a set of extra ports to be exposed via ddev-router -# Fill in all three fields even if you don’t intend to use the https_port! -# If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. -# -# The port behavior on the ddev-webserver must be arranged separately, for example -# using web_extra_daemons. -# For example, with a web app on port 3000 inside the container, this config would -# expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 -# web_extra_exposed_ports: -# - name: myapp -# container_port: 3000 -# http_port: 9998 -# https_port: 9999 - -#web_extra_daemons: -#- name: "http-1" -# command: "/var/www/html/node_modules/.bin/http-server -p 3000" -# directory: /var/www/html -#- name: "http-2" -# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" -# directory: /var/www/html - -# override_config: false -# By default, config.*.yaml files are *merged* into the configuration -# But this means that some things can't be overridden -# For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge -# and you can't erase existing hooks or all environment variables. -# However, with "override_config: true" in a particular config.*.yaml file, -# 'use_dns_when_possible: false' can override the existing values, and -# hooks: -# post-start: [] -# or -# web_environment: [] -# or -# additional_hostnames: [] -# can have their intended affect. 'override_config' affects only behavior of the -# config.*.yaml file it exists in. - -# Many DDEV commands can be extended to run tasks before or after the -# DDEV command is executed, for example "post-start", "post-import-db", -# "pre-composer", "post-composer" -# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more -# information on the commands that can be extended and the tasks you can define -# for them. Example: -#hooks: diff --git a/.ddev/docker-compose.phpmyadmin.yaml b/.ddev/docker-compose.phpmyadmin.yaml index 7c743b013..1064069f3 100644 --- a/.ddev/docker-compose.phpmyadmin.yaml +++ b/.ddev/docker-compose.phpmyadmin.yaml @@ -22,9 +22,6 @@ services: - UPLOAD_LIMIT=4000M - HTTP_EXPOSE=8036:80 - HTTPS_EXPOSE=8037:80 - healthcheck: - interval: 120s - timeout: 2s - retries: 1 depends_on: - - db + db: + condition: service_healthy diff --git a/.ddev/nginx/fossbilling-security.conf b/.ddev/nginx/fossbilling-security.conf index 6c1c27e62..96de32725 100644 --- a/.ddev/nginx/fossbilling-security.conf +++ b/.ddev/nginx/fossbilling-security.conf @@ -1,16 +1,20 @@ # Deny direct access to sensitive project files and directories. location = /config.php { - return 403; + deny all; } location ^~ /vendor/ { - return 403; + deny all; } location ~* ^/themes/[^/]+/config/ { - return 403; + deny all; } location ^~ /data/ { - return 403; + deny all; +} + +location ~* (?:\.(?:twig|lock|old|ya?ml|htaccess|htpasswd)|~)$ { + deny all; } diff --git a/.ddev/nginx_full/fossbilling.dev.conf b/.ddev/nginx_full/fossbilling.dev.conf deleted file mode 100644 index bdc73c8e0..000000000 --- a/.ddev/nginx_full/fossbilling.dev.conf +++ /dev/null @@ -1,67 +0,0 @@ -server { - listen 80; - listen 443 ssl; - sendfile off; - include /etc/nginx/mime.types; - - # Set your domain name here - server_name fossbilling.dev; - - # Document root - root /var/www/html/src; - - # SSL Setup - ssl_certificate /etc/ssl/certs/master.crt; - ssl_certificate_key /etc/ssl/certs/master.key; - - # Security Headers - #add_header Strict-Transport-Security "max-age=31536000;" always; - #add_header X-Content-Type-Options nosniff; - - # Set the index - index index.php; - - # Block access to sensitive files - location ~* \.(htaccess|htpasswd|bak|conf|inc|ini|lock|log|old|sh|sql|twig|ya?ml)$|(composer\.json|README\.md|CHANGELOG\.md)$ { - return 403; - } - - # Block access to hidden files except .well-known - location ~ /\.(?!well-known\/) { - return 403; - } - - # Block access to theme configuration files - location ~* ^/themes/[^/]+/config/ { - return 403; - } - - # Deny access to runtime data. - location ^~ /data/ { - return 403; - } - - # Routing - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - # PHP Handling - location ~ ^/(index\.php|ipn\.php|install/index\.php|install/install\.php)$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - - # On severs with multiple PHP versions the syntax the socket would likely be similar to "unix:/run/php/php8.4-fpm.sock;" - fastcgi_pass unix:/run/php-fpm.sock; - - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_intercept_errors on; - } - - # Block PHP files - location ~* \.php$ { - return 403; - } -} diff --git a/.ddev/php/fossbilling-security.ini b/.ddev/php/fossbilling-security.ini new file mode 100644 index 000000000..50e5b8631 --- /dev/null +++ b/.ddev/php/fossbilling-security.ini @@ -0,0 +1,2 @@ +[PHP] +disable_functions = exec,passthru,system,shell_exec,popen,proc_open diff --git a/.ddev/web-entrypoint.d/10-fossbilling-php-security.sh b/.ddev/web-entrypoint.d/10-fossbilling-php-security.sh deleted file mode 100755 index 0a522f7b1..000000000 --- a/.ddev/web-entrypoint.d/10-fossbilling-php-security.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -eu - -PHP_VERSION="$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')" -FPM_CONF_DIR="/etc/php/${PHP_VERSION}/fpm/conf.d" -SECURITY_INI="${FPM_CONF_DIR}/99-fossbilling-security.ini" - -cat > "${SECURITY_INI}" <<'EOF' -[PHP] -disable_functions = exec,passthru,system,shell_exec,popen,proc_open -EOF diff --git a/src/library/FOSSBilling/Http/ExceptionResponseFactory.php b/src/library/FOSSBilling/Http/ExceptionResponseFactory.php index 7db78029e..5e2a4df97 100644 --- a/src/library/FOSSBilling/Http/ExceptionResponseFactory.php +++ b/src/library/FOSSBilling/Http/ExceptionResponseFactory.php @@ -41,7 +41,10 @@ final readonly class ExceptionResponseFactory return new Response($this->renderWhoops($exception), $this->getStatusCode($exception)); } - return new Response((new ErrorPage())->renderPage($exception->getCode(), $message), $this->getStatusCode($exception)); + // PDO exceptions can carry a string SQLSTATE instead of an application error code. + $code = $exception->getCode(); + + return new Response((new ErrorPage())->renderPage(is_int($code) ? $code : 0, $message), $this->getStatusCode($exception)); } public function formatTestingMessage(\Throwable $exception): string diff --git a/tests/Unit/FOSSBilling/ExceptionResponseFactoryTest.php b/tests/Unit/FOSSBilling/ExceptionResponseFactoryTest.php index 555a9e940..bc3b7a3d6 100644 --- a/tests/Unit/FOSSBilling/ExceptionResponseFactoryTest.php +++ b/tests/Unit/FOSSBilling/ExceptionResponseFactoryTest.php @@ -50,3 +50,20 @@ test('error page can render without sending output', function (): void { ->and($page)->toContain('Error Code: #404') ->and($page)->toContain('Missing route'); }); + +test('exception response factory renders string SQLSTATE errors outside testing', function (): void { + $previousEnv = getenv('APP_ENV'); + putenv('APP_ENV=prod'); + $exception = new PDOException('Unknown column'); + (new ReflectionProperty($exception, 'code'))->setValue($exception, '42S22'); + + try { + $response = (new ExceptionResponseFactory())->create($exception); + } finally { + putenv($previousEnv === false ? 'APP_ENV' : 'APP_ENV=' . $previousEnv); + } + + expect($response->getStatusCode())->toBe(Response::HTTP_INTERNAL_SERVER_ERROR) + ->and($response->getContent())->toContain('FOSSBilling Error') + ->and($response->getContent())->toContain('Unknown column'); +});