diff --git a/docker/.env b/docker/.env index 31d8948d624acd82a12edd2a83bccaa1315cac0e..ecf872733b33c79e687f16cf9f38fcfb32f13d3e 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ # ====================================================================== # -# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e +# GENERATED BY init.sh - template: templates/dot_env - e2cde05722688ff85d3a93e9cd55787e # values to be used in docker-composer.yml # # ====================================================================== diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile index 31263885c9f8b327a89409308bd918e7787a82f4..75b09744aa9c284855cb84e6cbe0340e3027fdb3 100644 --- a/docker/containers/web-server/Dockerfile +++ b/docker/containers/web-server/Dockerfile @@ -1,7 +1,7 @@ # -# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 +# GENERATED BY init.sh - template: templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 # -FROM php:8.2-apache +FROM php:8.3-apache # install packages RUN apt-get update && apt-get install -y git unzip zip diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf index 7ed4d6fc28954f0e749328a68bc927f268fa8b3c..77c28584763a0df629562da2ae1a68efdeaf4224 100644 --- a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf +++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf @@ -1,5 +1,5 @@ # -# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9 +# GENERATED BY init.sh - template: templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9 # <VirtualHost *:80> DocumentRoot /var/www/my_new_app/public_html diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini index aa13bd779afa40bbfa25f10adef9baeae6d14f7d..8fc56969fb24b14a03349e4d16746d7607f67c2e 100644 --- a/docker/containers/web-server/php/extra-php-config.ini +++ b/docker/containers/web-server/php/extra-php-config.ini @@ -1,5 +1,5 @@ ; -; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2 +; GENERATED BY init.sh - template: templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2 ; [PHP] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9b5d9940d606b8c37485cad39e0a4393efc06120..4fa133ff15feb1815a239591c1f718fa7cc5d21c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,5 @@ # -# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b +# GENERATED BY init.sh - template: templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b # # ====================================================================== # @@ -19,7 +19,7 @@ services: build: context: . dockerfile: ./containers/web-server/Dockerfile - image: "php:8.2-apache" + image: "php:8.3-apache" container_name: 'my_new_app-server' ports: - '${APP_PORT}:80' diff --git a/docker/init.sh b/docker/init.sh index 060db24942ad10e1bb0f3abca96443d85f55d289..d75223516bd09e854db9378231f85b42b1ba4e70 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -4,53 +4,271 @@ # 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) +# 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) +# 2023-11-10 v1.7 <axel.hahn@unibe.ch> replace docker-compose with "docker compose" +# 2023-11-13 v1.8 <axel.hahn@unibe.ch> UNDO "docker compose"; update infos +# 2023-11-15 v1.9 <axel.hahn@unibe.ch> add help; execute multiple actions by params; new menu item: open app +# 2023-12-07 v1.10 <www.axel-hahn.de> simplyfy console command; add php linter +# 2024-07-01 v1.11 <www.axel-hahn.de> diff with colored output; suppress errors on port check +# 2024-07-19 v1.12 <axel.hahn@unibe.ch> apply shell fixes +# 2024-07-22 v1.13 <axel.hahn@unibe.ch> show info if there is no database container; speedup replacements +# 2024-07-22 v1.14 <axel.hahn@unibe.ch> show colored boxes with container status +# 2024-07-24 v1.15 <axel.hahn@unibe.ch> update menu output +# 2024-07-26 v1.16 <axel.hahn@unibe.ch> hide unnecessary menu items (WIP) +# 2024-07-29 v1.17 <www.axel-hahn.de> hide unnecessary menu items; reorder functions +# 2024-08-14 v1.18 <www.axel-hahn.de> update container view +# 2024-09-20 v1.19 <www.axel-hahn.de> detect dockerd-rootless (hides menu item to set permissions) # ====================================================================== -cd $( dirname $0 ) -. $( basename $0 ).cfg +cd "$( dirname "$0" )" || exit 1 + +# init used vars +gittarget= +frontendurl= + +_self=$( basename "$0" ) + +# shellcheck source=/dev/null +. "${_self}.cfg" || exit 1 + +_version="1.19" # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git selfgitrepo="docker-php-starterkit.git" -_version="1.6" +fgGray="\e[1;30m" +fgRed="\e[31m" +fgGreen="\e[32m" +fgBrown="\e[33m" +fgBlue="\e[34m" + +fgInvert="\e[7m" +fgReset="\e[0m" + +# ----- status varsiables +# running containers +DC_WEB_UP=0 +DC_DB_UP=0 + +# repo of docker-php-starterkit is here? +DC_REPO=1 + +DC_CONFIG_CHANGED=0 + +DC_WEB_URL="" + +isDockerRootless=0 +ps -ef | grep dockerd-rootless | grep -q $USER && isDockerRootless=1 # ---------------------------------------------------------------------- # FUNCTIONS # ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- +# STATUS FUNCTIONS + +# get container status and set global variable DC_REPO +# DC_REPO = 0 nothing to do - repo was changed to project +# DC_REPO = 1 if repo is in selfgitrepo (must be deleted) +function _getStatus_repo(){ + DC_REPO=0 + git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo && DC_REPO=1 +} + +# check if any of the templates has a change that must be applied +function _getStatus_template(){ + _generateFiles "dryrun" +} + +# get container status and set global variables +# DC_WEB_UP - web container +# DC_DB_UP - database container +# 0 = down +# 1 = up +function _getStatus_docker(){ + local _out + _out=$( docker-compose -p "$APP_NAME" ps) + + DC_WEB_UP=0 + DC_DB_UP=0 + grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1 + grep -q "${APP_NAME}-db" <<< "$_out" && DC_DB_UP=1 +} + +function _getWebUrl(){ + DC_WEB_URL="$frontendurl" + grep -q "${APP_NAME}-server" /etc/hosts && DC_WEB_URL="https://${APP_NAME}-server/" +} + +# ---------------------------------------------------------------------- +# OUTPUT + # draw a headline 2 function h2(){ echo - echo -e "\e[33m>>>>> $*\e[0m" + echo -e "$fgBrown>>>>> $*$fgReset" } # draw a headline 3 function h3(){ echo - echo -e "\e[34m----- $*\e[0m" + echo -e "$fgBlue----- $*$fgReset" } -# 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} -# } +# helper for menu: print an inverted key +function _key(){ + echo -en "\e[4;7m ${1} \e[0m" +} + +# show menu in interactive mode and list keys in help with param -h +# param string optional: set to "all" to show all menu items +function showMenu(){ + + local _bAll=0 + test -n "$1" && _bAll=1 + + local _spacer=" " + + echo + if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key g ) - remove git data of starterkit" + echo + fi + + if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key i ) - init application: set permissions" + fi + + if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key t ) - generate files from templates" + fi + if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key T ) - remove generated files" + fi + echo + if [ $DC_WEB_UP -eq 0 ] || [ $_bAll -eq 1 ]; then + if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key u ) - startup containers docker-compose ... up -d" + echo "${_spacer}$( _key U ) - startup containers docker-compose ... up -d --build" + echo + echo "${_spacer}$( _key r ) - remove containers docker-compose rm -f" + fi + fi + if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + echo "${_spacer}$( _key s ) - shutdown containers docker-compose stop" + echo + echo "${_spacer}$( _key m ) - more infos" + echo "${_spacer}$( _key o ) - open app [${APP_NAME}] $DC_WEB_URL" + echo "${_spacer}$( _key c ) - console (bash)" + echo "${_spacer}$( _key p ) - console check with php linter" + fi + echo + echo "${_spacer}$( _key q ) - quit" + +} +function showHelp(){ + cat <<EOH + +INITIALIZER FOR DOCKER APP v$_version + +A helper script written in Bash to bring up a PHP+Mysql application in docker. + +📄 Source : https://git-repo.iml.unibe.ch/iml-open-source/docker-php-starterkit +📗 Docs : https://os-docs.iml.unibe.ch/docker-php-starterkit/ +📜 License: GNU GPL 3.0 +(c) Institute for Medical Education; University of Bern + + +SYNTAX: + $_self [-h|-v] + $_self [menu key [.. menu key N]] + +OPTIONS: + -h show this help and exit + -v show version exit + +MENU KEYS: + In the interactive menu are some keys to init an action. + The same keys can be put as parameter to start this action. + You can add multiples keys to apply multiple actions. + +$( showMenu "all" ) + +EXAMPLES: + + $_self starts interactive mode + $_self u bring up docker container(s) and stay in interactive mode + $_self i q set write permissions and quit + $_self p q start php linter and exit + +EOH +} + + +# show urls for app container +function _showBrowserurl(){ + echo "In a web browser open:" + echo " $DC_WEB_URL" +} + +# detect + show ports and urls for app container and db container +function _showInfos(){ + _showContainers long + h2 INFO + + h3 "processes webserver" + # docker-compose top + docker top "${APP_NAME}-server" + if [ ! "$DB_ADD" = "false" ]; then + h3 "processes database" + docker top "${APP_NAME}-db" + fi + + h3 "What to open in browser" + if echo >"/dev/tcp/localhost/${APP_PORT}"; then + # echo "OK, app port ${APP_PORT} is reachable" + # echo + _showBrowserurl + else + echo "ERROR: app port ${APP_PORT} is not available" + fi 2>/dev/null + + if [ "$DB_ADD" != "false" ]; then + h3 "Check database port" + if echo >"/dev/tcp/localhost/${DB_PORT}"; then + echo "OK, db port ${DB_PORT} is reachable" + echo + echo "In a local DB admin tool you can connect it:" + echo " host : localhost" + echo " port : ${DB_PORT}" + echo " user : root" + echo " password: ${MYSQL_ROOT_PASS}" + else + echo "NO, db port ${DB_PORT} is not available" + fi 2>/dev/null + + fi + echo +} + +# ---------------------------------------------------------------------- +# ACTIONS # 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 + local _user; _user=$( id -gn ) + local _user_uid; typeset -i _user_uid=0 + + test -f /etc/subuid && _user_uid=$( grep "$_user" /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1 + local DOCKER_USER_OUTSIDE; typeset -i DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID set -vx @@ -62,10 +280,10 @@ function _setWritepermissions(){ 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}" + 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}" + sudo setfacl -Rm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}" done set +vx @@ -76,11 +294,10 @@ 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 + if git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo; then echo echo -n "Delete local .git and .gitignore? [y/N] > " - read answer + read -r answer test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore ) else echo "It was done already - $selfgitrepo was not found." @@ -92,36 +309,64 @@ function _removeGitdata(){ # 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 [ "$DB_ADD" = "false" ]; then + local iStart; typeset -i iStart + iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1 if [ $iStart -gt 0 ]; then - sed -ni "1,${iStart}p" ${_file} + sed -ni "1,${iStart}p" "${_file}" fi fi } +# helper function to generate replacements using sed +# it loops over all vars in the config file +# used in _generateFiles +function _getreplaces(){ + # loop over vars to make the replacement + grep "^[a-zA-Z]" "$_self.cfg" | while read -r line + do + # echo replacement: $line + mykey=$( echo "$line" | cut -f 1 -d '=' ) + myvalue="$( eval echo \"\$"$mykey"\" )" + + # TODO: multiline values fail here in replacement with sed + echo -e "s#{{$mykey}}#${myvalue}#g" + + done +} + # 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 +# If the 1st parameter is set to "dryrun" it will not generate files. +# param string dryrun optional: set to "dryrun" to not generate files function _generateFiles(){ - # re-read config vars - . $( basename $0 ).cfg + local _dryrun="$1" + DC_CONFIG_CHANGED=0 + + # shellcheck source=/dev/null + . "${_self}.cfg" || exit 1 + + params=$( _getreplaces | while read -r line; do echo -n "-e '$line' "; done ) local _tmpfile=/tmp/newfilecontent$$.tmp - h2 "generate files from templates..." - for mytpl in $( ls -1 ./templates/* ) + + test "$_dryrun" = "dryrun" || h2 "generate files from templates..." + for mytpl in 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 }' ) + 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 + if [ "$_dryrun" != "dryrun" ]; then + echo "SKIP: $mytpl - target was not found in 1st line" + fi _doReplace=0 fi @@ -129,39 +374,37 @@ function _generateFiles(){ if [ $_doReplace -eq 1 ]; then # write file from line 2 to a tmp file - sed -n '2,$p' $mytpl >$_tmpfile + 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 + local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) + sed -i "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile + + # apply all replacements to the tmp file + eval sed -i "$params" "$_tmpfile" || exit + _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 + if diff --color=always "../$target" "$_tmpfile" 2>/dev/null | grep -v "$_md5" | grep -v "^---" | grep . || [ ! -f "../$target" ]; then + if [ "$_dryrun" = "dryrun" ] + then + DC_CONFIG_CHANGED=1 + else + echo -n "$mytpl - changes detected - writing [$target] ... " + mkdir -p "$( dirname ../"$target" )" || exit 2 + mv "$_tmpfile" "../$target" || exit 2 + echo OK + echo + fi else rm -f $_tmpfile - echo "SKIP: $mytpl - Nothing to do." + if [ "$_dryrun" != "dryrun" ]; then + echo "SKIP: $mytpl - Nothing to do." + fi fi fi - echo done } @@ -170,104 +413,116 @@ function _generateFiles(){ # a traget file. function _removeGeneratedFiles(){ h2 "remove generated files..." - for mytpl in $( ls -1 ./templates/* ) + for mytpl in templates/* do - h3 $mytpl + h3 "$mytpl" # fetch traget file from first line - target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) + target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) - if [ ! -z "$target" -a -f "../$target" ]; then + if [ -n "$target" ] && [ -f "../$target" ]; then echo -n "REMOVING " ls -l "../$target" || exit 2 rm -f "../$target" || exit 2 echo OK else - echo SKIP: $target + echo "SKIP: $target" fi done } + +# show running containers function _showContainers(){ local bLong=$1 - h2 CONTAINERS - if [ -z "$bLong" ]; then - docker-compose -p "$APP_NAME" ps - else - docker ps | grep $APP_NAME + + local _out + + local sUp=".. UP" + local sDown=".. down" + + local Status= + local StatusWeb="$sDown" + local StatusDb="$sDown" + local colWeb= + local colDb= + + colDb="$fgRed" + colWeb="$fgRed" + + if [ $DC_WEB_UP -eq 1 ]; then + colWeb="$fgGreen" + StatusWeb="$sUp" + fi + + if [ $DC_DB_UP -eq 1 ]; then + colDb="$fgGreen" + StatusDb="$sUp" fi -} + if [ "$DB_ADD" = "false" ]; then + colDb="$fgGray" + local StatusDb=".. N/A" + Status="This app has no database container." + fi -# a bit stupid ... i think I need to delete it. -function _showInfos(){ - _showContainers long - h2 INFO + h2 CONTAINERS - h3 "processes" - docker-compose top + echo + printf " $colWeb$fgInvert %-32s $fgReset $colDb$fgInvert %-32s $fgReset\n" "WEB ${StatusWeb}" "DB ${StatusDb}" + printf " %-32s $fgReset %-32s $fgReset\n" "PHP ${APP_PHP_VERSION}" "${MYSQL_IMAGE}" + printf " %-32s $fgReset %-32s $fgReset\n" ":${APP_PORT}" ":${DB_PORT}" + + echo - h3 "Check app port" - >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && ( - echo "OK, app port ${APP_PORT} is reachable" + if [ -n "$Status" ]; then + echo " $Status" 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" + fi + + if [ -n "$bLong" ]; then + echo "$_out" + + h2 STATS + docker stats --no-stream echo - echo "In a local DB admin tool:" - echo " host : localhost" - echo " port : ${DB_PORT}" - echo " user : root" - echo " password: ${MYSQL_ROOT_PASS}" - ) - echo -} + fi -# 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 + local _wait=15 + echo -n "... press RETURN ... or wait $_wait sec > "; read -r -t $_wait + echo } # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- -action=$1 +action=$1; shift 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 + _getStatus_repo + _getStatus_docker + _getStatus_template + _getWebUrl - _showContainers + if [ -z "$action" ]; then - h2 MENU - echo " $( _key g ) - remove git data of starterkit" + echo "_______________________________________________________________________________" echo - echo " $( _key i ) - init application: set permissions" - echo " $( _key t ) - generate files from templates" - echo " $( _key T ) - remove generated files" + printf " %-70s ______\n" "${APP_NAME^^} :: Initializer for docker" + echo "________________________________________________________________________/ $_version" 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" + + _showContainers + + h2 MENU + showMenu echo echo -n " select >" read -rn 1 action @@ -275,6 +530,8 @@ while true; do fi case "$action" in + "-h") showHelp; exit 0 ;; + "-v") echo "$_self $_version"; exit 0 ;; g) _removeGitdata ;; @@ -289,51 +546,84 @@ while true; do _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" + h2 "Bring up..." + dockerUp="docker-compose -p $APP_NAME --verbose up -d --remove-orphans" if [ "$action" = "U" ]; then dockerUp+=" --build" fi + echo "$dockerUp" if $dockerUp; then - echo "In a web browser:" - echo " $frontendurl" + _showBrowserurl else echo "ERROR: docker-compose up failed :-/" docker-compose -p "$APP_NAME" logs | tail fi echo - _wait ;; s) + h2 "Stopping..." docker-compose -p "$APP_NAME" stop ;; r) + h2 "Removing..." 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 + h2 "Console" + _containers=$( docker-compose -p "$APP_NAME" ps | sed -n "2,\$p" | awk '{ print $1}' ) + if [ "$DB_ADD" = "false" ]; then + dockerid=$_containers + else + echo "Select a container:" + sed "s#^# #g" <<< "$_containers" + echo -n "id or name >" + read -r dockerid + fi + test -z "$dockerid" || ( + echo + echo "> docker exec -it $dockerid /bin/bash (type 'exit' + Return when finished)" + docker exec -it "$dockerid" /bin/bash + ) + ;; + p) + h2 "PHP $APP_PHP_VERSION linter" + + dockerid="${APP_NAME}-server" + echo -n "Scanning ... " + typeset -i _iFiles + _iFiles=$( docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' " | wc -l ) + + if [ $_iFiles -gt 0 ]; then + echo "found $_iFiles [*.php] files ... errors from PHP $APP_PHP_VERSION linter:" + time if echo "$APP_PHP_VERSION" | grep -E "([567]\.|8\.[012])" >/dev/null ; then + docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' -exec php -l {} \; | grep -v '^No syntax errors detected'" + else + docker exec -it "$dockerid" /bin/bash -c "php -l \$( find . -name '*.php' ) | grep -v '^No syntax errors detected' " + fi + echo + _wait + else + echo "Start your docker container first." + fi + ;; + o) + h2 "Open app ..." + xdg-open "$DC_WEB_URL" ;; q) + h2 "Bye!" exit 0; ;; *) test -n "$action" && ( echo " ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 ) esac - action= + action=$1; shift 1 done diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg index 7c781c5f4288fe3acf990cd7ad6db2241f78a9bd..e30c69e0a9664ebff0532f299a6af60826959057 100644 --- a/docker/init.sh.cfg +++ b/docker/init.sh.cfg @@ -17,7 +17,7 @@ APP_APT_PACKAGES="git unzip zip" #APP_APACHE_MODULES="rewrite" APP_APACHE_MODULES="" -APP_PHP_VERSION=8.2 +APP_PHP_VERSION=8.3 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug" # APP_PHP_MODULES="curl mbstring xml zip xdebug" APP_PHP_MODULES="xdebug" diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md" index 99773cfd6df1ecfd8aa573663ab5c07fa1af4797..372e2f38a6168e613b1b932853b13353859d53a6 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+ (up to PHP 8.2) +* PHP 8 (up to PHP 8.3) * Webserver (docs describe usage for Apache httpd) ## Features diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php index 7513e18e30ed4e2157056a8b230c3cb51c91f146..d11fbe0ffcca843ff61d96ecb6247703e3833291 100644 --- a/public_html/classes/redirect.admin.class.php +++ b/public_html/classes/redirect.admin.class.php @@ -18,6 +18,7 @@ require_once 'redirect.class.php'; * 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 + * 2024-10-03 v1.9 ah php8 only: typed variables */ /** @@ -25,39 +26,46 @@ require_once 'redirect.class.php'; * * @author axel */ -class redirectadmin extends redirect { +class redirectadmin extends redirect +{ - protected function _getCurlOptions(){ - $aReturn=array( + /** + * Get default curl options + * @return array + */ + protected function _getCurlOptions(): array + { + $aReturn = [ CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => strip_tags($this->sAbout), CURLOPT_VERBOSE => false, CURLOPT_ENCODING => 'gzip, deflate', // to fetch encoding - CURLOPT_HTTPHEADER => array( + CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: en', 'DNT: 1', - ), + ], // TODO: this is unsafe .. better: let the user configure it CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 5, - ); + ]; return $aReturn; } /** - * make a single http(s) get request and return the response body + * Make a single http(s) get request and return the response body * @param string $url url to fetch * @param boolean $bHeaderOnly optional: true=make HEAD request; default: false (=GET) * @return string */ - public function httpGet($url, $bHeaderOnly = false) { + public function httpGet(string $url, bool $bHeaderOnly = false): bool|string + { $ch = curl_init($url); - foreach ($this->_getCurlOptions() as $sCurlOption=>$sCurlValue){ + foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) { curl_setopt($ch, $sCurlOption, $sCurlValue); } if ($bHeaderOnly) { @@ -69,113 +77,124 @@ class redirectadmin extends redirect { return ($res); } - public function renderHttpResponseHeader($sHeader){ - $sReturn=$sHeader; - if(!$sReturn){ - $sReturn='<pre><span class="status status-error">Request failed. </span><br>' - .'No data... no response.<br>' - .'Maybe ... ' - .'<ul>' - .'<li>the nostname does not exist ... </li>' - .'<li>or there is a network problem ... or </li>' - .'<li>the webservice on the target system does not run.</li>' - .'</ul>' - .'</pre>' + /** + * Get html code for a response header of a request + * + * @param string $sHeader + * @return string + */ + public function renderHttpResponseHeader(string $sHeader): string + { + $sReturn = $sHeader; + if (!$sReturn) { + $sReturn = '<pre><span class="status status-error">Request failed. </span><br>' + . 'No data... no response.<br>' + . 'Maybe ... ' + . '<ul>' + . '<li>the nostname does not exist ... </li>' + . '<li>or there is a network problem ... or </li>' + . '<li>the webservice on the target system does not run.</li>' + . '</ul>' + . '</pre>' ; } else { - $sReturn=preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn); - $sReturn=preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn); - $sReturn=preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn); + $sReturn = preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn); + $sReturn = preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn); + $sReturn = preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn); - $sReturn=preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn); - $sReturn=preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn); - $sReturn.='</pre>'; + $sReturn = preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn); + $sReturn = preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn); + $sReturn .= '</pre>'; preg_match_all('/(HTTP\/.*)/i', $sReturn, $aTmp); // $sReturn.=print_r($aTmp, 1); - $iHops=(count($aTmp[0])-1); + $iHops = (count($aTmp[0]) - 1); - $sReturn=($iHops>0 - ? 'Found hops: <strong>'.$iHops.'</strong>' - .($iHops>1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '' ).'<br><br>' + $sReturn = ($iHops > 0 + ? 'Found hops: <strong>' . $iHops . '</strong>' + . ($iHops > 1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '') . '<br><br>' : '' - ).$sReturn - ; + ) . $sReturn + ; } return $sReturn; } /** - * check if admin is enabled + * Check if admin is enabled + * * @return bool */ - public function isEnabled(){ - $sFile2Enable=__DIR__ . '/'.basename(__FILE__).'_enabled.txt'; + public function isEnabled(): bool + { + $sFile2Enable = __DIR__ . '/' . basename(__FILE__) . '_enabled.txt'; return file_exists($sFile2Enable); } /** - * get ip address of a given hostname as ip (string) or false + * Get ip address of a given hostname as ip (string) or false * @return string|bool */ - protected function _getIp($sHostname){ - $sIp=gethostbyname($sHostname); - return $sIp===$sHostname ? false : $sIp; + protected function _getIp(string $sHostname): string + { + $sIp = gethostbyname($sHostname); + return $sIp === $sHostname ? '' : $sIp; } /** - * get an array with all config entries in all json files + * Get an array with all config entries in all json files * including some error checking + * * @return array */ - public function getHosts(){ - $aReturn = array(); - $aErrors = array(); - foreach(glob($this->sConfigDir . '/redirects_*.json') as $sFilename){ - $sMyHost= str_replace(array('redirects_', '.json'), array('',''), basename($sFilename)); - $aReturn[$sMyHost]=array( - 'type'=>'config', - 'file'=>$sFilename, - 'ip'=> $this->_getIp($sMyHost), - 'aliases'=>array(), - 'redirects'=>json_decode(file_get_contents($sFilename), 1), - ); - if (!$aReturn[$sMyHost]['ip']){ - $aErrors[]=basename($sFilename).': The hostname was not found in DNS: '.$sMyHost; + public function getHosts(): array + { + $aReturn = []; + $aErrors = []; + foreach (glob($this->sConfigDir . '/redirects_*.json') as $sFilename) { + $sMyHost = str_replace(['redirects_', '.json'], ['', ''], basename($sFilename)); + $aReturn[$sMyHost] = [ + 'type' => 'config', + 'file' => $sFilename, + 'ip' => $this->_getIp($sMyHost), + 'aliases' => [], + 'redirects' => json_decode(file_get_contents($sFilename), 1), + ]; + if (!$aReturn[$sMyHost]['ip']) { + $aErrors[] = basename($sFilename) . ': The hostname was not found in DNS: ' . $sMyHost; } } - $aAliases=$this->_getAliases(); - 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)."; + $aAliases = $this->_getAliases(); + 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 { - if(!isset($aReturn[$sConfig])){ - $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist."; + 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; + $aReturn[$sConfig]['aliases'][] = $sAlias; + $aReturn[$sAlias] = [ + 'type' => 'alias', + 'target' => $sConfig, + 'ip' => $this->_getIp($sAlias), + ]; + if (!$aReturn[$sAlias]['ip']) { + $aErrors[] = 'alias.json: The hostname was not found in DNS: ' . $sAlias; } } } } } - $aReturn['_errors']=$aErrors; + $aReturn['_errors'] = $aErrors; ksort($aReturn); return $aReturn; } - } diff --git a/public_html/classes/redirect.class.php b/public_html/classes/redirect.class.php index c04108a2279046efa59d944753aa28b4d45c725b..168151820d4dfb956b28d09f83b9ced86a023f86 100644 --- a/public_html/classes/redirect.class.php +++ b/public_html/classes/redirect.class.php @@ -24,6 +24,7 @@ * 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. + * 2024-10-03 v1.6 ah php8 only: typed variables */ /** @@ -31,86 +32,156 @@ * * @author axel */ -class redirect { +class redirect +{ // ---------------------------------------------------------------------- // CONFIG // ---------------------------------------------------------------------- - protected $bDebug = false; - protected $sConfigDir = __DIR__ . '/../../config'; - protected $sAbout = 'IML redirect <small>v1.4</small>'; + /** + * Flag: debug is enabled? + * @var bool + */ + protected bool $bDebug = false; + + /** + * configuration dir + * @var string + */ + protected string $sConfigDir = __DIR__ . '/../../config'; - protected $sHostname = false; - protected $sRequest = false; + /** + * About message + * @var string + */ + protected string $sAbout = 'IML redirect <small>v1.4</small>'; - protected $sCfgfile = false; - protected $aConfig = false; - protected $aRedirect = false; - - public $urlRepo='https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler'; - public $urlDocs='https://os-docs.iml.unibe.ch/redirect-handler/'; + /** + * Hostname + * @var string + */ + protected string $sHostname = ''; + + /** + * Request to handle and to redirect to aconfigured target url + * @var string + */ + protected string $sRequest = ''; + + /** + * Config file + * @var + */ + protected string $sCfgfile = ''; + + /** + * Config data for the current hostname + * @var + */ + protected array $aConfig = []; + + /** + * Redirect data for the current request + * @var array + */ + protected array $aRedirect = []; + + /** + * URL to the repository of this project + * @var string + */ + public string $urlRepo = 'https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler'; + + /** + * Url to the docs + * @var string + */ + public string $urlDocs = 'https://os-docs.iml.unibe.ch/redirect-handler/'; // ---------------------------------------------------------------------- // CONSTRUCTOR // ---------------------------------------------------------------------- - public function __constructor($sHostname=false, $sRequest=false) { - if($sHostname){ + + /** + * Constructor + * @param string $sHostname hostname + * @param string $sRequest request to proces + * @return void + */ + public function __constructor(string $sHostname = '', string $sRequest = '') + { + if ($sHostname) { $this->setHost($sHostname); } - if($sRequest){ + if ($sRequest) { $this->setRequest($sRequest); } - - return true; + } - + // ---------------------------------------------------------------------- // DEBUG // ---------------------------------------------------------------------- - + /** - * write a debug message into the header + * Write a debug message into the http response header + * * @global boolean $bDebug flag: show debug infos? * @staticvar int $i counter + * * @param string $sDebugMessage message to show * @return boolean */ - protected function _wd($sDebugMessage) { - + protected function _wd(string $sDebugMessage): bool + { + if (!$this->bDebug) { return false; } static $i; $i++; - header('X-DEBUG-' . $i . ': ' . $sDebugMessage); + header("X-DEBUG-$i: $sDebugMessage"); return true; } + // ---------------------------------------------------------------------- // SET INTERNAL VARS // ---------------------------------------------------------------------- + /** - * get a string with full path of a config file with aliases + * Get a string with full path of a config file with aliases * @return string */ - protected function _generateAliasfile() { + protected function _generateAliasfile(): string + { return $this->sConfigDir . '/aliases.json'; } + /** - * get a string with full path of a config file based on hostname - * @param string $sHostname + * Get a string with full path of a config file based on hostname + * + * @param string $sHostname hostname/ fqdn * @return string */ - protected function _generateCfgfile($sHostname) { - + protected function _generateCfgfile(string $sHostname): string + { + return $this->sConfigDir . '/redirects_' . $sHostname . '.json'; } - - protected function _getAliases(){ - $sAliasfile=$this->_generateAliasfile(); - $this->_wd('check alias file '.$sAliasfile); - if(!file_exists($sAliasfile)){ + + /** + * Get Aliases from aliases.json + * It returns false if no alias was found + * + * @return bool|array + */ + protected function _getAliases(): bool|array + { + $sAliasfile = $this->_generateAliasfile(); + $this->_wd('check alias file ' . $sAliasfile); + if (!file_exists($sAliasfile)) { $this->_wd('alias do not exist'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; @@ -120,49 +191,51 @@ class redirect { } /** - * get an array with redirect config based on a given hostname - * @param string $sHostname hostname - * @param boolean $bAbort flag: true = do not scan aliases (used for loop detection) - * @return array + * Get an array with redirect config based on a given hostname. + * @return bool */ - protected function _getConfig() { - $this->aConfig=false; - $this->aRedirect=false; + protected function _getConfig(): bool + { + $this->aConfig = []; + $this->aRedirect = []; $this->_getEffectiveConfigfile(); - if($this->sCfgfile){ - $aConfig=json_decode(file_get_contents($this->sCfgfile), 1); + if ($this->sCfgfile) { + $aConfig = json_decode(file_get_contents($this->sCfgfile), 1); if (!is_array($aConfig) || !count($aConfig)) { $this->_wd('no config available'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 02'); + } else { + $this->aConfig = $aConfig; } - $this->aConfig=$aConfig; } return true; } - + /** - * get an array with redirect config based on a given hostname + * Get filename of config file based on a given hostname * or detection in the alias config + * * @param string $sHostname hostname * @param boolean $bAbort flag: true = do not scan aliases (used for loop detection) - * @return array + * @return bool|string */ - protected function _getEffectiveConfigfile($sHostname=false, $bAbort=false) { - if(!$sHostname){ - $sHostname=$this->sHostname; + protected function _getEffectiveConfigfile(string $sHostname = '', bool $bAbort = false): bool|string + { + if (!$sHostname) { + $sHostname = $this->sHostname; } - $this->sCfgfile=false; + $this->sCfgfile = false; $sCfgfile = $this->_generateCfgfile($sHostname); - $this->_wd('check config '.$sCfgfile); + $this->_wd('check config ' . $sCfgfile); if (!file_exists($sCfgfile)) { - if($bAbort){ + if ($bAbort) { // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } $aAliases = $this->_getAliases(); - $this->_wd('checking aliases '.print_r($aAliases,1)); - if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))){ - $this->_wd('sorry no valid alias for '. $sHostname); + $this->_wd('checking aliases ' . print_r($aAliases, 1)); + if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))) { + $this->_wd('sorry no valid alias for ' . $sHostname); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } @@ -170,39 +243,42 @@ class redirect { return $this->_getEffectiveConfigfile($aAliases[$this->sHostname], 1); } - $this->sCfgfile=$sCfgfile; + $this->sCfgfile = $sCfgfile; return $sCfgfile; } - - + + /** - * enable/ disable debug + * Enable/ disable debug * @param boolean $bEnable */ - public function setDebug($bEnable){ - $this->bDebug=!!$bEnable; + public function setDebug(bool $bEnable): bool + { + $this->bDebug = !!$bEnable; return true; } - + /** - * set hostname; internally it detects the config too - * @param type $sHostname + * Set hostname; internally it detects the config too + * @param string $sHostname * @return boolean */ - public function setHost($sHostname){ - $this->sHostname=$sHostname; + public function setHost(string $sHostname): bool + { + $this->sHostname = $sHostname; $this->_getConfig(); return true; } - + /** - * set the request - * @param type $sRequest + * Set the request + * @param string $sRequest * @return boolean */ - public function setRequest($sRequest){ - $this->sRequest=$sRequest; - $this->aRedirect=false; + public function setRequest(string $sRequest): bool + { + $this->sRequest = $sRequest; + $this->aRedirect = false; return true; } @@ -211,26 +287,28 @@ class redirect { // ---------------------------------------------------------------------- /** - * get an array with the matching redirect; it returns false if none was + * Get an array with the matching redirect; it returns false if none was * detected + * * @return array */ - public function getRedirect(){ - if(is_array($this->aRedirect)){ + public function getRedirect(): array + { + if (is_array($this->aRedirect)) { return $this->aRedirect; } - - $aRedirect = false; - + + $aRedirect = []; + // remark: // $this->aConfig is set in setHost() with $this->_getConfig(); - + if (isset($this->aConfig['direct'][$this->sRequest])) { $this->_wd("DIRECT MATCH"); $aRedirect = $this->aConfig['direct'][$this->sRequest]; } else { $this->_wd("no direct match ... scanning regex"); - if(isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])){ + 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)) { @@ -241,21 +319,32 @@ class redirect { } } } - $this->aRedirect=$aRedirect; + $this->aRedirect = $aRedirect; return $aRedirect; } - - public function getRedirectCode($iNone=false){ - $aRedirect=$this->getRedirect(); + + /** + * Get 30x redirect code + * @param integer $iNone fallback value if no redirect status code was found + * @return integer + */ + public function getRedirectCode($iNone = 307): int + { + $aRedirect = $this->getRedirect(); return isset($aRedirect['code']) ? $aRedirect['code'] : $iNone; } - public function getRedirectTarget(){ - $aRedirect=$this->getRedirect(); - return isset($aRedirect['target']) ? $aRedirect['target'] : false; + + /** + * Get Target url of redirect + * @return string + */ + public function getRedirectTarget(): string + { + $aRedirect = $this->getRedirect(); + return $aRedirect['target'] ?? ''; } - - + // ---------------------------------------------------------------------- // FUNCTIONS - SEND DATA // ---------------------------------------------------------------------- @@ -264,27 +353,29 @@ class redirect { * make the redirect if it was found ... or a 404 * @return true */ - public function makeRedirect(){ - $sTarget=$this->getRedirectTarget(); - if(!$sTarget){ + public function makeRedirect(): bool + { + $sTarget = $this->getRedirectTarget(); + if (!$sTarget) { $this->_wd('send a not found'); $this->sendBody(404, '<h1>404 Not found</h1>'); } else { - $iCode=$this->getRedirectCode(); - $this->_wd("Redirect with http status [" . $iCode . "] to new Location: " . $sTarget); + $iCode = $this->getRedirectCode(); + $this->_wd("Redirect with http status [$iCode] to new Location: $sTarget"); $this->sendHttpStatusheader($iCode); header("Location: " . $sTarget); } return true; } - + /** * send http status header * @param integer $iCode http code * @return boolean */ - public function sendHttpStatusheader($iCode) { - $aHeaders = array( + public function sendHttpStatusheader(int $iCode): bool + { + $aHeaders = [ 301 => 'Moved Permanently', 302 => 'Found', // (Moved Temporarily) 303 => 'See other', // redirect with GET @@ -293,7 +384,7 @@ class redirect { 404 => 'Not found', 410 => 'Gone', 500 => 'Internal Server Error', - ); + ]; $iCode = (int) $iCode; if (!isset($aHeaders[$iCode])) { @@ -308,28 +399,29 @@ class redirect { * @see sendHttpStatusheader() * * @param integer $iCode http status code - * @param stringg $sBody message text as html code + * @param string $sBody message text as html code + * @return void */ - public function sendBody($iCode, $sBody) { + public function sendBody(int $iCode, string $sBody): void + { $this->sendHttpStatusheader($iCode); die('<!doctype html><html><head>' - . '<title>Redirect</title>' - . '<style>' - . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}' - . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}' - . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}' - . 'h2 small{font-size: 50%;}' - . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }' - . '</style>' - . '</head>' - . '<body>' - . $sBody - . '<h2>' . $this->sAbout . '</h2>' - . '<footer>© ' . date('Y') . ' '.$this->sHostname.'</footer>' - . '</body></html>' + . '<title>Redirect</title>' + . '<style>' + . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}' + . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}' + . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}' + . 'h2 small{font-size: 50%;}' + . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }' + . '</style>' + . '</head>' + . '<body>' + . $sBody + . '<h2>' . $this->sAbout . '</h2>' + . '<footer>© ' . date('Y') . ' ' . $this->sHostname . '</footer>' + . '</body></html>' ); } - }