diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 8305e6f..242e8c8 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -14,6 +14,8 @@ jobs: - "4/fastcgi" - "5" - "5/fastcgi" + - "6" + - "6/fastcgi" steps: - uses: actions/checkout@v7 - name: Build the image. @@ -83,7 +85,7 @@ jobs: env: container_name: adminer_defaultserver - name: Verify that loading plugins work. - if: ${{ ! contains(steps.build.outputs.image, 'fastcgi') }} + if: ${{ ! contains(steps.build.outputs.image, 'fastcgi') && ! startsWith(matrix.variant, '6') }} run: | docker run --name $container_name -d -e ADMINER_PLUGINS="tables-filter version-noverify" ${{ steps.build.outputs.image }} docker run -i --rm --link $container_name:$container_name buildpack-deps:curl \ @@ -91,6 +93,26 @@ jobs: |grep 'verifyVersion =' env: container_name: adminer_plugins + - name: Verify that loading plugins work. + if: ${{ ! contains(steps.build.outputs.image, 'fastcgi') && startsWith(matrix.variant, '6') }} + run: | + # Since Adminer 6, the version-noverify plugin no longer injects a + # 'verifyVersion =' script. Instead it suppresses the version check, + # so the 'partial(verifyVersion' onload handler (present by default) + # must disappear when the plugin is loaded. + docker run --name ${container_name}_default -d ${{ steps.build.outputs.image }} + docker run -i --rm --link ${container_name}_default:${container_name}_default buildpack-deps:curl \ + curl -fsSL http://${container_name}_default:8080/ \ + |grep 'partial(verifyVersion' + docker run --name $container_name -d -e ADMINER_PLUGINS="tables-filter version-noverify" ${{ steps.build.outputs.image }} + docker run -i --rm --link $container_name:$container_name buildpack-deps:curl \ + curl -fsSL http://$container_name:8080/ \ + |grep '' + ! docker run -i --rm --link $container_name:$container_name buildpack-deps:curl \ + curl -fsSL http://$container_name:8080/ \ + |grep 'partial(verifyVersion' + env: + container_name: adminer_plugins - name: Show Containers and Images run: | printf "::group::docker ps -a\n" diff --git a/6/Dockerfile b/6/Dockerfile new file mode 100644 index 0000000..f9e0dc1 --- /dev/null +++ b/6/Dockerfile @@ -0,0 +1,61 @@ +FROM php:8.4-alpine + +RUN echo "upload_max_filesize = 128M" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "post_max_size = 128M" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "memory_limit = 1G" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "max_execution_time = 600" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "max_input_vars = 5000" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini + +STOPSIGNAL SIGINT + +RUN addgroup -S adminer \ +&& adduser -S -G adminer adminer \ +&& mkdir -p /var/www/html \ +&& mkdir /var/www/html/plugins-enabled \ +&& chown -R adminer:adminer /var/www/html + +WORKDIR /var/www/html + +RUN set -x \ +&& apk add --no-cache --virtual .build-deps \ + postgresql-dev \ + sqlite-dev \ + unixodbc-dev \ + freetds-dev \ +&& docker-php-ext-configure pdo_odbc --with-pdo-odbc=unixODBC,/usr \ +&& docker-php-ext-install \ + mysqli \ + pdo_pgsql \ + pdo_sqlite \ + pdo_odbc \ + pdo_dblib \ +&& runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )" \ +&& apk add --no-cache --virtual .phpexts-rundeps $runDeps \ +&& apk del --no-network .build-deps + +COPY *.php /var/www/html/ + +ENV ADMINER_VERSION=6.0.0 +ENV ADMINER_DOWNLOAD_SHA256=8181c9be10f290243724acb0e4dc930fe04440a343fb11dcc8ddb9821095e274 +ENV ADMINER_SRC_DOWNLOAD_SHA256=6e81950e86317a0c199a666d0118a315e410d179cc33111be88e41a122a0f7a8 + +RUN set -x \ +&& curl -fsSL https://github.com/vrana/adminer/releases/download/v$ADMINER_VERSION/adminer-$ADMINER_VERSION.php -o adminer.php \ +&& echo "$ADMINER_DOWNLOAD_SHA256 adminer.php" |sha256sum -c - \ +&& curl -fsSL https://github.com/vrana/adminer/archive/v$ADMINER_VERSION.tar.gz -o source.tar.gz \ +&& echo "$ADMINER_SRC_DOWNLOAD_SHA256 source.tar.gz" |sha256sum -c - \ +&& tar xzf source.tar.gz --strip-components=1 "adminer-$ADMINER_VERSION/designs/" "adminer-$ADMINER_VERSION/plugins/" \ +&& rm source.tar.gz + +COPY entrypoint.sh /usr/local/bin/ +ENTRYPOINT [ "entrypoint.sh", "docker-php-entrypoint" ] + +USER adminer +CMD [ "php", "-S", "[::]:8080", "-t", "/var/www/html" ] + +EXPOSE 8080 diff --git a/6/entrypoint.sh b/6/entrypoint.sh new file mode 100755 index 0000000..15fa976 --- /dev/null +++ b/6/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +if [ -n "$ADMINER_DESIGN" ]; then + # Only create link on initial start, to ensure that explicit changes to + # adminer.css after the container was started once are preserved. + if [ ! -e .adminer-init ]; then + ln -sf "designs/$ADMINER_DESIGN/adminer.css" . + fi +fi + +number=1 +for PLUGIN in $ADMINER_PLUGINS; do + php plugin-loader.php "$PLUGIN" > plugins-enabled/$(printf "%03d" $number)-$PLUGIN.php + number=$(($number+1)) +done + +touch .adminer-init || true + +exec "$@" diff --git a/6/fastcgi/Dockerfile b/6/fastcgi/Dockerfile new file mode 100644 index 0000000..750061d --- /dev/null +++ b/6/fastcgi/Dockerfile @@ -0,0 +1,55 @@ +FROM php:8.4-fpm-alpine + +RUN echo "upload_max_filesize = 128M" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "post_max_size = 128M" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "memory_limit = 1G" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "max_execution_time = 600" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini \ +&& echo "max_input_vars = 5000" >> /usr/local/etc/php/conf.d/0-upload_large_dumps.ini + +RUN addgroup -S adminer \ +&& adduser -S -G adminer adminer \ +&& mkdir -p /var/www/html \ +&& mkdir /var/www/html/plugins-enabled \ +&& chown -R adminer:adminer /var/www/html + +RUN set -x \ +&& apk add --no-cache --virtual .build-deps \ + postgresql-dev \ + sqlite-dev \ + unixodbc-dev \ + freetds-dev \ +&& docker-php-ext-configure pdo_odbc --with-pdo-odbc=unixODBC,/usr \ +&& docker-php-ext-install \ + mysqli \ + pdo_pgsql \ + pdo_sqlite \ + pdo_odbc \ + pdo_dblib \ +&& runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )" \ +&& apk add --no-cache --virtual .phpexts-rundeps $runDeps \ +&& apk del --no-network .build-deps + +COPY *.php /var/www/html/ + +ENV ADMINER_VERSION=6.0.0 +ENV ADMINER_DOWNLOAD_SHA256=8181c9be10f290243724acb0e4dc930fe04440a343fb11dcc8ddb9821095e274 +ENV ADMINER_SRC_DOWNLOAD_SHA256=6e81950e86317a0c199a666d0118a315e410d179cc33111be88e41a122a0f7a8 + +RUN set -x \ +&& curl -fsSL https://github.com/vrana/adminer/releases/download/v$ADMINER_VERSION/adminer-$ADMINER_VERSION.php -o adminer.php \ +&& echo "$ADMINER_DOWNLOAD_SHA256 adminer.php" |sha256sum -c - \ +&& curl -fsSL https://github.com/vrana/adminer/archive/v$ADMINER_VERSION.tar.gz -o source.tar.gz \ +&& echo "$ADMINER_SRC_DOWNLOAD_SHA256 source.tar.gz" |sha256sum -c - \ +&& tar xzf source.tar.gz --strip-components=1 "adminer-$ADMINER_VERSION/designs/" "adminer-$ADMINER_VERSION/plugins/" \ +&& rm source.tar.gz + +COPY entrypoint.sh /usr/local/bin/ +ENTRYPOINT [ "entrypoint.sh", "docker-php-entrypoint" ] + +USER adminer +CMD [ "php-fpm" ] diff --git a/6/fastcgi/entrypoint.sh b/6/fastcgi/entrypoint.sh new file mode 100755 index 0000000..15fa976 --- /dev/null +++ b/6/fastcgi/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +if [ -n "$ADMINER_DESIGN" ]; then + # Only create link on initial start, to ensure that explicit changes to + # adminer.css after the container was started once are preserved. + if [ ! -e .adminer-init ]; then + ln -sf "designs/$ADMINER_DESIGN/adminer.css" . + fi +fi + +number=1 +for PLUGIN in $ADMINER_PLUGINS; do + php plugin-loader.php "$PLUGIN" > plugins-enabled/$(printf "%03d" $number)-$PLUGIN.php + number=$(($number+1)) +done + +touch .adminer-init || true + +exec "$@" diff --git a/6/fastcgi/index.php b/6/fastcgi/index.php new file mode 100644 index 0000000..0f9aa42 --- /dev/null +++ b/6/fastcgi/index.php @@ -0,0 +1,63 @@ +loginFormField(...$args); + + return \preg_replace_callback( + "/name='auth\[server\]' value='' title='(?:[^']+)'/", + static function (array $matches): string { + return \str_replace( + "value=''", + \sprintf("value='%s'", ($_ENV['ADMINER_DEFAULT_SERVER'] ?: 'db')), + $matches[0], + ); + }, + $field, + ); + })->call($this->adminer, ...$args); + } + } + + $plugins = []; + foreach (glob('plugins-enabled/*.php') as $plugin) { + $plugins[] = require($plugin); + } + + $adminer = new \Adminer\Plugins($plugins); + + (function () { + $last = &$this->hooks['loginFormField'][\array_key_last($this->hooks['loginFormField'])]; + if ($last instanceof \Adminer\Adminer) { + $defaultServerPlugin = new DefaultServerPlugin($last); + $this->plugins[] = $defaultServerPlugin; + $last = $defaultServerPlugin; + } + })->call($adminer); + + return $adminer; + } +} + +namespace { + if (basename($_SERVER['DOCUMENT_URI'] ?? $_SERVER['REQUEST_URI']) === 'adminer.css' && is_readable('adminer.css')) { + header('Content-Type: text/css'); + readfile('adminer.css'); + exit; + } + + function adminer_object() { + return \docker\adminer_object(); + } + + require('adminer.php'); +} diff --git a/6/fastcgi/plugin-loader.php b/6/fastcgi/plugin-loader.php new file mode 100644 index 0000000..bfc392d --- /dev/null +++ b/6/fastcgi/plugin-loader.php @@ -0,0 +1,78 @@ + 1) { + fwrite(STDERR, 'Unable to load plugin file "'.$name.'", because it defines multiple classes.'."\n"); + exit(1); +} + +if (!class_exists(\Adminer\Plugin::class)) { + class_alias(stdClass::class, \Adminer\Plugin::class); +} + +// Check constructor. +$class = $classes[0]; +require($file); + +$constructor = (new \ReflectionClass($class))->getConstructor(); + +if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) { + $requiredParameters = array_slice($constructor->getParameters(), 0, $constructor->getNumberOfRequiredParameters()); + + fwrite(STDERR, 'Unable to load plugin file "'.$name.'", because it has required parameters: '.implode(', ', array_map(function ($item) { + return $item->getName(); + }, $requiredParameters))."\n". +'Create a file "'.getcwd().'/plugins-enabled/'.$name.'.php" with the following contents to load the plugin:'."\n\n". +'getDocComment().' +return new '.$class.'( + '.implode(",\n\t", array_map(function ($item) { + return '$'.$item->getName()." = ".($item->isOptional() ? var_export($item->getDefaultValue(), true) : '???'); + }, $constructor->getParameters())).' +); +'); + exit(1); +} + +echo 'loginFormField(...$args); + + return \preg_replace_callback( + "/name='auth\[server\]' value='' title='(?:[^']+)'/", + static function (array $matches): string { + return \str_replace( + "value=''", + \sprintf("value='%s'", ($_ENV['ADMINER_DEFAULT_SERVER'] ?: 'db')), + $matches[0], + ); + }, + $field, + ); + })->call($this->adminer, ...$args); + } + } + + $plugins = []; + foreach (glob('plugins-enabled/*.php') as $plugin) { + $plugins[] = require($plugin); + } + + $adminer = new \Adminer\Plugins($plugins); + + (function () { + $last = &$this->hooks['loginFormField'][\array_key_last($this->hooks['loginFormField'])]; + if ($last instanceof \Adminer\Adminer) { + $defaultServerPlugin = new DefaultServerPlugin($last); + $this->plugins[] = $defaultServerPlugin; + $last = $defaultServerPlugin; + } + })->call($adminer); + + return $adminer; + } +} + +namespace { + if (basename($_SERVER['DOCUMENT_URI'] ?? $_SERVER['REQUEST_URI']) === 'adminer.css' && is_readable('adminer.css')) { + header('Content-Type: text/css'); + readfile('adminer.css'); + exit; + } + + function adminer_object() { + return \docker\adminer_object(); + } + + require('adminer.php'); +} diff --git a/6/plugin-loader.php b/6/plugin-loader.php new file mode 100644 index 0000000..bfc392d --- /dev/null +++ b/6/plugin-loader.php @@ -0,0 +1,78 @@ + 1) { + fwrite(STDERR, 'Unable to load plugin file "'.$name.'", because it defines multiple classes.'."\n"); + exit(1); +} + +if (!class_exists(\Adminer\Plugin::class)) { + class_alias(stdClass::class, \Adminer\Plugin::class); +} + +// Check constructor. +$class = $classes[0]; +require($file); + +$constructor = (new \ReflectionClass($class))->getConstructor(); + +if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) { + $requiredParameters = array_slice($constructor->getParameters(), 0, $constructor->getNumberOfRequiredParameters()); + + fwrite(STDERR, 'Unable to load plugin file "'.$name.'", because it has required parameters: '.implode(', ', array_map(function ($item) { + return $item->getName(); + }, $requiredParameters))."\n". +'Create a file "'.getcwd().'/plugins-enabled/'.$name.'.php" with the following contents to load the plugin:'."\n\n". +'getDocComment().' +return new '.$class.'( + '.implode(",\n\t", array_map(function ($item) { + return '$'.$item->getName()." = ".($item->isOptional() ? var_export($item->getDefaultValue(), true) : '???'); + }, $constructor->getParameters())).' +); +'); + exit(1); +} + +echo '