diff --git a/config/redirects_domain.example.com.json.dist b/config/redirects_www.example.com.json.dist similarity index 100% rename from config/redirects_domain.example.com.json.dist rename to config/redirects_www.example.com.json.dist diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000000000000000000000000000000000000..31d8948d624acd82a12edd2a83bccaa1315cac0e --- /dev/null +++ b/docker/.env @@ -0,0 +1,16 @@ +# ====================================================================== +# +# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e +# values to be used in docker-composer.yml +# +# ====================================================================== + +# ----- application +APP_NAME=my_new_app + +# uid of www-data in the docker container +DOCKER_USER_UID=33 + +APP_PORT=8008 +WEBROOT=/var/www/my_new_app/public_html + diff --git a/docker/containers/db-server/mariadb/my.cnf b/docker/containers/db-server/mariadb/my.cnf new file mode 100644 index 0000000000000000000000000000000000000000..cc3b80d295df31125a4e5c600a7301e2d44b9d2f --- /dev/null +++ b/docker/containers/db-server/mariadb/my.cnf @@ -0,0 +1,3 @@ +[mysqld] +; collation-server = utf8mb4_unicode_ci +; character-set-server = utf8mb4 \ No newline at end of file diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..31263885c9f8b327a89409308bd918e7787a82f4 --- /dev/null +++ b/docker/containers/web-server/Dockerfile @@ -0,0 +1,14 @@ +# +# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 +# +FROM php:8.2-apache + +# install packages +RUN apt-get update && apt-get install -y git unzip zip + +# enable apache modules +RUN a2enmod + +# install php packages +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ +RUN install-php-extensions xdebug diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf new file mode 100644 index 0000000000000000000000000000000000000000..7ed4d6fc28954f0e749328a68bc927f268fa8b3c --- /dev/null +++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf @@ -0,0 +1,17 @@ +# +# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9 +# +<VirtualHost *:80> + DocumentRoot /var/www/my_new_app/public_html + <Directory /var/www/my_new_app/public_html> + AllowOverride None + Order Allow,Deny + Allow from All + </Directory> + + # example to prevent access with http + <Location "/no-access"> + Require all denied + </Location> + +</VirtualHost> \ No newline at end of file diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini new file mode 100644 index 0000000000000000000000000000000000000000..aa13bd779afa40bbfa25f10adef9baeae6d14f7d --- /dev/null +++ b/docker/containers/web-server/php/extra-php-config.ini @@ -0,0 +1,24 @@ +; +; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2 +; +[PHP] + +error_reporting=E_ALL +display_errors=1 + +; ---------------------------------------------------------------------- +; XDEBUG STUFF BELOW +; ---------------------------------------------------------------------- +; +; error_reporting=E_ALL +; +; [xdebug] +; xdebug.mode=develop,debug +; ; xdebug.client_host=localhost +; xdebug.start_with_request=yes +; ; xdebug.start_with_request=trigger +; +; xdebug.log=/tmp/xdebug.log +; xdebug.discover_client_host = 1 +; ; xdebug.client_port=9003 +; xdebug.idekey="netbeans-xdebug" \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9b5d9940d606b8c37485cad39e0a4393efc06120 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,44 @@ +# +# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b +# +# ====================================================================== +# +# (1) see .env for set variables +# (2) run "docker-compose up" to startup +# +# ====================================================================== +version: '3.9' + +networks: + my_new_app-network: + +services: + + # ----- apache httpd + php + my_new_app-web-server: + build: + context: . + dockerfile: ./containers/web-server/Dockerfile + image: "php:8.2-apache" + container_name: 'my_new_app-server' + ports: + - '${APP_PORT}:80' + + working_dir: ${WEBROOT} + + volumes: + - ../:/var/www/${APP_NAME} + - ./containers/web-server/apache/sites-enabled:/etc/apache2/sites-enabled + - ./containers/web-server/php/extra-php-config.ini:/usr/local/etc/php/conf.d/extra-php-config.ini + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 10s + timeout: 3s + retries: 5 + # start_period: 40s + + networks: + - my_new_app-network + + user: ${DOCKER_USER_UID} + diff --git a/docker/init.sh b/docker/init.sh new file mode 100755 index 0000000000000000000000000000000000000000..060db24942ad10e1bb0f3abca96443d85f55d289 --- /dev/null +++ b/docker/init.sh @@ -0,0 +1,340 @@ +#!/bin/bash +# ====================================================================== +# +# DOCKER PHP DEV ENVIRONMENT :: INIT +# +# ---------------------------------------------------------------------- +# 2021-11-nn v1.0 <axel.hahn@iml.unibe.ch> +# 2022-07-19 v1.1 <axel.hahn@iml.unibe.ch> support multiple dirs for setfacl +# 2022-11-16 v1.2 <www.axel-hahn.de> use docker-compose -p "$APP_NAME" +# 2022-12-18 v1.3 <www.axel-hahn.de> add -p "$APP_NAME" in other docker commands +# 2022-12-20 v1.4 <axel.hahn@unibe.ch> replace fgrep with grep -F +# 2023-03-06 v1.5 <www.axel-hahn.de> up with and without --build +# 2023-08-17 v1.6 <www.axel-hahn.de> menu selection with single key (without return) +# ====================================================================== + +cd $( dirname $0 ) +. $( basename $0 ).cfg + +# git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git +selfgitrepo="docker-php-starterkit.git" + +_version="1.6" + +# ---------------------------------------------------------------------- +# FUNCTIONS +# ---------------------------------------------------------------------- + +# draw a headline 2 +function h2(){ + echo + echo -e "\e[33m>>>>> $*\e[0m" +} + +# draw a headline 3 +function h3(){ + echo + echo -e "\e[34m----- $*\e[0m" +} + +# function _gitinstall(){ +# h2 "install/ update app from git repo ${gitrepo} in ${gittarget} ..." +# test -d ${gittarget} && ( cd ${gittarget} && git pull ) +# test -d ${gittarget} || git clone -b ${gitbranch} ${gitrepo} ${gittarget} +# } + +# set acl on local directory +function _setWritepermissions(){ + h2 "set write permissions on ${gittarget} ..." + + local _user=$( id -gn ) + typeset -i local _user_uid=0 + test -f /etc/subuid && _user_uid=$( grep $_user /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1 + typeset -i local DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID + + set -vx + + for mywritedir in ${WRITABLEDIR} + do + + echo "--- ${mywritedir}" + # remove current acl + sudo setfacl -bR "${mywritedir}" + + # default permissions: both the host user and the user with UID 33 (www-data on many systems) are owners with rwx perms + sudo setfacl -dRm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}" + + # permissions: make both the host user and the user with UID 33 owner with rwx perms for all existing files/directories + sudo setfacl -Rm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}" + done + + set +vx +} + +# cleanup starterkit git data +function _removeGitdata(){ + h2 "Remove git data of starterkit" + echo -n "Current git remote url: " + git config --get remote.origin.url + git config --get remote.origin.url 2>/dev/null | grep $selfgitrepo >/dev/null + if [ $? -eq 0 ]; then + echo + echo -n "Delete local .git and .gitignore? [y/N] > " + read answer + test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore ) + else + echo "It was done already - $selfgitrepo was not found." + fi + +} + +# helper function: cut a text file starting from database start marker +# see _generateFiles() +function _fix_no-db(){ + local _file=$1 + if [ $DB_ADD = false ]; then + typeset -i local iStart=$( cat ${_file} | grep -Fn "$CUTTER_NO_DATABASE" | cut -f 1 -d ':' )-1 + if [ $iStart -gt 0 ]; then + sed -ni "1,${iStart}p" ${_file} + fi + fi +} + +# loop over all files in templates subdir make replacements and generate +# a target file. +# It skips if +# - 1st line is not starting with "# TARGET: filename" +# - target file has no updated lines +function _generateFiles(){ + + # re-read config vars + . $( basename $0 ).cfg + + local _tmpfile=/tmp/newfilecontent$$.tmp + h2 "generate files from templates..." + for mytpl in $( ls -1 ./templates/* ) + do + # h3 $mytpl + local _doReplace=1 + + # fetch traget file from first line + target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) + + if [ -z "$target" ]; then + echo SKIP: $mytpl - target was not found in 1st line + _doReplace=0 + fi + + # write generated files to target + if [ $_doReplace -eq 1 ]; then + + # write file from line 2 to a tmp file + sed -n '2,$p' $mytpl >$_tmpfile + + # add generator + # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile + local _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) + sed -i "s#{{generator}}#GENERATED BY $( basename $0 ) - template: $mytpl - $_md5#g" $_tmpfile + + # loop over vars to make the replacement + grep "^[a-zA-Z]" $( basename $0 ).cfg | while read line + do + # echo replacement: $line + mykey=$( echo $line | cut -f 1 -d '=' ) + myvalue="$( eval echo \"\${$mykey}\" )" + # grep "{{$mykey}}" $_tmpfile + + # TODO: multiline values fail here in replacement with sed + sed -i "s#{{$mykey}}#${myvalue}#g" $_tmpfile + done + _fix_no-db $_tmpfile + + # echo "changes for $target:" + diff "../$target" "$_tmpfile" | grep -v "$_md5" | grep -v "^---" | grep . + if [ $? -eq 0 -o ! -f "../$target" ]; then + echo -n "$mytpl - changes detected - writing [$target] ... " + mkdir -p $( dirname "../$target" ) || exit 2 + mv "$_tmpfile" "../$target" || exit 2 + echo OK + else + rm -f $_tmpfile + echo "SKIP: $mytpl - Nothing to do." + fi + fi + echo + done + +} + +# loop over all files in templates subdir make replacements and generate +# a traget file. +function _removeGeneratedFiles(){ + h2 "remove generated files..." + for mytpl in $( ls -1 ./templates/* ) + do + h3 $mytpl + + # fetch traget file from first line + target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) + + if [ ! -z "$target" -a -f "../$target" ]; then + echo -n "REMOVING " + ls -l "../$target" || exit 2 + rm -f "../$target" || exit 2 + echo OK + else + echo SKIP: $target + fi + + done +} + +function _showContainers(){ + local bLong=$1 + h2 CONTAINERS + if [ -z "$bLong" ]; then + docker-compose -p "$APP_NAME" ps + else + docker ps | grep $APP_NAME + fi +} + + +# a bit stupid ... i think I need to delete it. +function _showInfos(){ + _showContainers long + h2 INFO + + h3 "processes" + docker-compose top + + h3 "Check app port" + >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && ( + echo "OK, app port ${APP_PORT} is reachable" + echo + echo "In a web browser open:" + echo " $frontendurl" + ) + h3 "Check database port" + >/dev/tcp/localhost/${DB_PORT} 2>/dev/null && ( + echo "OK, db port ${DB_PORT} is reachable" + echo + echo "In a local DB admin tool:" + echo " host : localhost" + echo " port : ${DB_PORT}" + echo " user : root" + echo " password: ${MYSQL_ROOT_PASS}" + ) + echo +} + +# helper for menu: print an inverted key +function _key(){ + printf "\e[4;7m ${1} \e[0m" +} + +# helper: wait for a return key +function _wait(){ + echo -n "... press RETURN > "; read -r +} + +# ---------------------------------------------------------------------- +# MAIN +# ---------------------------------------------------------------------- + +action=$1 + +while true; do + echo + echo -e "\e[32m===== INITIALIZER FOR DOCKER APP [$APP_NAME] v$_version ===== \e[0m\n\r" + + if [ -z "$action" ]; then + + _showContainers + + h2 MENU + echo " $( _key g ) - remove git data of starterkit" + echo + echo " $( _key i ) - init application: set permissions" + echo " $( _key t ) - generate files from templates" + echo " $( _key T ) - remove generated files" + echo + echo " $( _key u ) - startup containers docker-compose ... up -d" + echo " $( _key U ) - startup containers docker-compose ... up -d --build" + echo " $( _key s ) - shutdown containers docker-compose stop" + echo " $( _key r ) - remove containers docker-compose rm -f" + echo + echo " $( _key m ) - more infos" + echo " $( _key c ) - console (bash)" + echo + echo " $( _key q ) - quit" + echo + echo -n " select >" + read -rn 1 action + echo + fi + + case "$action" in + g) + _removeGitdata + ;; + i) + # _gitinstall + _setWritepermissions + ;; + t) + _generateFiles + ;; + T) + _removeGeneratedFiles + rm -rf containers + ;; + # not in the menu + # f) + # _removeGeneratedFiles + # _generateFiles + # _wait + # ;; + m) + _showInfos + _wait + ;; + u|U) + dockerUp="docker-compose -p "$APP_NAME" --verbose up -d --remove-orphans" + if [ "$action" = "U" ]; then + dockerUp+=" --build" + fi + if $dockerUp; then + echo "In a web browser:" + echo " $frontendurl" + else + echo "ERROR: docker-compose up failed :-/" + docker-compose -p "$APP_NAME" logs | tail + fi + echo + + _wait + ;; + s) + docker-compose -p "$APP_NAME" stop + ;; + r) + docker-compose -p "$APP_NAME" rm -f + ;; + c) + docker ps + echo -n "id or name >" + read dockerid + test -z "$dockerid" || docker exec -it $dockerid /bin/bash + ;; + q) + exit 0; + ;; + *) + test -n "$action" && ( echo " ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 ) + esac + action= +done + + +# ---------------------------------------------------------------------- diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg new file mode 100644 index 0000000000000000000000000000000000000000..7c781c5f4288fe3acf990cd7ad6db2241f78a9bd --- /dev/null +++ b/docker/init.sh.cfg @@ -0,0 +1,69 @@ +# ====================================================================== +# +# settings for init.sh and base values for replacements in template files +# This script is sourced by init.sh ... this file is bash syntax +# +# ---------------------------------------------------------------------- +# 2021-12-17 <axel.hahn@iml.unibe.ch> +# ====================================================================== + +APP_NAME=my_new_app + +# web port 80 in container is seen on localhost as ... +APP_PORT=8008 + +APP_APT_PACKAGES="git unzip zip" + +#APP_APACHE_MODULES="rewrite" +APP_APACHE_MODULES="" + +APP_PHP_VERSION=8.2 +# APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug" +# APP_PHP_MODULES="curl mbstring xml zip xdebug" +APP_PHP_MODULES="xdebug" + +# optional exec command after container was started with init.sh script +# APP_ONSTARTUP="php /var/www/${APP_NAME}/public_html/myservice.php" +APP_ONSTARTUP="" + +# ---------------------------------------------------------------------- + +# add a container with database? +DB_ADD=false + +# ---------------------------------------------------------------------- +# for an optional database server + +DB_PORT=13306 + +# ----- database settings +MYSQL_IMAGE=mariadb:10.5.9 +MYSQL_RANDOM_ROOT_PASSWORD=0 +MYSQL_ALLOW_EMPTY_PASSWORD=0 +MYSQL_ROOT_PASS=12345678 +MYSQL_USER=${APP_NAME} +MYSQL_PASS=mypassword +MYSQL_DB=${APP_NAME} + + + +# ====================================================================== +# ignore things below + + +# where to set acl where local user and web user in container +# can write simultanously +WRITABLEDIR=../public_html + + +# web service user in container +DOCKER_USER_UID=33 + +# document root inside web-server container +WEBROOT=/var/www/${APP_NAME}/public_html + +CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE" + +frontendurl=http://localhost:${APP_PORT}/ + +# ---------------------------------------------------------------------- diff --git a/docker/templates/docker-compose.yml b/docker/templates/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..3e039a763699162c9f53e84e5e7a78ddfa34d717 --- /dev/null +++ b/docker/templates/docker-compose.yml @@ -0,0 +1,71 @@ +# TARGET: docker/docker-compose.yml +# +# {{generator}} +# +# ====================================================================== +# +# (1) see .env for set variables +# (2) run "docker-compose up" to startup +# +# ====================================================================== +version: '3.9' + +networks: + {{APP_NAME}}-network: + +services: + + # ----- apache httpd + php + {{APP_NAME}}-web-server: + build: + context: . + dockerfile: ./containers/web-server/Dockerfile + image: "php:{{APP_PHP_VERSION}}-apache" + container_name: '{{APP_NAME}}-server' + ports: + - '${APP_PORT}:80' + + working_dir: ${WEBROOT} + + volumes: + - ../:/var/www/${APP_NAME} + - ./containers/web-server/apache/sites-enabled:/etc/apache2/sites-enabled + - ./containers/web-server/php/extra-php-config.ini:/usr/local/etc/php/conf.d/extra-php-config.ini + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 10s + timeout: 3s + retries: 5 + # start_period: 40s + + networks: + - {{APP_NAME}}-network + + user: ${DOCKER_USER_UID} + + # --- 8< --- {{CUTTER_NO_DATABASE}} --- 8< --- + + depends_on: + - {{APP_NAME}}-db-server + + # ----- mariadb + {{APP_NAME}}-db-server: + image: {{MYSQL_IMAGE}} + container_name: '${APP_NAME}-db' + # restart: always + ports: + - '${DB_PORT}:3306' + environment: + MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASS}' + MYSQL_USER: '${MYSQL_USER}' + MYSQL_PASSWORD: '${MYSQL_PASS}' + MYSQL_DATABASE: '${MYSQL_DB}' + volumes: + # - ./containers/db-server/db_data:/var/lib/mysql + - ./containers/db-server/mariadb/my.cnf:/etc/mysql/conf.d/my.cnf + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD + interval: 5s + retries: 5 + networks: + - {{APP_NAME}}-network diff --git a/docker/templates/dot_env b/docker/templates/dot_env new file mode 100644 index 0000000000000000000000000000000000000000..bc8af1d5372ca0731b4e2eebd0a3e95cc2d1c78b --- /dev/null +++ b/docker/templates/dot_env @@ -0,0 +1,28 @@ +# TARGET: docker/.env +# ====================================================================== +# +# {{generator}} +# values to be used in docker-composer.yml +# +# ====================================================================== + +# ----- application +APP_NAME={{APP_NAME}} + +# uid of www-data in the docker container +DOCKER_USER_UID={{DOCKER_USER_UID}} + +APP_PORT={{APP_PORT}} +WEBROOT={{WEBROOT}} + +# --- 8< --- {{CUTTER_NO_DATABASE}} --- 8< --- + +DB_PORT={{DB_PORT}} + +# ----- database settings +MYSQL_RANDOM_ROOT_PASSWORD={{MYSQL_RANDOM_ROOT_PASSWORD}} +MYSQL_ALLOW_EMPTY_PASSWORD={{MYSQL_ALLOW_EMPTY_PASSWORD}} +MYSQL_ROOT_PASS={{MYSQL_ROOT_PASS}} +MYSQL_USER={{APP_NAME}} +MYSQL_PASS={{MYSQL_PASS}} +MYSQL_DB={{APP_NAME}} diff --git a/docker/templates/extra-php-config.ini b/docker/templates/extra-php-config.ini new file mode 100644 index 0000000000000000000000000000000000000000..d3f53279bbc03ee66876efaeec97cfdd207f3c94 --- /dev/null +++ b/docker/templates/extra-php-config.ini @@ -0,0 +1,25 @@ +# TARGET: docker/containers/web-server/php/extra-php-config.ini +; +; {{generator}} +; +[PHP] + +error_reporting=E_ALL +display_errors=1 + +; ---------------------------------------------------------------------- +; XDEBUG STUFF BELOW +; ---------------------------------------------------------------------- +; +; error_reporting=E_ALL +; +; [xdebug] +; xdebug.mode=develop,debug +; ; xdebug.client_host=localhost +; xdebug.start_with_request=yes +; ; xdebug.start_with_request=trigger +; +; xdebug.log=/tmp/xdebug.log +; xdebug.discover_client_host = 1 +; ; xdebug.client_port=9003 +; xdebug.idekey="netbeans-xdebug" \ No newline at end of file diff --git a/docker/templates/my.cnf b/docker/templates/my.cnf new file mode 100644 index 0000000000000000000000000000000000000000..3692f17bf2799b45b3ce9746e31e3e8d1fcb5d64 --- /dev/null +++ b/docker/templates/my.cnf @@ -0,0 +1,4 @@ +# TARGET: docker/containers/db-server/mariadb/my.cnf +[mysqld] +; collation-server = utf8mb4_unicode_ci +; character-set-server = utf8mb4 \ No newline at end of file diff --git a/docker/templates/readme.md b/docker/templates/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..34c2c57c1ad0518f5cc00d12148cdddc0720187f --- /dev/null +++ b/docker/templates/readme.md @@ -0,0 +1,7 @@ +# Templates + +## Rules + +* in the first line must be a line `# TARGET: [name of target file]` to define the target file +* Placeholdrs have the syntax variable in double brackets, i.e. `{{VARNAME}}` +* variables to be replaced are those in docker/init.sh.cfg and `{{genrator}}` diff --git a/docker/templates/vhost_app.conf b/docker/templates/vhost_app.conf new file mode 100644 index 0000000000000000000000000000000000000000..ae25f2f19107a610cea871b83b55cd78ebd4d274 --- /dev/null +++ b/docker/templates/vhost_app.conf @@ -0,0 +1,18 @@ +# TARGET: docker/containers/web-server/apache/sites-enabled/vhost_app.conf +# +# {{generator}} +# +<VirtualHost *:80> + DocumentRoot {{WEBROOT}} + <Directory {{WEBROOT}}> + AllowOverride None + Order Allow,Deny + Allow from All + </Directory> + + # example to prevent access with http + <Location "/no-access"> + Require all denied + </Location> + +</VirtualHost> \ No newline at end of file diff --git a/docker/templates/web-server-Dockerfile b/docker/templates/web-server-Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d2443af6363a7d5ad2194d9c02349fb8fc3e218d --- /dev/null +++ b/docker/templates/web-server-Dockerfile @@ -0,0 +1,15 @@ +# TARGET: docker/containers/web-server/Dockerfile +# +# {{generator}} +# +FROM php:{{APP_PHP_VERSION}}-apache + +# install packages +RUN apt-get update && apt-get install -y {{APP_APT_PACKAGES}} + +# enable apache modules +RUN a2enmod {{APP_APACHE_MODULES}} + +# install php packages +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ +RUN install-php-extensions {{APP_PHP_MODULES}} diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md" index f98e2e196f855dec69aaf2f14762abe7bcc305f2..99773cfd6df1ecfd8aa573663ab5c07fa1af4797 100644 --- "a/docs/10_\360\237\223\221_Description.md" +++ "b/docs/10_\360\237\223\221_Description.md" @@ -1,6 +1,6 @@ ## Requirements -* PHP 7+ +* PHP 7+ (up to PHP 8.2) * Webserver (docs describe usage for Apache httpd) ## Features diff --git "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md" index 2c7480e5b0c6190a9bdaa3cf9ae896c27325ecc7..c69d22a1be4209d3f63264bd8fdd9896b8b4b67c 100644 --- "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" +++ "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md" @@ -19,6 +19,8 @@ Then you can open **/admin** in your webbrowser, eg. `http(s)://servername/admi You get a list with all defined domains and its redirects + aliases. + + In the table you see the columns * **Host** - the hostname/ FQDN that has a redirect rule diff --git a/docs/images/admin_ui.png b/docs/images/admin_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f9671a7a69da673f55571a7e127a94848f8b09 Binary files /dev/null and b/docs/images/admin_ui.png differ diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 9b52942c85b5d9f0938fd54efdce480f6d24b159..74c931cf6babd09c289c1569cc8aa4abda0a9943 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -13,6 +13,7 @@ * ---------------------------------------------------------------------- * 2022-02-03 v0.1 <axel.hahn@iml.unibe.ch> initial version * 2022-05-31 v0.2 <axel.hahn@iml.unibe.ch> optical changes; use debugredirect=1 if url is a local domain + * 2023-08-28 v1.0 <axel.hahn@unibe.ch> Welcome message if there is no config yet * ---------------------------------------------------------------------- */ @@ -37,6 +38,7 @@ $aIco=[ 'type_alias'=>'◻️', 'url'=>'🌐', + 'welcome'=>'🪄', ]; // ---------------------------------------------------------------------- @@ -52,7 +54,12 @@ function getId($sDomain){ // ---------------------------------------------------------------------- if (!$oR->isEnabled()){ - $sHtml.='<div class="error">Admin is disabled.</div>'; + $sHtml.='<div class="content"> + <h3>Nothing to see here.</h3> + <div class="error"> + The Admin interface is disabled. + </div> + </div>'; } else { @@ -99,32 +106,7 @@ if (!$oR->isEnabled()){ // ---------- LOOP OVER ALL ENTRIES - - $sHtml.=' - <!-- - <h2>'.$aIco['h2_head'].' Http head tester</h2> - <div class="content"> - <form> - '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/> - <button>Http HEAD</button> - </form> - </div> - <br> - --> - <h2>'.$aIco['h2_config'].' Domains and their redirects</h2> - <div class="content"> - - <table class="mydatatable"><thead> - <tr> - <th>Host</th> - <th>Ip address</th> - <th>Setup</th> - <th>Type</th> - <th>From</th> - <th>Code</th> - <th>Target</th> - </tr> - </thead><tbody>'; + $sTable=''; foreach($aHosts as $sHost => $aCfg){ $sTdFirst='<tr class="cfgtype-'.$aCfg['type'].'">' .'<td>' @@ -159,7 +141,7 @@ if (!$oR->isEnabled()){ if (count($aCfg['redirects'][$sType])){ foreach($aCfg['redirects'][$sType] as $sFrom=>$aTo){ $iCount++; - $sHtml.=$sTdFirst + $sTable.=$sTdFirst .'<td class="type-'.$sType.'">'.$sType.'</td>' .'<td class="type-'.$sType.'">' .($sType == 'direct' @@ -177,29 +159,24 @@ if (!$oR->isEnabled()){ } else { // type = alias // $sHtml.='<tr>'.$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <a href="#'.getId($aCfg['target']).'">'.$aCfg['target'].'</a>' : '').'</td></tr>'; - $sHtml.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>'; + $sTable.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>'; } } - $sHtml.='</tbody></table></div>' - /* - .'<h2>Config array</h2> - <pre>'.print_r($aHosts, 1).'</pre>' - */ - ; - $sErrors = $sErrors - ? '<h2>'.$aIco['h2_err'].' Found errors</h2>' - .'<div class="content">' - .'<ol class="error">' - .$sErrors - .'</ol>' - .'</div>' - : '' - ; - - $sHtml.='' - .'<br><br>' + $sTable=$sTable + ? '<table class="mydatatable"><thead> + <tr> + <th>Host</th> + <th>Ip address</th> + <th>Setup</th> + <th>Type</th> + <th>From</th> + <th>Code</th> + <th>Target</th> + </tr> + </thead><tbody>'.$sTable.'</tbody></table></div>' + . '<br><br>' .'<div class="content legend">' . '<strong>Legend</strong>:<br>' . '<table><tbody>' @@ -230,7 +207,54 @@ if (!$oR->isEnabled()){ .'</tbody></table>' .'</div>' - .'<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>' + : '<h3>'.$aIco['welcome'].' Welcome!</h3> + <p> + Thank you for the installation!<br> + Now is a good moment to create your first config. + </p> + <ul> + <li>Go to the directory ./config/</li> + <li>Watch the *.dist files - make a copy of them to *.json (without .dist)</li> + <li>Relaod this page</li> + </ul> + <p> + See the <a href="'.$oR->urlDocs.'Configuration.html" target="_blank">Docs</a> for details. + </p> + ' + ; + + $sHtml.=' + <!-- + <h2>'.$aIco['h2_head'].' Http head tester</h2> + <div class="content"> + <form> + '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/> + <button>Http HEAD</button> + </form> + </div> + <br> + --> + <h2>'.$aIco['h2_config'].' Domains and their redirects</h2> + <div class="content">' + + /* + .'<h2>Config array</h2> + <pre>'.print_r($aHosts, 1).'</pre>' + */ + .$sTable + ; + + $sErrors = $sErrors + ? '<h2>'.$aIco['h2_err'].' Found errors</h2>' + .'<div class="content">' + .'<ol class="error">' + .$sErrors + .'</ol>' + .'</div>' + : '' + ; + + $sHtml.='<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>' ; } diff --git a/public_html/admin/main.css b/public_html/admin/main.css index a09355ae30946e6cecc4543fe85d3e6fe99262db..224eebe01ee21135fc46250bd46fceb738f863d4 100644 --- a/public_html/admin/main.css +++ b/public_html/admin/main.css @@ -3,6 +3,7 @@ body{background: #f8f8f8; color: #234; font-family: arial; margin: 0;} h1{background:rgba(0,0,0,0.05); margin: 0 0 1em;; padding: 0.5em;} h1 a{color:#234; text-decoration: none;} h2{background: #d0e0e8; color:#458; margin: 1em 0 0.5em; border-top: 2px solid #fff; border-left: 5px solid #fff; border-top-left-radius: 0.5em; padding: 0.5em; margin: 0 0 1em;} +h3{color:#ccc; font-size: 250%} pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2);} @@ -11,8 +12,8 @@ footer{background:rgba(0,0,0,0.03); margin-top: 4em; text-align: right;padding: .content{margin: 0 1em;} .legend{background: #fff; padding: 1em;} -.error{background: #fcc;} -.warning{color:#651; background:#fec;} +.error{background: #fcc; padding: 0.2em 1em;} +.warning{color:#651; background:#fec; padding: 0.2em 1em;} .cfgtype-alias{color:#89a;} .http-301::after{color:#a55; content: ' (Moved Permanently)'} diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php index abd763837213890d7c8c8a56d41bde9eeee9e27c..7513e18e30ed4e2157056a8b230c3cb51c91f146 100644 --- a/public_html/classes/redirect.admin.class.php +++ b/public_html/classes/redirect.admin.class.php @@ -17,6 +17,7 @@ require_once 'redirect.class.php'; * 2022-02-03 v1.5 ah add method isEnabled * 2022-05-23 v1.6 ah add http head check+render output; * 2022-05-31 v1.7 ah optical changes + * 2023-08-28 v1.8 ah remove php warning if there is no config yet */ /** @@ -149,21 +150,23 @@ class redirectadmin extends redirect { } } $aAliases=$this->_getAliases(); - foreach($aAliases as $sAlias=>$sConfig){ - if(isset($aReturn[$sAlias])){ - $aErrors[]="alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority)."; - } else { - if(!isset($aReturn[$sConfig])){ - $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist."; + if(is_array($aAliases) && count($aAliases)){ + foreach($aAliases as $sAlias=>$sConfig){ + if(isset($aReturn[$sAlias])){ + $aErrors[]="alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority)."; } else { - $aReturn[$sConfig]['aliases'][]=$sAlias; - $aReturn[$sAlias]=array( - 'type'=>'alias', - 'target'=>$sConfig, - 'ip'=> $this->_getIp($sAlias), - ); - if (!$aReturn[$sAlias]['ip']){ - $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias; + if(!isset($aReturn[$sConfig])){ + $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist."; + } else { + $aReturn[$sConfig]['aliases'][]=$sAlias; + $aReturn[$sAlias]=array( + 'type'=>'alias', + 'target'=>$sConfig, + 'ip'=> $this->_getIp($sAlias), + ); + if (!$aReturn[$sAlias]['ip']){ + $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias; + } } } } diff --git a/public_html/classes/redirect.class.php b/public_html/classes/redirect.class.php index 7d9231d5ce910c331abd37bee4109d8d432ef320..c04108a2279046efa59d944753aa28b4d45c725b 100644 --- a/public_html/classes/redirect.class.php +++ b/public_html/classes/redirect.class.php @@ -23,6 +23,7 @@ * 2019-04-25 v1.2 ah use REQUEST_URI (works on Win and Linux) * 2020-05-06 v1.3 ah added aliases for multiple domains with the same config * 2020-05-06 v1.4 ah rewrite as class + * 2023-08-28 v1.5 ah fix loop over config with missing regex section. */ /** @@ -229,13 +230,14 @@ class redirect { $aRedirect = $this->aConfig['direct'][$this->sRequest]; } else { $this->_wd("no direct match ... scanning regex"); - foreach (array_keys($this->aConfig['regex']) as $sRegex) { - - $this->_wd("check if regex [$sRegex] matches $this->sRequest"); - if (preg_match('#' . $sRegex . '#', $this->sRequest)) { - $this->_wd("REGEX MATCH! aborting tests"); - $aRedirect = $this->aConfig['regex'][$sRegex]; - break; + if(isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])){ + foreach (array_keys($this->aConfig['regex']) as $sRegex) { + $this->_wd("check if regex [$sRegex] matches $this->sRequest"); + if (preg_match('#' . $sRegex . '#', $this->sRequest)) { + $this->_wd("REGEX MATCH! aborting tests"); + $aRedirect = $this->aConfig['regex'][$sRegex]; + break; + } } } } diff --git a/readme.md b/readme.md index 71ad0b2ece2760ad69c4c4b2984b5d407d8dff6f..da271843ca6810186ea9b715a5dafc14612803c4 100644 --- a/readme.md +++ b/readme.md @@ -8,3 +8,7 @@ Author: Axel Hahn; Institute for Medical Education; University of Bern 📄 Source: <https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler> \ 📜 License: GNU GPL 3.0 \ 📖 Docs: <https://os-docs.iml.unibe.ch/redirect-handler/> or see the [docs](./docs) + +- - - + + \ No newline at end of file